Пример #1
0
    def configure_host(self, host, config):
        # Extract provisioning-specific config from host

        # Establish host-specific list of aliases and their public FQDNs.
        host.aliases.update({
            alias.strip(): f'{alias}.{host.name}.{self.target_host}'
            for alias in config.get('provision-aliases', '').strip().split()
        })

        # Development containers have a different internal address for the
        # external aliases so that we need to explicitly override the resolver
        # so that services like nginx can bind to the correct local address.
        try:
            addr = batou.utils.resolve(host.name)
            for alias_fqdn in host.aliases.values():
                output.annotate(f' alias override v4 {alias_fqdn} -> {addr}',
                                debug=True)
                batou.utils.resolve_override[alias_fqdn] = addr
        except (socket.gaierror, ValueError):
            pass

        try:
            addr = batou.utils.resolve_v6(host.name)
            for alias_fqdn in host.aliases.values():
                output.annotate(f' alias override v6 {alias_fqdn} -> {addr}',
                                debug=True)
                batou.utils.resolve_v6_override[alias_fqdn] = addr
        except (socket.gaierror, ValueError):
            pass

        host.provision_channel = config.get('provision-channel', self.channel)
Пример #2
0
    def verify(self):
        if self._delayed:
            self._render()
        try:
            with open(self.path, 'rb') as target:
                current = target.read()
                if current == self.content:
                    return
        except FileNotFoundError:
            current = b''
        except Exception:
            output.annotate('Unknown content - can\'t predict diff.')
            raise batou.UpdateNeeded()

        encoding = self.encoding or 'ascii'
        current_text = current.decode(encoding, errors='replace')
        wanted_text = self.content.decode(encoding)
        for line in difflib.unified_diff(current_text.splitlines(),
                                         wanted_text.splitlines()):
            line = line.replace('\n', '')
            if not line.strip():
                continue
            output.annotate('\t{} {}'.format(os.path.basename(self.path),
                                             line),
                            red=line.startswith('-'),
                            green=line.startswith('+'))
        raise batou.UpdateNeeded()
Пример #3
0
 def report(self):
     output.error(self.cmd)
     output.tabular("Return code", str(self.returncode), red=True)
     output.line('STDOUT', red=True)
     output.annotate(self.stdout)
     output.line('STDERR', red=True)
     output.annotate(self.stderr)
Пример #4
0
    def deploy(self, predict_only=False):
        # Remember: this is a tight loop - we need to keep this code fast.

        # Reset changed flag here to support triggering deploy() multiple
        # times. This is mostly helpful for testing, but who knows.
        self.changed = False
        for sub_component in self.sub_components:
            sub_component.deploy(predict_only)
            if sub_component.changed:
                self.changed = True

        if not os.path.exists(self.workdir):
            os.makedirs(self.workdir)
        with self.chdir(self.workdir), self:
            try:
                with batou.utils.Timer('{} verify()'.format(
                        self._breadcrumbs)):
                    self.verify()
            except AssertionError:
                self.__trigger_event__('before-update',
                                       predict_only=predict_only)
                output.annotate(self.host.name + ' > ' + self._breadcrumbs)
                if not predict_only:
                    self.update()
                self.changed = True
Пример #5
0
def main(destination, **kw):
    develop = os.environ['BATOU_DEVELOP']
    if develop:
        output.annotate(
            'Initializing with a development copy of batou will cause your '
            'project to have a reference outside its repository. '
            'Use at your own risk. ')
        develop = os.path.abspath(develop)
    print((
        'Bootstrapping new batou project in {}. This can take a while.'.format(
            os.path.abspath(destination))))
    if os.path.exists(destination):
        print(('{} exists already. Not copying template structure.'.format(
            destination)))
        os.chdir(destination)
    else:
        source = os.path.dirname(__file__) + '/init-template'
        shutil.copytree(source, destination)
        os.chdir(destination)
        cmd('hg -y init .')
    update_bootstrap(os.environ['BATOU_VERSION'], develop)
    # Need to clean up to avoid inheriting info that we're bootstrapped
    # already.
    for key in list(os.environ):
        if key.startswith('BATOU_'):
            del os.environ[key]
    cmd('./batou --help')
