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'], ) return question.rstrip("\n")
def fix(self, status): if 'type' in status.info['needs_fixing']: # fixing the type fixes everything if status.info['path_info'].exists: LOG.info(_("{node}:{item}: fixing type...").format( item=self.id, node=self.node.name, )) else: LOG.info(_("{node}:{item}: creating...").format( item=self.id, node=self.node.name, )) self._fix_type(status) return for fix_type in ('owner', 'group'): if fix_type in status.info['needs_fixing']: if fix_type == 'group' and \ 'owner' in status.info['needs_fixing']: # owner and group are fixed with a single chown continue LOG.info(_("{node}:{item}: fixing {type}...").format( item=self.id, node=self.node.name, type=fix_type, )) getattr(self, "_fix_" + fix_type)(status)
def fix(self, status): if status.info['exists']: if self.attributes['delete']: msg = _("{node}:{item}: deleting...") else: msg = _("{node}:{item}: updating...") else: msg = _("{node}:{item}: creating...") LOG.info(msg.format(item=self.id, node=self.node.name)) if self.attributes['delete']: self.node.run("userdel {}".format(self.name)) else: self.node.run("{command} " "-d {home} " "-g {gid} " "-G {groups} " "-p {password_hash} " "-s {shell} " "-u {uid} " "{username}".format( command="useradd" if not status.info['exists'] else "usermod", home=quote(self.attributes['home']), gid=self.attributes['gid'], groups=quote(",".join(self.attributes['groups'])), password_hash=quote(self.attributes['password_hash']), shell=quote(self.attributes['shell']), uid=self.attributes['uid'], username=self.name, ) )
def validate_attributes(cls, bundle, item_id, attributes): if attributes.get('delete', False): for attr in attributes.keys(): if attr not in ['delete'] + BUILTIN_ITEM_ATTRIBUTES.keys(): raise BundleError(_( "{item} from bundle '{bundle}' cannot have other " "attributes besides 'delete'" ).format(item=item_id, bundle=bundle.name)) if 'content' in attributes and 'source' in attributes: raise BundleError(_( "{item} from bundle '{bundle}' cannot have both 'content' and 'source'" ).format(item=item_id, bundle=bundle.name)) if ( attributes.get('content_type', None) == "any" and ( 'content' in attributes or 'encoding' in attributes or 'source' in attributes ) ): raise BundleError(_( "{item} from bundle '{bundle}' with content_type 'any' " "must not define 'content', 'encoding' and/or 'source'" ).format(item=item_id, bundle=bundle.name)) for key, value in attributes.items(): ATTRIBUTE_VALIDATORS[key](item_id, value)
def validate_name(cls, bundle, name): for char in name: if char not in _USERNAME_VALID_CHARACTERS: raise BundleError(_( "Invalid character in group name '{name}': {char} (bundle '{bundle}')" ).format( char=char, bundle=bundle.name, name=name, )) if name.endswith("_") or name.endswith("-"): raise BundleError(_( "Group name '{name}' must not end in dash or underscore (bundle '{bundle}')" ).format( bundle=bundle.name, name=name, )) if len(name) > 30: raise BundleError(_( "Group name '{name}' is longer than 30 characters (bundle '{bundle}')" ).format( bundle=bundle.name, name=name, ))
def ask(self, status): before = _("installed") if status.info['installed'] \ else _("not installed") after = green(_("installed")) if self.attributes['installed'] \ else red(_("not installed")) return "{} {} → {}\n".format( bold(_("status")), before, after, )
def validate_name(cls, bundle, name): if normpath(name) == "/": raise BundleError(_("'/' cannot be a file")) if normpath(name) != name: raise BundleError(_( "'{path}' is an invalid symlink path, should be '{normpath}' (bundle '{bundle}')" ).format( path=name, normpath=normpath(name), bundle=bundle.name, ))
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 fix(self, status): if not status.info['exists']: LOG.info(_("{}:{}: creating...").format(self.node.name, self.id)) else: LOG.info(_("{}:{}: updating...").format(self.node.name, self.id)) self.node.run("echo '{} {} {}' | debconf-set-selections".format( self.attributes['pkg_name'], self.name, self.attributes['value'], )) sleep(1)
def fix(self, status): if self.attributes['running'] is False: LOG.info(_("{node}:{item}: stopping...").format( item=self.id, node=self.node.name, )) svc_stop(self.node, self.name) else: LOG.info(_("{node}:{item}: starting...").format( item=self.id, node=self.node.name, )) svc_start(self.node, self.name)
def validate_attributes(cls, bundle, item_id, attributes): if attributes.get('delete', False): for attr in attributes.keys(): if attr not in ['delete'] + BUILTIN_ITEM_ATTRIBUTES.keys(): raise BundleError(_( "{item} from bundle '{bundle}' cannot have other " "attributes besides 'delete'" ).format(item=item_id, bundle=bundle.name)) if not attributes.get('delete', False) and 'gid' not in attributes.keys(): raise BundleError(_( "{item} from bundle '{bundle}' must define 'gid'" ).format(item=item_id, bundle=bundle.name))
def fix(self, status): if self.attributes['installed'] is False: LOG.info(_("{node}:{item}: removing...").format( item=self.id, node=self.node.name, )) pkg_remove(self.node, self.name) else: LOG.info(_("{node}:{item}: installing...").format( item=self.id, node=self.node.name, )) pkg_install(self.node, self.name)
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, should_value in self.attributes.iteritems(): if key in ('delete', 'groups', 'hash_method', 'password', 'password_hash', 'salt', 'use_shadow'): continue is_value = status.info[key] if should_value != is_value: output += "{} {} → {}\n".format( bold(_ATTRIBUTE_NAMES[key]), is_value, should_value, ) 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" groups_should = set(self.attributes['groups']) groups_is = set(status.info['groups']) missing_groups = list(groups_should.difference(groups_is)) missing_groups.sort() extra_groups = list(groups_is.difference(groups_should)) extra_groups.sort() 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 validator_mode(item_id, value): if not value.isdigit(): raise BundleError( _("mode for {item} should be written as digits, got: '{value}'" "").format(item=item_id, value=value) ) for digit in value: if int(digit) > 7 or int(digit) < 0: raise BundleError(_( "invalid mode for {item}: '{value}'" ).format(item=item_id, value=value)) if not len(value) == 3 and not len(value) == 4: raise BundleError(_( "mode for {item} should be three or four digits long, was: '{value}'" ).format(item=item_id, value=value))
def get_auto_deps(self, items): deps = [] for item in items: if item == self: continue if ( ( item.ITEM_TYPE_NAME == "file" and is_subdirectory(item.name, self.name) ) or ( item.ITEM_TYPE_NAME in ("file", "symlink") and item.name == self.name ) ): raise BundleError(_( "{item1} (from bundle '{bundle1}') blocking path to " "{item2} (from bundle '{bundle2}')" ).format( item1=item.id, bundle1=item.bundle.name, item2=self.id, bundle2=self.bundle.name, )) elif item.ITEM_TYPE_NAME in ("directory", "symlink"): if is_subdirectory(item.name, self.name): deps.append(item.id) return deps
def fix(self, status): if not status.info['exists']: LOG.info(_("{node}:{item}: creating...").format(node=self.node.name, item=self.id)) self.node.run("groupadd -g {gid} {groupname}".format( gid=self.attributes['gid'], groupname=self.name, )) elif self.attributes['delete']: LOG.info(_("{node}:{item}: deleting...").format(node=self.node.name, item=self.id)) self.node.run("groupdel {}".format(self.name)) else: LOG.info(_("{node}:{item}: updating...").format(node=self.node.name, item=self.id)) self.node.run("groupmod -g {gid} {groupname}".format( gid=self.attributes['gid'], groupname=self.name, ))
def validate_attributes(cls, bundle, item_id, attributes): if not isinstance(attributes.get('installed', True), bool): raise BundleError(_( "expected boolean for 'installed' on {item} in bundle '{bundle}'" ).format( bundle=bundle.name, item=item_id, ))
def validator_content(item_id, value): if value is not None: try: value.decode('utf-8') except UnicodeDecodeError: raise BundleError( _("'content' for {} must be a UTF-8 encoded string").format(item_id) )
def _validate_name(cls, bundle, name): if ":" in name: raise BundleError(_( "invalid name for {type} in bundle '{bundle}': {name} (must not contain colon)" ).format( bundle=bundle.name, name=name, type=cls.ITEM_TYPE_NAME, ))
def ask(self, status): if not status.info['exists']: return _("'{}' not found in debconf-selections").format(self.name) return "{}: '{}' > '{}'\n".format( self.attributes['pkg_name'], status.info['value'], self.attributes['value'], )
def diff(content_old, content_new, filename, encoding_hint=None): output = "" LOG.debug("diffing {filename}: {len_before} B before, {len_after} B after".format( filename=filename, len_before=len(content_old), len_after=len(content_new), )) start = datetime.now() for line in unified_diff( content_old.splitlines(True), content_new.splitlines(True), fromfile=filename, tofile=_("<blockwart content>"), ): suffix = "" try: line = line.decode('UTF-8') except UnicodeDecodeError: if encoding_hint and encoding_hint.lower() != "utf-8": try: line = line.decode(encoding_hint) suffix += _(" (line encoded in {})").format(encoding_hint) except UnicodeDecodeError: line = line[0] suffix += _(" (line not encoded in UTF-8 or {})").format(encoding_hint) else: line = line[0] suffix += _(" (line not encoded in UTF-8)") line = line.rstrip("\n") if len(line) > DIFF_MAX_LINE_LENGTH: line = line[:DIFF_MAX_LINE_LENGTH] suffix += _(" (line truncated after {} characters)").format(DIFF_MAX_LINE_LENGTH) if line.startswith("+"): line = green(line) elif line.startswith("-"): line = red(line) output += line + suffix + "\n" duration = datetime.now() - start LOG.debug("diffing {file}: complete after {time}s".format( file=filename, time=duration.total_seconds(), )) return output
def run(self, interactive=False): result = self.bundle.node.run( self.attributes['command'], may_fail=True, ) if self.attributes['expected_return_code'] is not None and \ not result.return_code == self.attributes['expected_return_code']: if not interactive: LOG.error("{}:{}: {}".format( self.bundle.node.name, self.id, red(_("FAILED")), )) raise ActionFailure(_( "wrong return code for action '{action}' in bundle '{bundle}': " "expected {ecode}, but was {rcode}" ).format( action=self.name, bundle=self.bundle.name, ecode=self.attributes['expected_return_code'], rcode=result.return_code, )) if self.attributes['expected_stderr'] is not None and \ result.stderr != self.attributes['expected_stderr']: LOG.error("{}:{}: {}".format( self.bundle.node.name, self.id, red(_("FAILED")), )) raise ActionFailure(_( "wrong stderr for action '{action}' in bundle '{bundle}'" ).format( action=self.name, bundle=self.bundle.name, )) if self.attributes['expected_stdout'] is not None and \ result.stdout != self.attributes['expected_stdout']: LOG.error("{}:{}: {}".format( self.bundle.node.name, self.id, red(_("FAILED")), )) raise ActionFailure(_( "wrong stdout for action '{action}' in bundle '{bundle}'" ).format( action=self.name, bundle=self.bundle.name, )) LOG.info("{}:{}: {}".format( self.bundle.node.name, self.id, green(_("OK")), )) return result
def validate_name(cls, bundle, name): if normpath(name) != name: raise BundleError(_( "'{path}' is an invalid directory path, " "should be '{normpath}' (bundle '{bundle}')" ).format( bundle=bundle.name, normpath=normpath(name), path=name, ))
def unpickle_item_class(class_name, bundle, name, attributes, has_been_triggered): for item_class in bundle.node.repo.item_classes: if item_class.__name__ == class_name: return item_class( bundle, name, attributes, has_been_triggered=has_been_triggered, skip_validation=True, ) raise RuntimeError(_("unable to unpickle {cls}").format(cls=class_name))
def test(self): if self.attributes['source'] and not exists(self.template): raise BundleError(_( "{item} from bundle '{bundle}' refers to missing " "file '{path}' in its 'source' attribute" ).format( bundle=self.bundle.name, item=self.id, path=self.template, )) if self.attributes['content_type'] in ('mako', 'text'): self.content
def _check_bundle_collisions(self, items): for item in items: if item == self: continue if item.id == self.id: raise BundleError(_( "duplicate definition of {item} in bundles '{bundle1}' and '{bundle2}'" ).format( item=item.id, bundle1=item.bundle.name, bundle2=self.bundle.name, ))
def get_result(self, interactive=False, interactive_default=True): if interactive is False and self.attributes['interactive'] is True: return self.STATUS_ACTION_SKIPPED if self.triggered and not self.has_been_triggered: LOG.debug(_("skipping {} because it wasn't triggered").format(self.id)) return self.STATUS_ACTION_SKIPPED if self.unless: unless_result = self.bundle.node.run( self.unless, may_fail=True, ) if unless_result.return_code == 0: LOG.debug(_("{node}:action:{name}: failed 'unless', not running").format( name=self.name, node=self.bundle.node.name, )) return self.STATUS_ACTION_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_ACTION_SKIPPED try: self.run(interactive=interactive) return self.STATUS_ACTION_OK except ActionFailure: return self.STATUS_ACTION_FAILED
def fix(self, status): for fix_type in ('type', 'content', 'mode', 'owner', 'group'): if fix_type in status.info['needs_fixing']: if fix_type == 'group' and \ 'owner' in status.info['needs_fixing']: # owner and group are fixed with a single chown continue if fix_type in ('mode', 'owner', 'group') and \ 'content' in status.info['needs_fixing']: # fixing content implies settings mode and owner/group continue if status.info['path_info'].exists: if self.attributes['delete']: LOG.info(_("{node}:{item}: deleting...").format( node=self.node.name, item=self.id)) else: LOG.info(_("{node}:{item}: fixing {type}...").format( node=self.node.name, item=self.id, type=fix_type)) else: LOG.info(_("{node}:{item}: creating...").format( node=self.node.name, item=self.id)) getattr(self, "_fix_" + fix_type)(status)
def _validate_required_attributes(cls, bundle, item_id, attributes): missing = [] for attrname in cls.REQUIRED_ATTRIBUTES: if attrname not in attributes: missing.append(attrname) if missing: raise BundleError(_( "{item} in bundle '{bundle}' missing required attribute(s): {attrs}" ).format( item=item_id, bundle=bundle.name, attrs=", ".join(missing), ))
def content_processor_mako(item): from mako.template import Template template = Template( item._template_content, input_encoding='utf-8', output_encoding=item.attributes['encoding'], ) LOG.debug("{}:{}: rendering with Mako...".format(item.node.name, item.id)) start = datetime.now() try: content = template.render( item=item, bundle=item.bundle, node=item.node, repo=item.node.repo, **item.attributes['context'] ) except Exception as e: LOG.debug("".join(format_exception(*exc_info()))) if isinstance(e, NameError) and e.message == "Undefined": # Mako isn't very verbose here. Try to give a more useful # error message - even though we can't pinpoint the excat # location of the error. :/ e = _("Undefined variable (look for '${...}')") raise TemplateError(_( "Error while rendering template for {node}:{item}: {error}" ).format( error=e, item=item.id, node=item.node.name, )) duration = datetime.now() - start LOG.debug("{node}:{item}: rendered in {time}s".format( item=item.id, node=item.node.name, time=duration.total_seconds(), )) return content