def update_all(self, interactive=False): """Update all versions, prompting the user for each possible update""" io.activate() for name, table in self.toml.items(): latest = self._latest_version(name) current = self._cached_version(name) now = datetime.datetime.now() if latest == current: io.stdout("{x} {name} is up to date ({current})".format( x=green("✓"), name=name, current=current, )) continue if interactive: question = wrap_question( name, "{} → {}".format(red(current), green(latest)), "Update {}".format(bold(name)), prefix="{} versions".format(blue("?"))) if not io.ask(question, True): continue else: io.stdout("{x} updated {name}: {current} → {new}".format( x=green("✓"), name=bold(name), current=red(current), new=green(latest), )) table['version'] = latest table['version_date'] = now self.toml[name] = table self._save()
def _get_result(self, autoskip_selector="", interactive=False, interactive_default=True): if self.covered_by_autoskip_selector(autoskip_selector): io.debug(_( "autoskip matches {item} on {node}" ).format(item=self.id, node=self.node.name)) return (self.STATUS_SKIPPED, [_("cmdline")]) if interactive is False and self.attributes['interactive'] is True: return (self.STATUS_SKIPPED, [_("interactive only")]) if self.triggered and not self.has_been_triggered: io.debug(_("skipping {} because it wasn't triggered").format(self.id)) return (self.STATUS_SKIPPED, [_("no trigger")]) if self.unless: with io.job(_(" {node} {bundle} {item} checking 'unless' condition...").format( bundle=self.bundle.name, item=self.id, node=self.node.name, )): unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: io.debug(_("{node}:{bundle}:action:{name}: failed 'unless', not running").format( bundle=self.bundle.name, name=self.name, node=self.bundle.node.name, )) return (self.STATUS_SKIPPED, ["unless"]) if ( interactive and self.attributes['interactive'] is not False and not io.ask( wrap_question( self.id, self.attributes['command'], _("Run action {}?").format( bold(self.name), ), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ), interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), ) ): return (self.STATUS_SKIPPED, [_("interactive")]) try: self.run() return (self.STATUS_ACTION_SUCCEEDED, None) except ActionFailure: return (self.STATUS_FAILED, None)
def _test(self): with io.job(_("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): if self._faults_missing_for_attributes: self._raise_for_faults() return self.test()
def get_status(self, cached=True): """ Returns an ItemStatus instance describing the current status of the item on the actual node. """ with io.job(_("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): if not cached: del self._cache['cached_sdict'] return ItemStatus(self.cached_cdict, self.cached_sdict, self.display_dicts)
def ask(self, status): if not status.info['exists']: return _("'{}' not found in /etc/passwd").format(self.name) elif self.attributes['delete']: return _("'{}' found in /etc/passwd. Will be deleted.").format(self.name) output = "" for key in status.info['needs_fixing']: if key in ('groups', 'password', 'password_hash'): continue output += "{} {} → {}\n".format( bold(_ATTRIBUTE_NAMES[key]), status.info[key], self.attributes[key], ) if self.attributes['password_hash'] is not None: if self.attributes['use_shadow']: filename = "/etc/shadow" found_hash = status.info['shadow_hash'] else: filename = "/etc/passwd" found_hash = status.info['passwd_hash'] if found_hash is None: output += bold(_ATTRIBUTE_NAMES['password_hash']) + " " + \ _("not found in {}").format(filename) + "\n" elif found_hash != self.attributes['password_hash']: output += bold(_ATTRIBUTE_NAMES['password_hash']) + " " + \ found_hash + "\n" output += " " * (len(_ATTRIBUTE_NAMES['password_hash']) - 1) + "→ " + \ self.attributes['password_hash'] + "\n" if self.attributes['groups'] is not None: groups_should = set(self.attributes['groups']) groups_is = set(status.info['groups']) missing_groups = sorted(groups_should.difference(groups_is)) extra_groups = sorted(groups_is.difference(groups_should)) if missing_groups: output += bold(_("missing groups")) + " " + \ ", ".join(missing_groups) + "\n" if extra_groups: output += bold(_("extra groups")) + " " + \ ", ".join(extra_groups) + "\n" return output
def ask(self, status): if not status.info["exists"] and not self.attributes["delete"]: return _("Doesn't exist. Do you want to create it?") if status.info["exists"] and self.attributes["delete"]: return red(_("Will be deleted.")) if status.info["owner"] != self.attributes["owner"]: return "{} {} → {}".format(bold(_("owner")), status.info["owner"], self.attributes["owner"])
def apply(self, interactive=False, interactive_default=True): self.node.repo.hooks.item_apply_start( self.node.repo, self.node, self, ) status_code = None status_before = None status_after = None start_time = datetime.now() if self.triggered and not self.has_been_triggered: LOG.debug(_("skipping {} because it wasn't triggered").format(self.id)) status_code = self.STATUS_SKIPPED if status_code is None and self.cached_unless_result: LOG.debug(_("'unless' for {} succeeded, not fixing").format(self.id)) status_code = self.STATUS_SKIPPED if status_code is None: status_before = self.cached_status if status_before.correct: status_code = self.STATUS_OK if status_code is None: if not interactive: self.fix(status_before) status_after = self.get_status() else: question = wrap_question( self.id, self.ask(status_before), _("Fix {}?").format(bold(self.id)), ) if ask_interactively(question, interactive_default): self.fix(status_before) status_after = self.get_status() else: status_code = self.STATUS_SKIPPED if status_code is None: if status_after.correct: status_code = self.STATUS_FIXED else: status_code = self.STATUS_FAILED self.node.repo.hooks.item_apply_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, status_code=status_code, status_before=status_before, status_after=status_after, ) return status_code
def ask(self, status): before = _("running") if status.info['running'] \ else _("not running") after = green(_("running")) if self.attributes['running'] \ else red(_("not running")) return "{} {} → {}\n".format( bold(_("status")), before, after, )
def ask(self, status): if not status.info['exists']: return _("'{}' not found in /etc/group").format(self.name) elif self.attributes['delete']: return _("'{}' found in /etc/group. Will be deleted.").format(self.name) else: return "{} {} → {}\n".format( bold(_("GID")), status.info['gid'], self.attributes['gid'], )
def ask(self, status): before = status.info['version'] if status.info['version'] \ else _("not installed") target = green(self.attributes['version']) if self.attributes['version'] else \ green(_("installed")) after = target if self.attributes['installed'] \ else red(_("not installed")) return "{} {} → {}\n".format( bold(_("status")), before, after, )
def _fetch_secret(site, secret_id): try: return cache[site][secret_id] except KeyError: pass session = sessions.setdefault(getpid(), Session()) try: full_url = "{}/api/secrets/{}/".format( config.get(site, "url"), secret_id, ) credentials = ( config.get(site, "username"), config.get(site, "password"), ) except (NoSectionError, NoOptionError): raise FaultUnavailable( "Tried to get TeamVault secret with ID '{secret_id}' " "from site '{site}', but credentials missing in {path}".format( path=CONFIG_PATH, secret_id=secret_id, site=site, ), ) with io.job(_("{tv} fetching {secret}").format(tv=bold("TeamVault"), secret=secret_id)): response = session.get(full_url, auth=credentials) if response.status_code != 200: raise FaultUnavailable( "TeamVault returned {status} for {url}".format( status=response.status_code, url=full_url, ) ) secret = response.json() response = session.get(secret['current_revision'] + "data", auth=credentials) if response.status_code != 200: raise FaultUnavailable( "TeamVault returned {status} for {url}".format( status=response.status_code, url=full_url, ) ) secret['data'] = response.json() cache.setdefault(site, {})[secret_id] = secret return secret
def ask(self, status): if 'type' in status.info['needs_fixing']: if not status.info['path_info'].exists: return _("Doesn't exist.") else: return "{} {} → {}\n".format( bold(_("type")), status.info['path_info'].desc, _("file"), ) question = "" if 'owner' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("owner")), status.info['path_info'].owner, self.attributes['owner'], ) if 'group' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("group")), status.info['path_info'].group, self.attributes['group'], ) if 'target' in status.info['needs_fixing']: question += "{} {}\n".format( bold(_("target")), status.info['path_info'].symlink_target, ) question += "{}{} {}\n".format( " " * (len(_("target")) - 1), bold("→"), self.attributes['target'], ) return question.rstrip("\n")
def run(self): if self.attributes['data_stdin'] is not None: data_stdin = self.attributes['data_stdin'] # Allow users to use either a string/unicode object or raw # bytes -- or Faults. if isinstance(data_stdin, Fault): data_stdin = data_stdin.value if type(data_stdin) is not bytes: data_stdin = data_stdin.encode('UTF-8') else: data_stdin = None with io.job( _("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): result = self.bundle.node.run( self.attributes['command'], data_stdin=data_stdin, may_fail=True, ) if self.attributes['expected_return_code'] is not None and \ not result.return_code == self.attributes['expected_return_code']: raise ActionFailure( _("wrong return code: {}").format(result.return_code)) if self.attributes['expected_stderr'] is not None and \ result.stderr_text != self.attributes['expected_stderr']: raise ActionFailure(_("wrong stderr")) if self.attributes['expected_stdout'] is not None and \ result.stdout_text != self.attributes['expected_stdout']: raise ActionFailure(_("wrong stdout")) return result
def _fetch_secret(site, secret_id): try: return cache[site][secret_id] except KeyError: pass session = sessions.setdefault(getpid(), Session()) try: full_url = "{}/api/secrets/{}/".format( config.get(site, "url"), secret_id, ) credentials = ( config.get(site, "username"), config.get(site, "password"), ) except (NoSectionError, NoOptionError): raise FaultUnavailable( "Tried to get TeamVault secret with ID '{secret_id}' " "from site '{site}', but credentials missing in {path}".format( path=CONFIG_PATH, secret_id=secret_id, site=site, ), ) with io.job( _("{tv} fetching {secret}").format(tv=bold("TeamVault"), secret=secret_id)): response = session.get(full_url, auth=credentials) if response.status_code != 200: raise FaultUnavailable("TeamVault returned {status} for {url}".format( status=response.status_code, url=full_url, )) secret = response.json() response = session.get(secret['current_revision'] + "data", auth=credentials) if response.status_code != 200: raise FaultUnavailable("TeamVault returned {status} for {url}".format( status=response.status_code, url=full_url, )) secret['data'] = response.json() cache.setdefault(site, {})[secret_id] = secret return secret
def ask(self, status): if 'type' in status.info['needs_fixing']: if not status.info['path_info'].exists: return _("Doesn't exist. Do you want to create it?") else: return _( "Not a directory. " "The `file` utility says it's a '{}'.\n" "Do you want it removed and replaced?" ).format( status.info['path_info'].desc, ) question = "" if 'mode' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("mode")), status.info['path_info'].mode, self.attributes['mode'], ) if 'owner' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("owner")), status.info['path_info'].owner, self.attributes['owner'], ) if 'group' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("group")), status.info['path_info'].group, self.attributes['group'], ) return question.rstrip("\n")
def ask(self, status): if not status.info['exists'] and not self.attributes['delete']: return _("Doesn't exist. Do you want to create it?") if status.info['exists'] and self.attributes['delete']: return red(_("Will be deleted.")) output = [] for attr, attr_pretty in ATTRS.items(): if self.attributes[attr] is None: continue if status.info[attr] != self.attributes[attr]: if attr in ('password_hash',): output.append("{} {}\n{}→ {}".format( bold(attr_pretty), status.info[attr], " " * (len(attr_pretty) - 1), self.attributes[attr], )) else: output.append("{} {} → {}".format( bold(attr_pretty), status.info[attr], self.attributes[attr], )) return "\n".join(output)
def run(self): if self.attributes['data_stdin'] is not None: data_stdin = self.attributes['data_stdin'] # Allow users to use either a string/unicode object or raw # bytes -- or Faults. if isinstance(data_stdin, Fault): data_stdin = data_stdin.value if type(data_stdin) is not bytes: data_stdin = data_stdin.encode('UTF-8') else: data_stdin = None with io.job(_("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): result = self.bundle.node.run( self.attributes['command'], data_stdin=data_stdin, may_fail=True, ) if self.attributes['expected_return_code'] is not None and \ not result.return_code == self.attributes['expected_return_code']: raise ActionFailure(_("wrong return code: {}").format(result.return_code)) if self.attributes['expected_stderr'] is not None and \ result.stderr_text != self.attributes['expected_stderr']: raise ActionFailure(_("wrong stderr")) if self.attributes['expected_stdout'] is not None and \ result.stdout_text != self.attributes['expected_stdout']: raise ActionFailure(_("wrong stdout")) return result
def install_dir(source, target): relpath = os.path.relpath(source, target) for _, dirs, files in os.walk(source, topdown=True): for directory in dirs: os.mkdir(target / directory) for file in files: try: os.symlink( os.path.join(relpath, file), os.path.join(target, file), ) except FileExistsError: pass io.stdout("{} installed {}".format( green("✓"), bold(os.path.join(os.path.basename(target), file)), ))
def apply_start(repo, target, nodes, interactive=False, **kwargs): ''' Interactively update all versions if running in interactive mode. ''' _ = target _ = nodes _ = kwargs if interactive: question = wrap_question( bold("Version management"), "Do you want to check configured software versions for updates", "Check for updates ?", prefix="{} versions".format(blue("?")), ) if not io.ask(question, True): return repo.libs.versions.VersionManager().update_all(interactive=True)
def _get_result(self, interactive=False, interactive_default=True): if interactive is False and self.attributes['interactive'] is True: return self.STATUS_SKIPPED if self.triggered and not self.has_been_triggered: LOG.debug(_("skipping {} because it wasn't triggered").format(self.id)) return self.STATUS_SKIPPED if self.unless: unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: LOG.debug(_("{node}:{bundle}:action:{name}: failed 'unless', not running").format( bundle=self.bundle.name, name=self.name, node=self.bundle.node.name, )) return self.STATUS_SKIPPED if ( interactive and self.attributes['interactive'] is not False and not ask_interactively( wrap_question( self.id, self.attributes['command'], _("Run action {}?").format( bold(self.name), ), ), interactive_default, ) ): return self.STATUS_SKIPPED try: self.run(interactive=interactive) return self.STATUS_ACTION_SUCCEEDED except ActionFailure: return self.STATUS_FAILED
def apply( self, autoskip_selector="", autoonly_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): self.node.repo.hooks.item_apply_start( self.node.repo, self.node, self, ) status_code = None status_before = None status_after = None start_time = datetime.now() if not self.covered_by_autoonly_selector(autoonly_selector): io.debug( _("autoonly does not match {item} on {node}").format( item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_CMDLINE if self.covered_by_autoskip_selector(autoskip_selector): io.debug( _("autoskip matches {item} on {node}").format( item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_CMDLINE if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_SOFTLOCK for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): io.debug( _("preceding item {item} on {node} has been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) self.has_been_triggered = True break else: io.debug( _("preceding item {item} on {node} has NOT been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) if self.triggered and not self.has_been_triggered and status_code is None: io.debug( _("skipping {item} on {node} because it wasn't triggered"). format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_NO_TRIGGER if status_code is None and self.cached_unless_result and status_code is None: io.debug( _("'unless' for {item} on {node} succeeded, not fixing"). format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_UNLESS if self._faults_missing_for_attributes and status_code is None: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _("skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)").format( attrs=", ".join( sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE if status_code is None: try: status_before = self.cached_status except FaultUnavailable: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _("skipping {item} on {node} because it is missing Faults " "(most of the time this means you're missing " "a required key in your .secrets.cfg)").format( item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE else: if status_before.correct: status_code = self.STATUS_OK if status_code is None: if not interactive: with io.job( _("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): self.fix(status_before) else: if status_before.must_be_created: question_text = _("Doesn't exist. Will be created.") elif status_before.must_be_deleted: question_text = _("Found on node. Will be removed.") else: question_text = self.ask( status_before.display_cdict, status_before.display_sdict, status_before.display_keys_to_fix, ) if self.comment: question_text += format_comment(self.comment) question = wrap_question( self.id, question_text, _("Fix {}?").format(bold(self.id)), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ) answer = io.ask( question, interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), ) if answer: with io.job( _("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): self.fix(status_before) else: status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_INTERACTIVE if status_code is None: status_after = self.get_status(cached=False) status_code = self.STATUS_FIXED if status_after.correct else self.STATUS_FAILED if status_code == self.STATUS_OK: details = None elif status_code == self.STATUS_SKIPPED: details = skip_reason elif status_before.must_be_created: details = True elif status_before.must_be_deleted: details = False elif status_code == self.STATUS_FAILED: details = status_after.display_keys_to_fix else: details = status_before.display_keys_to_fix self.node.repo.hooks.item_apply_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, status_code=status_code, status_before=status_before, status_after=status_after, ) return (status_code, details)
def ask(self, status): if 'type' in status.info['needs_fixing']: if not status.info['path_info'].exists: return _("Doesn't exist.") elif self.attributes['delete']: if status.info['path_info'].is_directory: return _("Directory and its contents will be deleted.") else: return _("File will be deleted.") else: return "{} {} → {}\n".format( bold(_("type")), status.info['path_info'].desc, _("file"), ) question = "" if 'content' in status.info['needs_fixing']: question += bold(_("content ")) if ( status.info['path_info'].is_text_file and not self.attributes['content_type'] == 'binary' ): if status.info['path_info'].size > DIFF_MAX_FILE_SIZE: question += _("(remote file larger than {} bytes, skipping diff)\n").format( DIFF_MAX_FILE_SIZE, ) elif len(self.content) > DIFF_MAX_FILE_SIZE: question += _("(new content larger than {} bytes, skipping diff)\n").format( DIFF_MAX_FILE_SIZE, ) else: content_is = get_remote_file_contents(self.node, self.name) content_should = self.content question += "\n" + diff( content_is, content_should, self.name, encoding_hint=self.attributes['encoding'], ) + "\n" else: question += "'{}' → {}\n".format( status.info['path_info'].desc, _("<bundlewrap content>"), ) if 'mode' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("mode")), status.info['path_info'].mode, self.attributes['mode'], ) if 'owner' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("owner")), status.info['path_info'].owner, self.attributes['owner'], ) if 'group' in status.info['needs_fixing']: question += "{} {} → {}\n".format( bold(_("group")), status.info['path_info'].group, self.attributes['group'], ) return question.rstrip("\n")
def apply( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): self.node.repo.hooks.item_apply_start(self.node.repo, self.node, self) keys_to_fix = None status_code = None status_before = None status_after = None start_time = datetime.now() if self.covered_by_autoskip_selector(autoskip_selector): io.debug(_("autoskip matches {item} on {node}").format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED keys_to_fix = [_("cmdline")] if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): status_code = self.STATUS_SKIPPED keys_to_fix = [_("soft locked")] if self.triggered and not self.has_been_triggered and status_code is None: io.debug( _("skipping {item} on {node} because it wasn't triggered").format(item=self.id, node=self.node.name) ) status_code = self.STATUS_SKIPPED keys_to_fix = [_("not triggered")] if status_code is None and self.cached_unless_result and status_code is None: io.debug(_("'unless' for {item} on {node} succeeded, not fixing").format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED keys_to_fix = ["unless"] if self._faults_missing_for_attributes and status_code is None: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _( "skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)" ).format( attrs=", ".join(sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name ) ) status_code = self.STATUS_SKIPPED keys_to_fix = [_("Fault unavailable")] if status_code is None: try: status_before = self.cached_status except FaultUnavailable: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _( "skipping {item} on {node} because it is missing Faults " "(most of the time this means you're missing " "a required key in your .secrets.cfg)" ).format(item=self.id, node=self.node.name) ) status_code = self.STATUS_SKIPPED keys_to_fix = [_("Fault unavailable")] else: if status_before.correct: status_code = self.STATUS_OK if status_code is None: keys_to_fix = self.display_keys( copy(self.cached_cdict), copy(status_before.sdict), status_before.keys_to_fix[:] ) if not interactive: with io.job( _(" {node} {bundle} {item} fixing...").format( bundle=self.bundle.name, item=self.id, node=self.node.name ) ): self.fix(status_before) else: if status_before.must_be_created: question_text = _("Doesn't exist. Will be created.") elif status_before.must_be_deleted: question_text = _("Found on node. Will be removed.") else: cdict, sdict = self.display_dicts(copy(self.cached_cdict), copy(status_before.sdict), keys_to_fix) question_text = self.ask(cdict, sdict, keys_to_fix) question = wrap_question( self.id, question_text, _("Fix {}?").format(bold(self.id)), prefix="{x} {node} ".format(node=bold(self.node.name), x=blue("?")), ) answer = io.ask( question, interactive_default, epilogue="{x} {node}".format(node=bold(self.node.name), x=blue("?")) ) if answer: with io.job( _(" {node} {bundle} {item} fixing...").format( bundle=self.bundle.name, item=self.id, node=self.node.name ) ): self.fix(status_before) else: status_code = self.STATUS_SKIPPED keys_to_fix = [_("interactive")] if status_code is None: status_after = self.get_status(cached=False) status_code = self.STATUS_FIXED if status_after.correct else self.STATUS_FAILED if status_code == self.STATUS_SKIPPED: # can't use else for this because status_before is None changes = keys_to_fix elif status_before.must_be_created: changes = True elif status_before.must_be_deleted: changes = False elif status_code == self.STATUS_FAILED: changes = self.display_keys( self.cached_cdict.copy(), status_after.sdict.copy(), status_after.keys_to_fix[:] ) else: changes = keys_to_fix self.node.repo.hooks.item_apply_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, status_code=status_code, status_before=status_before, status_after=status_after, ) return (status_code, changes)
def format_comment(comment): result = "\n\n" for line in wrapper.wrap(cleandoc(comment)): for inlineline in line.split("\n"): result += "{} {}\n".format(bold("#"), italic(inlineline)) return result
def _fetch_secret(site, secret_id): try: return cache[site][secret_id] except KeyError: pass session = sessions.setdefault(getpid(), Session()) try: full_url = "{}/api/secrets/{}/".format( config.get(site, "url"), secret_id, ) if site not in cached_credentials: if ('password' not in config[site] or not config[site]['password'] and 'pass_command' in config[site]): try: password = check_output( config[site]['pass_command'], shell=True).decode('UTF-8').splitlines()[0].strip() except (FileNotFoundError, CalledProcessError, IndexError) as e: # To avoid trying to get the password over and over. cached_credentials[site] = None raise FaultUnavailable from e else: cached_credentials[site] = ( config.get(site, 'username'), password, ) else: cached_credentials[site] = ( config.get(site, "username"), config.get(site, "password"), ) except (NoSectionError, NoOptionError): raise FaultUnavailable( "Tried to get TeamVault secret with ID '{secret_id}' " "from site '{site}', but credentials missing in {path}".format( path=CONFIG_PATH, secret_id=secret_id, site=site, ), ) if cached_credentials[site] is None: raise FaultUnavailable( "Getting credentials for {site} failed in earlier try".format( site=site, ), ) try: with io.job( _("{tv} fetching {secret}").format(tv=bold("TeamVault"), secret=secret_id)): response = session.get(full_url, auth=cached_credentials[site]) except RequestException as e: raise FaultUnavailable( "Exception while getting secret {secret} from TeamVault: {exc}". format( secret=secret_id, exc=repr(e), )) if response.status_code != 200: raise FaultUnavailable("TeamVault returned {status} for {url}".format( status=response.status_code, url=full_url, )) secret = response.json() try: response = session.get(secret['current_revision'] + "data", auth=cached_credentials[site]) except RequestException as e: raise FaultUnavailable( "Exception while getting secret {secret} from TeamVault: {exc}". format( secret=secret_id, exc=repr(e), )) if response.status_code != 200: raise FaultUnavailable("TeamVault returned {status} for {url}".format( status=response.status_code, url=secret['current_revision'] + "data", )) secret['data'] = response.json() cache.setdefault(site, {})[secret_id] = secret return secret
def apply( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): self.node.repo.hooks.item_apply_start( self.node.repo, self.node, self, ) status_code = None status_before = None status_after = None start_time = datetime.now() if self.covered_by_autoskip_selector(autoskip_selector): io.debug(_( "autoskip matches {item} on {node}" ).format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_CMDLINE if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_SOFTLOCK for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): io.debug(_( "preceding item {item} on {node} has been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) self.has_been_triggered = True break else: io.debug(_( "preceding item {item} on {node} has NOT been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) if self.triggered and not self.has_been_triggered and status_code is None: io.debug(_( "skipping {item} on {node} because it wasn't triggered" ).format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_NO_TRIGGER if status_code is None and self.cached_unless_result and status_code is None: io.debug(_( "'unless' for {item} on {node} succeeded, not fixing" ).format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_UNLESS if self._faults_missing_for_attributes and status_code is None: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug(_( "skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)" ).format( attrs=", ".join(sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE if status_code is None: try: status_before = self.cached_status except FaultUnavailable: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug(_( "skipping {item} on {node} because it is missing Faults " "(most of the time this means you're missing " "a required key in your .secrets.cfg)" ).format( item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE else: if status_before.correct: status_code = self.STATUS_OK if status_code is None: if not interactive: with io.job(_("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): self.fix(status_before) else: if status_before.must_be_created: question_text = _("Doesn't exist. Will be created.") elif status_before.must_be_deleted: question_text = _("Found on node. Will be removed.") else: question_text = self.ask( status_before.display_cdict, status_before.display_sdict, status_before.display_keys_to_fix, ) if self.comment: question_text += format_comment(self.comment) question = wrap_question( self.id, question_text, _("Fix {}?").format(bold(self.id)), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ) answer = io.ask( question, interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), ) if answer: with io.job(_("{node} {bundle} {item}").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): self.fix(status_before) else: status_code = self.STATUS_SKIPPED skip_reason = self.SKIP_REASON_INTERACTIVE if status_code is None: status_after = self.get_status(cached=False) status_code = self.STATUS_FIXED if status_after.correct else self.STATUS_FAILED if status_code == self.STATUS_OK: details = None elif status_code == self.STATUS_SKIPPED: details = skip_reason elif status_before.must_be_created: details = True elif status_before.must_be_deleted: details = False elif status_code == self.STATUS_FAILED: details = status_after.display_keys_to_fix else: details = status_before.display_keys_to_fix self.node.repo.hooks.item_apply_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, status_code=status_code, status_before=status_before, status_after=status_after, ) return (status_code, details)
def _get_result( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): if self._faults_missing_for_attributes: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug(_( "skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)" ).format( attrs=", ".join(sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name, )) return (self.STATUS_SKIPPED, self.SKIP_REASON_FAULT_UNAVAILABLE) if self.covered_by_autoskip_selector(autoskip_selector): io.debug(_( "autoskip matches {item} on {node}" ).format(item=self.id, node=self.node.name)) return (self.STATUS_SKIPPED, self.SKIP_REASON_CMDLINE) if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): return (self.STATUS_SKIPPED, self.SKIP_REASON_SOFTLOCK) if interactive is False and self.attributes['interactive'] is True: return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE_ONLY) for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): io.debug(_( "preceding item {item} on {node} has been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) self.has_been_triggered = True break else: io.debug(_( "preceding item {item} on {node} has NOT been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) if self.triggered and not self.has_been_triggered: io.debug(_("skipping {} because it wasn't triggered").format(self.id)) return (self.STATUS_SKIPPED, self.SKIP_REASON_NO_TRIGGER) if self.unless: with io.job(_("{node} {bundle} {item} checking 'unless' condition").format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: io.debug(_("{node}:{bundle}:action:{name}: failed 'unless', not running").format( bundle=self.bundle.name, name=self.name, node=self.bundle.node.name, )) return (self.STATUS_SKIPPED, self.SKIP_REASON_UNLESS) question_body = "" if self.attributes['data_stdin'] is not None: question_body += "<" + _("data") + "> | " question_body += self.attributes['command'] if self.comment: question_body += format_comment(self.comment) if ( interactive and self.attributes['interactive'] is not False and not io.ask( wrap_question( self.id, question_body, _("Run action {}?").format( bold(self.name), ), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ), interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), ) ): return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE) try: self.run() return (self.STATUS_ACTION_SUCCEEDED, None) except ActionFailure as exc: return (self.STATUS_FAILED, [str(exc)])
def _get_result( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): if self.covered_by_autoskip_selector(autoskip_selector): io.debug( _("autoskip matches {item} on {node}").format( item=self.id, node=self.node.name)) return (self.STATUS_SKIPPED, [_("cmdline")]) if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): return (self.STATUS_SKIPPED, [_("soft locked")]) if interactive is False and self.attributes['interactive'] is True: return (self.STATUS_SKIPPED, [_("interactive only")]) if self.triggered and not self.has_been_triggered: io.debug( _("skipping {} because it wasn't triggered").format(self.id)) return (self.STATUS_SKIPPED, [_("no trigger")]) if self.unless: with io.job( _(" {node} {bundle} {item} checking 'unless' condition..." ).format( bundle=self.bundle.name, item=self.id, node=self.node.name, )): unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: io.debug( _("{node}:{bundle}:action:{name}: failed 'unless', not running" ).format( bundle=self.bundle.name, name=self.name, node=self.bundle.node.name, )) return (self.STATUS_SKIPPED, ["unless"]) question_body = "" if self.attributes['data_stdin'] is not None: question_body += "<" + _("data") + "> | " question_body += self.attributes['command'] if self.comment: question_body += format_comment(self.comment) if (interactive and self.attributes['interactive'] is not False and not io.ask( wrap_question( self.id, question_body, _("Run action {}?").format(bold(self.name), ), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ), interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), )): return (self.STATUS_SKIPPED, [_("interactive")]) try: self.run() return (self.STATUS_ACTION_SUCCEEDED, None) except ActionFailure as exc: return (self.STATUS_FAILED, [str(exc)])
def apply( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): self.node.repo.hooks.item_apply_start( self.node.repo, self.node, self, ) keys_to_fix = None status_code = None status_before = None status_after = None start_time = datetime.now() if self.covered_by_autoskip_selector(autoskip_selector): io.debug( _("autoskip matches {item} on {node}").format( item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED keys_to_fix = [_("cmdline")] if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): status_code = self.STATUS_SKIPPED keys_to_fix = [_("soft locked")] if self.triggered and not self.has_been_triggered and status_code is None: io.debug( _("skipping {item} on {node} because it wasn't triggered"). format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED keys_to_fix = [_("not triggered")] if status_code is None and self.cached_unless_result and status_code is None: io.debug( _("'unless' for {item} on {node} succeeded, not fixing"). format(item=self.id, node=self.node.name)) status_code = self.STATUS_SKIPPED keys_to_fix = ["unless"] if self._faults_missing_for_attributes and status_code is None: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _("skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)").format( attrs=", ".join( sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED keys_to_fix = [_("Fault unavailable")] if status_code is None: try: status_before = self.cached_status except FaultUnavailable: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _("skipping {item} on {node} because it is missing Faults " "(most of the time this means you're missing " "a required key in your .secrets.cfg)").format( item=self.id, node=self.node.name, )) status_code = self.STATUS_SKIPPED keys_to_fix = [_("Fault unavailable")] else: if status_before.correct: status_code = self.STATUS_OK if status_code is None: keys_to_fix = self.display_keys( copy(self.cached_cdict), copy(status_before.sdict), status_before.keys_to_fix[:], ) if not interactive: with io.job( _(" {node} {bundle} {item} fixing...").format( bundle=self.bundle.name, item=self.id, node=self.node.name, )): self.fix(status_before) else: if status_before.must_be_created: question_text = _("Doesn't exist. Will be created.") elif status_before.must_be_deleted: question_text = _("Found on node. Will be removed.") else: cdict, sdict = self.display_dicts( copy(self.cached_cdict), copy(status_before.sdict), keys_to_fix, ) question_text = self.ask(cdict, sdict, keys_to_fix) if self.comment: question_text += format_comment(self.comment) question = wrap_question( self.id, question_text, _("Fix {}?").format(bold(self.id)), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ) answer = io.ask( question, interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), ) if answer: with io.job( _(" {node} {bundle} {item} fixing...").format( bundle=self.bundle.name, item=self.id, node=self.node.name, )): self.fix(status_before) else: status_code = self.STATUS_SKIPPED keys_to_fix = [_("interactive")] if status_code is None: status_after = self.get_status(cached=False) status_code = self.STATUS_FIXED if status_after.correct else self.STATUS_FAILED if status_code == self.STATUS_SKIPPED: # can't use else for this because status_before is None changes = keys_to_fix elif status_before.must_be_created: changes = True elif status_before.must_be_deleted: changes = False elif status_code == self.STATUS_FAILED: changes = self.display_keys( self.cached_cdict.copy(), status_after.sdict.copy(), status_after.keys_to_fix[:], ) else: changes = keys_to_fix self.node.repo.hooks.item_apply_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, status_code=status_code, status_before=status_before, status_after=status_after, ) return (status_code, changes)
def test_ansi_clean(): assert red("test") != "test" assert len(red("test")) != len("test") assert ansi_clean(red("test")) == "test" assert ansi_clean(bold(red("test"))) == "test"
def _get_result( self, autoskip_selector="", my_soft_locks=(), other_peoples_soft_locks=(), interactive=False, interactive_default=True, ): if self._faults_missing_for_attributes: if self.error_on_missing_fault: self._raise_for_faults() else: io.debug( _("skipping {item} on {node} because it is missing faults " "for these attributes: {attrs} " "(most of the time this means you're missing " "a required key in your .secrets.cfg)").format( attrs=", ".join( sorted(self._faults_missing_for_attributes)), item=self.id, node=self.node.name, )) return (self.STATUS_SKIPPED, self.SKIP_REASON_FAULT_UNAVAILABLE) if self.covered_by_autoskip_selector(autoskip_selector): io.debug( _("autoskip matches {item} on {node}").format( item=self.id, node=self.node.name)) return (self.STATUS_SKIPPED, self.SKIP_REASON_CMDLINE) if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): return (self.STATUS_SKIPPED, self.SKIP_REASON_SOFTLOCK) if interactive is False and self.attributes['interactive'] is True: return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE_ONLY) for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): io.debug( _("preceding item {item} on {node} has been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) self.has_been_triggered = True break else: io.debug( _("preceding item {item} on {node} has NOT been triggered by {other_item}" ).format(item=self.id, node=self.node.name, other_item=item.id)) if self.triggered and not self.has_been_triggered: io.debug( _("skipping {} because it wasn't triggered").format(self.id)) return (self.STATUS_SKIPPED, self.SKIP_REASON_NO_TRIGGER) if self.unless: with io.job( _("{node} {bundle} {item} checking 'unless' condition"). format( bundle=bold(self.bundle.name), item=self.id, node=bold(self.node.name), )): unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: io.debug( _("{node}:{bundle}:action:{name}: failed 'unless', not running" ).format( bundle=self.bundle.name, name=self.name, node=self.bundle.node.name, )) return (self.STATUS_SKIPPED, self.SKIP_REASON_UNLESS) question_body = "" if self.attributes['data_stdin'] is not None: question_body += "<" + _("data") + "> | " question_body += self.attributes['command'] if self.comment: question_body += format_comment(self.comment) if (interactive and self.attributes['interactive'] is not False and not io.ask( wrap_question( self.id, question_body, _("Run action {}?").format(bold(self.name), ), prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), ), ), interactive_default, epilogue="{x} {node}".format( node=bold(self.node.name), x=blue("?"), ), )): return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE) try: self.run() return (self.STATUS_ACTION_SUCCEEDED, None) except ActionFailure as exc: return (self.STATUS_FAILED, [str(exc)])