Пример #6
0
 def _ship(self, host):
     head = host.rpc.git_current_head()
     if head is None:
         bundle_range = self.branch
     else:
         head = head.decode("ascii")
         bundle_range = "{head}..{branch}".format(
             head=head, branch=self.branch)
     fd, bundle_file = tempfile.mkstemp()
     os.close(fd)
     out, err = cmd(
         "git bundle create {file} {range}".format(
             file=bundle_file, range=bundle_range),
         acceptable_returncodes=[0, 128])
     if "create empty bundle" in err:
         return
     change_size = os.stat(bundle_file).st_size
     output.annotate(
         "Sending {} bytes of changes".format(change_size), debug=True)
     rsync = execnet.RSync(bundle_file, verbose=False)
     rsync.add_target(host.gateway,
                      host.remote_repository + "/batou-bundle.git")
     rsync.send()
     os.unlink(bundle_file)
     output.annotate("Unbundling changes", debug=True)
     host.rpc.git_unbundle_code()
Пример #7
0
    def verify(self):
        with self.chdir(self.target):
            if not os.path.exists(".hg"):
                raise UpdateNeeded()

            if not self.vcs_update:
                return

            if self.has_outgoing_changesets():
                output.annotate(
                    "Hg clone at {} has outgoing changesets.".format(
                        self.target),
                    red=True,
                )

            if self.has_changes():
                output.annotate(
                    "Hg clone at {} is dirty, going to lose changes.".format(
                        self.target),
                    red=True,
                )
                raise UpdateNeeded()

            if self.revision:
                long_rev = len(self.revision) == 40
                if self.current_revision(long_rev) != self.revision:
                    raise UpdateNeeded()
            if self.branch and (self.current_branch() != self.branch
                                or self.has_incoming_changesets()):
                raise UpdateNeeded()
Пример #8
0
 def call(*args, **kw):
     output.annotate("rpc {}: {}(*{}, **{})".format(
         self.host.fqdn, name, args, kw),
                     debug=True)
     self.host.channel.send((name, args, kw))
     while True:
         message = self.host.channel.receive()
         output.annotate("{}: message: {}".format(
             self.host.fqdn, message),
                         debug=True)
         type = message[0]
         if type == "batou-result":
             return message[1]
         elif type == "batou-output":
             _, output_cmd, args, kw = message
             getattr(output, output_cmd)(*args, **kw)
         elif type == "batou-configuration-error":
             raise SilentConfigurationError()
         elif type == "batou-deployment-error":
             raise DeploymentError()
         elif type == "batou-unknown-error":
             output.error(message[1])
             raise RuntimeError(
                 "{}: Remote exception encountered.".format(
                     self.host.fqdn))
         elif type == "batou-error":
             # Remote put out the details already.
             raise RuntimeError(
                 "{}: Remote exception encountered.".format(
                     self.host.fqdn))
         else:
             raise RuntimeError("{}: Unknown message type {}".format(
                 self.host.fqdn, type))
Пример #9
0
 def update(self, host):
     source, target = self.root, host.remote_repository
     output.annotate("rsync: {} -> {}".format(source, target), debug=True)
     rsync = FilteredRSync(source, verbose=False)
     # We really want to use `delete=True` here but there's an execnet issue
     # preventing us to use it. See
     # https://github.com/flyingcircusio/batou/issues/107
     rsync.add_target(host.gateway, target)
     rsync.send()
Пример #10
0
    def verify(self):
        stdout, stderr = self.cmd('rsync {} {}{}/ {}'.format(
            self.verify_opts, self.exclude_arg, self.source, self.path))

        # In case of we see non-convergent rsync runs
        output.annotate('rsync result:', debug=True)
        output.annotate(stdout, debug=True)

        if len(stdout.strip().splitlines()) - 4 > 0:
            raise batou.UpdateNeeded()
