Example #1
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)
Example #2
0
    def provision(self, host):
        container = host.name
        self._prepare_ssh(host)

        rsync_path = ''
        if host.environment.service_user:
            rsync_path = (
                f'--rsync-path="sudo -u {host.environment.service_user} '
                f'rsync"')
        env = {
            'PROVISION_CONTAINER': container,
            'PROVISION_HOST': self.target_host,
            'PROVISION_CHANNEL': host.provision_channel,
            'PROVISION_ALIASES': ' '.join(host.aliases.keys()),
            'SSH_CONFIG': self.ssh_config_file,
            'RSYNC_RSH': 'ssh -F {}'.format(self.ssh_config_file)
        }
        if self.rebuild:
            env['PROVISION_REBUILD'] = '1'
        # Add all component variables (uppercased) to the environment
        # $COMPONENT_<component>_<variable>
        # this includes environment overrides and secrets.
        for root_name, root in host.components.items():
            root = root.factory
            for name in dir(root):
                if name.startswith('_'):
                    continue
                if name in ['changed', 'workdir', 'namevar']:
                    continue
                value = getattr(root, name)
                if callable(value):
                    continue
                if isinstance(value, property):
                    continue
                key = f'COMPONENT_{root_name}_{name}'
                key = key.upper()
                env[key] = str(value)
            for name, value in host.environment.overrides.get(root_name,
                                                              {}).items():
                key = f'COMPONENT_{root_name}_{name}'
                key = key.upper()
                env[key] = str(value)

        seed_script = ''
        seed_basedir = f'environments/{host.environment.name}'
        seed_script_file = f'{seed_basedir}/provision.sh'
        if os.path.exists(seed_script_file):
            output.annotate(f'    Including {seed_script_file}')
            seed_raw_script = open(seed_script_file).read()
            seed_script += textwrap.dedent(f"""\
                # BEGIN CUSTOM SEED SCRIPT
                (
                    cd {seed_basedir}
                    {seed_raw_script}
                )
                # END CUSTOM SEED SCRIPT
                """)

        seed_nixos_file = f'environments/{host.environment.name}/provision.nix'
        if os.path.exists(
                seed_nixos_file) and 'provision.nix' not in seed_script:
            output.annotate(f'    Including {seed_nixos_file}')
            seed_script = textwrap.dedent(f"""\
                # BEGIN AUTOMATICALLY INCLUDED provision.nix
                (
                    cd {seed_basedir}
                    COPY provision.nix /etc/local/nixos/provision-container.nix
                )
                # END AUTOMATICALLY INCLUDED provision.nix
                """) + seed_script

        seed_script = seed_script.strip()
        if not seed_script:
            output.annotate(
                f'No provisioning code found in '
                f'environments/{host.environment.name}/provision.nix or '
                f'environments/{host.environment.name}/provision.sh. '
                f'This might be unintentional.',
                yellow=True)

        stdout = stderr = ''
        with tempfile.NamedTemporaryFile(mode='w+',
                                         prefix='batou-provision',
                                         delete=False) as f:
            try:
                os.chmod(f.name, 0o700)
                # We're placing the ENV vars directly in the script because
                # that helps debugging a lot. We need to be careful to
                # deleted it later, though, because it might contain secrets.
                f.write(
                    SEED_TEMPLATE.format(seed_script=seed_script,
                                         rsync_path=rsync_path,
                                         ENV='\n'.join(
                                             sorted(
                                                 'export {}="{}"'.format(k, v)
                                                 for k, v in env.items()))))
                f.close()
                stdout, stderr = cmd(f.name)
            except Exception:
                raise
            else:
                if '__FC_MANAGE_DEFECT_INDICATOR__' in stdout:
                    stdout = stdout.replace('__FC_MANAGE_DEFECT_INDICATOR__',
                                            '')
                    output.section('Errors detected during provisioning',
                                   red=True)
                    output.line('STDOUT')
                    output.annotate(stdout)
                    output.line('STDERR')
                    output.annotate(stderr)
                    output.line(
                        'WARNING: Continuing deployment optimistically '
                        'despite provisioning errors. Check errors above this '
                        'line first if encountering subsequent errors.',
                        yellow=True)
                else:
                    output.line("STDOUT", debug=True)
                    output.annotate(stdout, debug=True)
                    output.line("STDERR", debug=True)
                    output.annotate(stderr, debug=True)
            finally:
                # The script includes secrets so we must be sure that we delete
                # it.
                if output.enable_debug:
                    output.annotate((f'Not deleting provision script '
                                     f'{f.name} in debug mode!'),
                                    red=True)
                    os.unlink(f.name)
Example #3
0
    def verify(self, predicting=False):
        try:
            if self._delayed:
                self._render()
        except FileNotFoundError:
            if predicting:
                # During prediction runs we accept that delayed rending may
                # not yet work and that we will change. We might want to
                # turn this into an explicit flag so we don't implicitly
                # run into a broken deployment.
                assert False
            # If we are not predicting then this is definitely a problem.
            # Stop here.
            raise
        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()

        if self.encoding:
            current_text = current.decode(self.encoding, errors="replace")
            wanted_text = self.content.decode(self.encoding, errors="replace")

        if not self.encoding:
            output.annotate("Not showing diff for binary data.", yellow=True)
        elif self.sensitive_data:
            output.annotate("Not showing diff as it contains sensitive data.",
                            red=True)
        else:
            current_lines = current_text.splitlines()
            wanted_lines = wanted_text.splitlines()
            words = set(
                itertools.chain(*(x.split() for x in current_lines),
                                *(x.split() for x in wanted_lines)))
            contains_secrets = bool(
                self.environment.secret_data.intersection(words))

            diff = difflib.unified_diff(current_lines, wanted_lines)
            if not os.path.exists(self.diff_dir):
                os.makedirs(self.diff_dir)
            diff, diff_too_long, diff_log = limited_buffer(
                diff,
                self._max_diff,
                self._max_diff_lead,
                logdir=self.diff_dir)

            if diff_too_long:
                output.line(
                    f"More than {self._max_diff} lines of diff. Showing first "
                    f"and last {self._max_diff_lead} lines.",
                    yellow=True)
                output.line(f"see {diff_log} for the full diff.".format(),
                            yellow=True)
            if contains_secrets:
                output.line("Not showing diff as it contains sensitive data,",
                            yellow=True)
                output.line(f"see {diff_log} for the diff.".format(),
                            yellow=True)
            else:
                for line in diff:
                    line = line.replace("\n", "")
                    if not line.strip():
                        continue
                    output.annotate(f"  {os.path.basename(self.path)} {line}",
                                    red=line.startswith("-"),
                                    green=line.startswith("+"))
        raise batou.UpdateNeeded()
Example #4
0
 def summarize(self, host):
     for alias, fqdn in host.aliases.items():
         output.line(f' 🌐 https://{fqdn}/')