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)
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)
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()
def summarize(self, host): for alias, fqdn in host.aliases.items(): output.line(f' 🌐 https://{fqdn}/')