Пример #11
0
 def expand(self, templatestr, args, identifier="<template>"):
     if len(templatestr) > 100 * 1024:
         output.error(
             "You are trying to render a template that is bigger than "
             "100KiB we've seen that Jinja can crash at large templates "
             "and suggest you find alternatives for this. The affected "
             "template starts with:")
         output.annotate(templatestr[:100])
     tmpl = self.env.from_string(templatestr)
     tmpl.filename = identifier
     return tmpl.render(**args)
Пример #12
0
 def __trigger_event__(self, event, predict_only):
     # We notify all components that belong to the same root.
     for target in self.root.component.recursive_sub_components:
         for handler in target._event_handlers.get(event, []):
             if not check_event_scope(handler._event['scope'], self,
                                      target):
                 continue
             if predict_only:
                 output.annotate('Trigger {}: {}.{}'.format(
                     event, handler.__self__, handler.__name__))
                 continue
             handler(self)
Пример #13
0
 def evade(self, component):
     if self.deployment == 'hot':
         return
     if self._evaded:
         return
     # Only try once. Keep going anyway.
     self._evaded = True
     output.annotate(
         "\u2623 Stopping {} for cold deployment".format(self.name))
     try:
         self.ctl('stop {}'.format(self.name))
     except Exception:
         pass
Пример #14
0
 def verify(self):
     if self._delayed:
         self._render()
     with open(self.path, 'rb') as target:
         current = target.read()
         if current == self.content:
             return
         if self.encoding:
             current_text = current.decode(self.encoding, errors='replace')
             wanted_text = self.content.decode(self.encoding)
             for line in difflib.unified_diff(current_text.splitlines(),
                                              wanted_text.splitlines()):
                 output.annotate(line, debug=True)
         raise batou.UpdateNeeded()
Пример #15
0
    def assert_component_is_current(self, requirements=[], **kw):
        """Assert that this component has been updated more recently
        than the components specified in the ``requirements``,
        raise :py:class:`UpdateNeeded` otherwise.

        :param list requirements: The list of components you want to
            check against.

        :return: ``None``, if this component is as new or newer as all
            ``requirements``.

        :param dict kw: Arguments that are passed through to
            each ``last_update`` call. The semantics depend on the components'
            implementations.

        :raises UpdateNeeded: if this component is older than any of the
            ``requirements``.

        The age of a component is determined by calling ``last_updated``
        on this and each requirement component.

        """

        if isinstance(requirements, Component):
            requirements = [requirements]
        reference = self.last_updated(**kw)
        if reference is None:
            output.annotate(
                "assert_component_is_current({}, ...): No reference".format(
                    self._breadcrumb),
                debug=True,
            )
            raise batou.UpdateNeeded()
        for requirement in requirements:
            self |= requirement
            required = requirement.last_updated(**kw)
            if required is None:
                continue
            if reference < required:
                output.annotate(
                    "assert_component_is_current({}, {}): {} < {}".format(
                        self._breadcrumb,
                        requirement._breadcrumb,
                        reference,
                        required,
                    ),
                    debug=True,
                )
                raise batou.UpdateNeeded()
Пример #16
0
    def update(self, host):
        env = self.environment
        blacklist = ['.batou', 'work', '.git', '.hg', '.vagrant', '.kitchen',
                     '.batou-lock']
        for candidate in os.listdir(env.base_dir):
            if candidate in blacklist:
                continue

            source = os.path.join(env.base_dir, candidate)
            target = os.path.join(host.remote_base, candidate)
            output.annotate("rsync: {} -> {}".format(source, target),
                            debug=True)
            rsync = execnet.RSync(source, verbose=False)
            rsync.add_target(host.gateway, target, delete=True)
            rsync.send()
Пример #17
0
def resolve(host, port=0, resolve_override=resolve_override):
    if host in resolve_override:
        address = resolve_override[host]
        output.annotate('resolved `{}` to {} (override)'.format(host, address))
    else:
        output.annotate('resolving `{}` (v4)'.format(host))
        responses = socket.getaddrinfo(host, int(port), socket.AF_INET)
        output.annotate('resolved `{}` to {}'.format(host, responses))
        address = responses[0][4][0]
        output.annotate('selected '.format(host, address))
    return address
Пример #18
0
def cmd(
    cmd,
    silent=False,
    ignore_returncode=False,
    communicate=True,
    env=None,
    acceptable_returncodes=[0],
    encoding="utf-8",
):
    if not isinstance(cmd, str):
        # We use `shell=True`, so the command needs to be a single string and
        # we need to pay attention to shell quoting.
        quoted_args = []
        for arg in cmd:
            arg = arg.replace("'", "\\'")
            if " " in arg:
                arg = "'{}'".format(arg)
            quoted_args.append(arg)
        cmd = " ".join(quoted_args)
    if env is not None:
        add_to_env = env
        env = os.environ.copy()
        env.update(add_to_env)
    output.annotate("cmd: {}".format(cmd), debug=True)
    process = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stdin=subprocess.PIPE,
        shell=True,
        env=env,
    )
    if not communicate:
        # XXX See #12550
        return process
    stdout, stderr = process.communicate()
    if encoding is not None:
        stdout = stdout.decode(encoding, errors="replace")
        stderr = stderr.decode(encoding, errors="replace")
    if process.returncode not in acceptable_returncodes:
        if not ignore_returncode:
            raise CmdExecutionError(cmd, process.returncode, stdout, stderr)
    return stdout, stderr
Пример #19
0
    def verify(self):
        self._force_clone = False
        if not os.path.exists(self.target):
            self._force_clone = True
            raise UpdateNeeded()
        # if the path does exist but isn't a directory, just let the error
        # bubble for now, the message will at least tell something useful
        with self.chdir(self.target):
            if not os.path.exists(".git"):
                self._force_clone = True
                raise UpdateNeeded()

            if self.remote_url() != self.url:
                self._force_clone = True
                raise UpdateNeeded()

            if not self.vcs_update:
                return

            if self.has_outgoing_changesets():
                output.annotate(
                    "Git clone at {} has outgoing changesets.".format(
                        self.target
                    )
                )

            if self.has_changes():
                output.annotate(
                    "Git clone at {} is dirty, going to lose changes.".format(
                        self.target
                    ),
                    red=True,
                )
                raise UpdateNeeded()

            if self.revision and self.current_revision() != self.revision:
                raise UpdateNeeded()
            if self.branch and (
                self.current_branch() != self.branch
                or self.has_incoming_changesets()
            ):
                raise UpdateNeeded()
Пример #20
0
    def connect(self, interpreter="python3"):
        if self.gateway:
            output.annotate("Disconnecting ...", debug=True)
            self.disconnect()

        output.annotate("Connecting ...", debug=True)
        # Call sudo, ensuring:
        # - no password will ever be asked (fail instead)
        # - we ensure a consistent set of environment variables
        #   irregardless of the local configuration of env_reset, etc.

        CONDITIONAL_SUDO = """\
if [ -n "$ZSH_VERSION" ]; then setopt SH_WORD_SPLIT; fi;
if [ \"$USER\" = \"{user}\" ]; then \
pre=\"\"; else pre=\"sudo -ni -u {user}\"; fi; $pre\
""".format(user=self.service_user)

        spec = "ssh={fqdn}//python={sudo} {interpreter}//type={method}".format(
            fqdn=self.fqdn,
            sudo=CONDITIONAL_SUDO,
            interpreter=interpreter,
            method=self.environment.connect_method,
        )
        if os.path.exists("ssh_config"):
            spec += "//ssh_config=ssh_config"
        self.gateway = execnet.makegateway(spec)
        try:
            self.channel = self.gateway.remote_exec(remote_core)
        except IOError:
            raise RuntimeError(
                "Could not start batou on host `{}`. "
                "The output above may contain more information. ".format(
                    self.fqdn))

        output.annotate("Connected ...", debug=True)
Пример #21
0
    def connect(self, interpreter='python3'):
        if self.gateway:
            output.annotate('Reconnecting ...', debug=True)
            self.disconnect()

        try_sudo = False
        try:
            self.gateway = execnet.makegateway(
                "ssh={}//python={}//type={}".format(
                    self.fqdn, interpreter,
                    self.environment.connect_method))
            self.channel = self.gateway.remote_exec(remote_core)
            if not self.environment.service_user:
                self.environment.service_user = self.rpc.whoami()
            elif self.rpc.whoami() != self.environment.service_user:
                try_sudo = True
                self.disconnect()
        except IOError:
            try_sudo = True

        if try_sudo:
            output.annotate('Trying to switch to sudo ...', debug=True)
            # Call sudo, ensuring:
            # - no password will ever be asked (fail instead)
            # - we ensure a consistent set of environment variables
            #   irregardless of the local configuration of env_reset, etc.
            self.gateway = execnet.makegateway(
                "ssh={}//python=sudo -ni -u {} {}//type={}".format(
                    self.fqdn,
                    self.environment.service_user,
                    interpreter,
                    self.environment.connect_method))
            self.channel = self.gateway.remote_exec(remote_core)

        output.annotate('Connected ...', debug=True)
Пример #22
0
    def verify(self):
        # Safety belt that we're acting on a clean repository.
        if self.environment.deployment.dirty:
            output.annotate(
                "You are running a dirty deployment. This can cause "
                "inconsistencies -- continuing on your own risk!", red=True)
            return

        try:
            status, _ = cmd('hg -q stat')
        except CmdExecutionError:
            output.error('Unable to check repository status. '
                         'Is there an HG repository here?')
            raise
        else:
            status = status.strip()
            if status.strip():
                output.error("Your repository has uncommitted changes.")
                output.annotate("""\
I am refusing to deploy in this situation as the results will be unpredictable.
Please commit and push first.
""", red=True)
                output.annotate(status, red=True)
                raise DeploymentError()
        try:
            cmd('hg -q outgoing -l 1', acceptable_returncodes=[1])
        except CmdExecutionError:
            output.error("""\
Your repository has outgoing changes.

I am refusing to deploy in this situation as the results will be unpredictable.
Please push first.
""")
            raise DeploymentError()
Пример #23
0
 def _ship(self, host):
     heads = host.rpc.hg_current_heads()
     if not heads:
         raise ValueError("Remote repository did not find any heads. "
                          "Can not continue creating a bundle.")
     fd, bundle_file = tempfile.mkstemp()
     os.close(fd)
     bases = " ".join("--base {}".format(x) for x in heads)
     cmd("hg -qy bundle {} {}".format(bases, bundle_file),
         acceptable_returncodes=[0, 1])
     change_size = os.stat(bundle_file).st_size
     if not change_size:
         return
     output.annotate(
         "Sending {} bytes of changes".format(change_size), debug=True)
     rsync = execnet.RSync(bundle_file, verbose=False)
     rsync.add_target(host.gateway,
                      host.remote_repository + "/batou-bundle.hg")
     rsync.send()
     os.unlink(bundle_file)
     output.annotate("Unbundling changes", debug=True)
     host.rpc.hg_unbundle_code()
Пример #24
0
def resolve_v6(host, port=0, resolve_override=resolve_v6_override):
    if host in resolve_override:
        address = resolve_override[host]
        output.annotate('resolved `{}` to {} (override)'.format(host, address))
    else:
        output.annotate('resolving (v6) `{}` (getaddrinfo)'.format(host))
        responses = socket.getaddrinfo(host, int(port), socket.AF_INET6)
        output.annotate('resolved (v6) `{}` to {}'.format(host, responses))
        address = None
        for _, _, _, _, sockaddr in responses:
            addr, _, _, _ = sockaddr
            if addr.startswith('fe80:'):
                continue
            address = addr
            break
        if not address:
            raise ValueError('No valid address found for `{}`.'.format(host))
        output.annotate('selected {}'.format(address))
    return address
Пример #25
0
    def verify(self):
        # Safety belt that we're acting on a clean repository.
        if self.environment.deployment.dirty:
            output.annotate(
                "You are running a dirty deployment. This can cause "
                "inconsistencies -- continuing on your own risk!",
                red=True,
            )
            return

        try:
            status, _ = cmd("git status --porcelain")
        except CmdExecutionError:
            output.error("Unable to check repository status. "
                         "Is there a Git repository here?")
            raise
        else:
            status = status.strip()
            if status.strip():
                output.error("Your repository has uncommitted changes.")
                output.annotate(
                    """\
I am refusing to deploy in this situation as the results will be unpredictable.
Please commit and push first.
""",
                    red=True,
                )
                output.annotate(status, red=True)
                raise DeploymentError()
        outgoing, _ = cmd(
            "git log {remote}/{branch}..{branch} --pretty=oneline".format(
                remote=self.remote, branch=self.branch),
            acceptable_returncodes=[0, 128],
        )
        if outgoing.strip():
            output.error("""\
Your repository has outgoing changes.

I am refusing to deploy in this situation as the results will be unpredictable.
Please push first.
""")
            raise DeploymentError()
Пример #26
0
    def connect(self, interpreter='python3'):
        if self.gateway:
            output.annotate('Disconnecting ...', debug=True)
            self.disconnect()

        output.annotate('Connecting ...', debug=True)
        # Call sudo, ensuring:
        # - no password will ever be asked (fail instead)
        # - we ensure a consistent set of environment variables
        #   irregardless of the local configuration of env_reset, etc.

        spec = "ssh={}//python=sudo -ni -u {} {}//type={}".format(
            self.fqdn, self.environment.service_user, interpreter,
            self.environment.connect_method)
        if os.path.exists('ssh_config'):
            spec += '//ssh_config=ssh_config'
        self.gateway = execnet.makegateway(spec)
        self.channel = self.gateway.remote_exec(remote_core)

        output.annotate('Connected ...', debug=True)
Пример #27
0
    def verify(self):
        # Safety belt that we're acting on a clean repository.
        if self.environment.deployment.dirty:
            output.annotate(
                "You are running a dirty deployment. This can cause "
                "inconsistencies -- continuing on your own risk!",
                red=True)
            return

        try:
            status = hg_cmd("hg stat")
        except CmdExecutionError:
            output.error("Unable to check repository status. "
                         "Is there an HG repository here?")
            raise
        else:
            if status:
                output.error("Your repository has uncommitted changes.")
                output.annotate(
                    """\
I am refusing to deploy in this situation as the results will be unpredictable.
Please commit and push first.
""",
                    red=True)
                for item in status:
                    output.annotate(
                        "{} {}".format(item['status'], item['path']), red=True)
                raise DeploymentError("Uncommitted changes")
        try:
            cmd("hg -q outgoing -l 1", acceptable_returncodes=[1])
        except CmdExecutionError:
            output.error("""\
Your repository has outgoing changes.

I am refusing to deploy in this situation as the results will be unpredictable.
Please push first.
""")
            raise DeploymentError("Outgoing changes")
Пример #28
0
 def log_finish_configure(self):
     for msg, args in self._logs:
         output.annotate(msg % args)
     self._logs = None
Пример #29
0
 def log(self, msg, *args):
     if self._logs is None:
         msg = '%s: %s' % (self.host.fqdn, msg)
         output.annotate(msg % args)
     else:
         self._logs.append((msg, args))
Пример #30
0
 def __exit__(self, exc1, exc2, exc3):
     self.duration = time.time() - self.started
     output.annotate(self.note + ' took %fs' % self.duration, debug=True)