diff -Nru bundlewrap-4.4.2/AUTHORS bundlewrap-4.5.1/AUTHORS --- bundlewrap-4.4.2/AUTHORS 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/AUTHORS 2021-02-19 09:07:26.000000000 +0000 @@ -5,3 +5,4 @@ Peter Hofmann Tim Buchwaldt Rico Ullmann +Christian Nicolai diff -Nru bundlewrap-4.4.2/bundlewrap/cmdline/apply.py bundlewrap-4.5.1/bundlewrap/cmdline/apply.py --- bundlewrap-4.4.2/bundlewrap/cmdline/apply.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/cmdline/apply.py 2021-02-19 09:07:26.000000000 +0000 @@ -61,6 +61,7 @@ 'autoonly_selector': args['autoonly'], 'force': args['force'], 'interactive': args['interactive'], + 'show_diff': args['show_diff'], 'skip_list': skip_list, 'workers': args['item_workers'], }, diff -Nru bundlewrap-4.4.2/bundlewrap/cmdline/diff.py bundlewrap-4.5.1/bundlewrap/cmdline/diff.py --- bundlewrap-4.4.2/bundlewrap/cmdline/diff.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/cmdline/diff.py 2021-02-19 09:07:26.000000000 +0000 @@ -4,7 +4,7 @@ from ..metadata import metadata_to_json from ..repo import Repository from ..utils.cmdline import get_target_nodes -from ..utils.dicts import diff_keys +from ..utils.dicts import diff_keys, diff_value from ..utils.scm import get_git_branch, get_git_rev, set_git_rev from ..utils.text import force_text, mark_for_translation as _, red, blue, yellow from ..utils.ui import io, QUIT_EVENT @@ -43,7 +43,10 @@ item_b_dict['content'] = item_b.content relevant_keys = diff_keys(item_a_dict, item_b_dict) - io.stdout(item_a.ask(item_b_dict, item_a_dict, relevant_keys)) + io.stdout("\n".join( + diff_value(key, item_a_dict[key], item_b_dict[key]) + for key in relevant_keys + )) def diff_node(node_a, node_b): diff -Nru bundlewrap-4.4.2/bundlewrap/cmdline/parser.py bundlewrap-4.5.1/bundlewrap/cmdline/parser.py --- bundlewrap-4.4.2/bundlewrap/cmdline/parser.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/cmdline/parser.py 2021-02-19 09:07:26.000000000 +0000 @@ -89,6 +89,13 @@ help=HELP_get_target_nodes, ) parser_apply.add_argument( + "-D", + "--no-diff", + action='store_false', + dest='show_diff', + help=_("hide diff for incorrect items when NOT using --interactive"), + ) + parser_apply.add_argument( "-f", "--force", action='store_true', @@ -956,6 +963,13 @@ dest='show_all', help=_("show correct items as well as incorrect ones"), ) + parser_verify.add_argument( + "-D", + "--no-diff", + action='store_false', + dest='show_diff', + help=_("hide diff for incorrect items"), + ) bw_verify_p_default = int(environ.get("BW_NODE_WORKERS", "4")) parser_verify.add_argument( "-p", diff -Nru bundlewrap-4.4.2/bundlewrap/cmdline/verify.py bundlewrap-4.5.1/bundlewrap/cmdline/verify.py --- bundlewrap-4.4.2/bundlewrap/cmdline/verify.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/cmdline/verify.py 2021-02-19 09:07:26.000000000 +0000 @@ -127,6 +127,7 @@ 'task_id': node.name, 'kwargs': { 'show_all': args['show_all'], + 'show_diff': args['show_diff'], 'workers': args['item_workers'], }, } diff -Nru bundlewrap-4.4.2/bundlewrap/exceptions.py bundlewrap-4.5.1/bundlewrap/exceptions.py --- bundlewrap-4.4.2/bundlewrap/exceptions.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/exceptions.py 2021-02-19 09:07:26.000000000 +0000 @@ -1,10 +1,3 @@ -class ActionFailure(Exception): - """ - Raised when an action failes to meet the expected rcode/output. - """ - pass - - class DontCache(Exception): """ Used in the cached_property decorator to temporily prevent caching diff -Nru bundlewrap-4.4.2/bundlewrap/__init__.py bundlewrap-4.5.1/bundlewrap/__init__.py --- bundlewrap-4.4.2/bundlewrap/__init__.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/__init__.py 2021-02-19 09:07:26.000000000 +0000 @@ -1,2 +1,2 @@ -VERSION = (4, 4, 2) +VERSION = (4, 5, 1) VERSION_STRING = ".".join([str(v) for v in VERSION]) diff -Nru bundlewrap-4.4.2/bundlewrap/items/actions.py bundlewrap-4.5.1/bundlewrap/items/actions.py --- bundlewrap-4.4.2/bundlewrap/items/actions.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/actions.py 2021-02-19 09:07:26.000000000 +0000 @@ -1,6 +1,6 @@ from datetime import datetime -from bundlewrap.exceptions import ActionFailure, BundleError +from bundlewrap.exceptions import BundleError from bundlewrap.items import format_comment, Item from bundlewrap.utils import Fault from bundlewrap.utils.ui import io @@ -8,6 +8,15 @@ from bundlewrap.utils.text import blue, bold, wrap_question +class ActionFailure(Exception): + """ + Raised when an action failes to meet the expected rcode/output. + """ + + def __init__(self, failed_expectations): + self.failed_expectations = failed_expectations + + class Action(Item): """ A command that is run on a node. @@ -18,7 +27,7 @@ 'data_stdin': None, 'expected_stderr': None, 'expected_stdout': None, - 'expected_return_code': 0, + 'expected_return_code': {0}, 'interactive': None, } ITEM_TYPE_NAME = 'action' @@ -32,6 +41,7 @@ other_peoples_soft_locks=(), interactive=False, interactive_default=True, + show_diff=True, ): if self._faults_missing_for_attributes: if self.error_on_missing_fault: @@ -47,25 +57,25 @@ item=self.id, node=self.node.name, )) - return (self.STATUS_SKIPPED, self.SKIP_REASON_FAULT_UNAVAILABLE) + return (self.STATUS_SKIPPED, self.SKIP_REASON_FAULT_UNAVAILABLE, None, None) 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)) - return (self.STATUS_SKIPPED, self.SKIP_REASON_CMDLINE) + return (self.STATUS_SKIPPED, self.SKIP_REASON_CMDLINE, None, None) 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) + return (self.STATUS_SKIPPED, self.SKIP_REASON_CMDLINE, None, None) if self._skip_with_soft_locks(my_soft_locks, other_peoples_soft_locks): - return (self.STATUS_SKIPPED, self.SKIP_REASON_SOFTLOCK) + return (self.STATUS_SKIPPED, self.SKIP_REASON_SOFTLOCK, None, None) if interactive is False and self.attributes['interactive'] is True: - return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE_ONLY) + return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE_ONLY, None, None) for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): @@ -81,7 +91,7 @@ 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) + return (self.STATUS_SKIPPED, self.SKIP_REASON_NO_TRIGGER, None, None) if self.unless: with io.job(_("{node} {bundle} {item} checking 'unless' condition").format( @@ -99,7 +109,7 @@ name=self.name, node=self.bundle.node.name, )) - return (self.STATUS_SKIPPED, self.SKIP_REASON_UNLESS) + return (self.STATUS_SKIPPED, self.SKIP_REASON_UNLESS, None, None) question_body = "" if self.attributes['data_stdin'] is not None: @@ -130,12 +140,12 @@ ), ) ): - return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE) + return (self.STATUS_SKIPPED, self.SKIP_REASON_INTERACTIVE, None, None) try: self.run() - return (self.STATUS_ACTION_SUCCEEDED, None) + return (self.STATUS_ACTION_SUCCEEDED, None, None, None) except ActionFailure as exc: - return (self.STATUS_FAILED, [str(exc)]) + return (self.STATUS_FAILED, exc.failed_expectations, None, None) def apply(self, *args, **kwargs): return self.get_result(*args, **kwargs) @@ -151,17 +161,17 @@ ) start_time = datetime.now() - status_code = self._get_result(*args, **kwargs) + result = self._get_result(*args, **kwargs) self.node.repo.hooks.action_run_end( self.node.repo, self.node, self, duration=datetime.now() - start_time, - status=status_code[0], + status=result[0], ) - return status_code + return result def run(self): if self.attributes['data_stdin'] is not None: @@ -186,20 +196,36 @@ may_fail=True, ) + failed_expectations = ({}, {}, []) + 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)) + result.return_code not in self.attributes['expected_return_code']: + failed_expectations[0][_("return code")] = str(self.attributes['expected_return_code']) + failed_expectations[1][_("return code")] = str(result.return_code) + failed_expectations[2].append(_("return code")) if self.attributes['expected_stderr'] is not None and \ result.stderr_text != self.attributes['expected_stderr']: - raise ActionFailure(_("wrong stderr")) + failed_expectations[0][_("stderr")] = self.attributes['expected_stderr'] + failed_expectations[1][_("stderr")] = result.stderr_text + failed_expectations[2].append(_("stderr")) if self.attributes['expected_stdout'] is not None and \ result.stdout_text != self.attributes['expected_stdout']: - raise ActionFailure(_("wrong stdout")) + failed_expectations[0][_("stdout")] = self.attributes['expected_stdout'] + failed_expectations[1][_("stdout")] = result.stdout_text + failed_expectations[2].append(_("stdout")) + + if failed_expectations[2]: + raise ActionFailure(failed_expectations) return result + def patch_attributes(self, attributes): + if isinstance(attributes.get('expected_return_code'), int): + attributes['expected_return_code'] = {attributes['expected_return_code']} + return attributes + @classmethod def validate_attributes(cls, bundle, item_id, attributes): if attributes.get('interactive', None) not in (True, False, None): @@ -209,6 +235,6 @@ def verify(self): if self.unless and self.cached_unless_result: - return self.cached_unless_result, None + return self.cached_unless_result, None, None else: raise NotImplementedError diff -Nru bundlewrap-4.4.2/bundlewrap/items/directories.py bundlewrap-4.5.1/bundlewrap/items/directories.py --- bundlewrap-4.4.2/bundlewrap/items/directories.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/directories.py 2021-02-19 09:07:26.000000000 +0000 @@ -68,6 +68,10 @@ cdict[optional_attr] = self.attributes[optional_attr] return cdict + def display_on_create(self, cdict): + del cdict['type'] + return cdict + def display_dicts(self, cdict, sdict, keys): try: keys.remove('paths_to_purge') diff -Nru bundlewrap-4.4.2/bundlewrap/items/files.py bundlewrap-4.5.1/bundlewrap/items/files.py --- bundlewrap-4.4.2/bundlewrap/items/files.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/files.py 2021-02-19 09:07:26.000000000 +0000 @@ -16,6 +16,7 @@ from bundlewrap.items import BUILTIN_ITEM_ATTRIBUTES, Item from bundlewrap.items.directories import validator_mode from bundlewrap.utils import cached_property, hash_local_file, sha1, tempfile +from bundlewrap.utils.dicts import diff_value_text from bundlewrap.utils.remote import PathInfo from bundlewrap.utils.text import force_text, mark_for_translation as _ from bundlewrap.utils.text import is_subdirectory @@ -340,6 +341,16 @@ 'size': path_info.size, } + def display_on_create(self, cdict): + if ( + self.attributes['content_type'] not in ('any', 'base64', 'binary') and + len(self.content) < DIFF_MAX_FILE_SIZE + ): + del cdict['content_hash'] + cdict['content'] = diff_value_text("", "", force_text(self.content)).rstrip("\n") + del cdict['type'] + return cdict + def display_dicts(self, cdict, sdict, keys): if ( 'content_hash' in keys and @@ -359,6 +370,22 @@ keys.remove('content_hash') return (cdict, sdict, keys) + def display_on_delete(self, sdict): + del sdict['content_hash'] + path_info = PathInfo(self.node, self.name) + if ( + sdict['size'] < DIFF_MAX_FILE_SIZE and + path_info.is_text_file + ): + sdict['content'] = diff_value_text( + "", + get_remote_file_contents(self.node, self.name), + "", + ).rstrip("\n") + if path_info.is_file: + sdict['size'] = f"{sdict['size']} bytes" + return sdict + def patch_attributes(self, attributes): if ( 'content' not in attributes and diff -Nru bundlewrap-4.4.2/bundlewrap/items/groups.py bundlewrap-4.5.1/bundlewrap/items/groups.py --- bundlewrap-4.4.2/bundlewrap/items/groups.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/groups.py 2021-02-19 09:07:26.000000000 +0000 @@ -33,7 +33,7 @@ @classmethod def block_concurrent(cls, node_os, node_os_version): # https://github.com/bundlewrap/bundlewrap/issues/367 - if node_os == 'openbsd': + if node_os in ('freebsd', 'openbsd'): return [cls.ITEM_TYPE_NAME] else: return [] @@ -50,25 +50,20 @@ return cdict def fix(self, status): - if status.must_be_created: - if self.attributes['gid'] is None: - command = "groupadd {}".format(self.name) - else: - command = "groupadd -g {gid} {groupname}".format( - gid=self.attributes['gid'], - groupname=self.name, - ) - self.run(command, may_fail=True) - elif status.must_be_deleted: - self.run("groupdel {}".format(self.name), may_fail=True) + if self.node.os == 'freebsd': + command = "pw " else: - self.run( - "groupmod -g {gid} {groupname}".format( - gid=self.attributes['gid'], - groupname=self.name, - ), - may_fail=True, - ) + command = "" + + if status.must_be_deleted: + command += f"groupdel {self.name}" + else: + command += "groupadd " if status.must_be_created else "groupmod " + command += f"{self.name} " + + if self.attributes['gid'] is not None: + command += "-g {}".format(self.attributes['gid']) + self.run(command, may_fail=True) def sdict(self): # verify content of /etc/group diff -Nru bundlewrap-4.4.2/bundlewrap/items/__init__.py bundlewrap-4.5.1/bundlewrap/items/__init__.py --- bundlewrap-4.4.2/bundlewrap/items/__init__.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/__init__.py 2021-02-19 09:07:26.000000000 +0000 @@ -45,7 +45,7 @@ result = "\n\n" for line in wrapper.wrap(cleandoc(comment)): for inlineline in line.split("\n"): - result += "# {}\n".format(italic(inlineline)) + result += "{} {}\n".format(bold("#"), italic(inlineline)) return result @@ -55,7 +55,7 @@ fixing and what's broken. """ - def __init__(self, cdict, sdict, display_dicts): + def __init__(self, cdict, sdict): self.cdict = cdict self.sdict = sdict self.keys_to_fix = [] @@ -64,12 +64,6 @@ if not self.must_be_deleted and not self.must_be_created: self.keys_to_fix = diff_keys(cdict, sdict) - self.display_cdict, self.display_sdict, self.display_keys_to_fix = display_dicts( - copy(cdict), - copy(sdict), - copy(self.keys_to_fix), - ) - def __repr__(self): return "".format(self.correct) @@ -84,7 +78,12 @@ lists and have them converted to the proper type automatically. """ if type(attribute_default) in (dict, list, set, tuple): - return type(attribute_default) + def normalize(attribute_value): + if attribute_value is None: + return attribute_value + else: + return type(attribute_default)(attribute_value) + return normalize else: return copy @@ -430,6 +429,7 @@ other_peoples_soft_locks=(), interactive=False, interactive_default=True, + show_diff=True, ): self.node.repo.hooks.item_apply_start( self.node.repo, @@ -439,6 +439,7 @@ status_code = None status_before = None status_after = None + details = None start_time = datetime.now() if not self.covered_by_autoonly_selector(autoonly_selector): @@ -446,18 +447,18 @@ "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 + details = 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 + details = 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 + details = self.SKIP_REASON_SOFTLOCK for item in self._precedes_items: if item._triggers_preceding_items(interactive=interactive): @@ -476,14 +477,14 @@ "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 + details = 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 + details = self.SKIP_REASON_UNLESS if self._faults_missing_for_attributes and status_code is None: if self.error_on_missing_fault: @@ -500,7 +501,7 @@ node=self.node.name, )) status_code = self.STATUS_SKIPPED - skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE + details = self.SKIP_REASON_FAULT_UNAVAILABLE if status_code is None: try: @@ -518,10 +519,22 @@ node=self.node.name, )) status_code = self.STATUS_SKIPPED - skip_reason = self.SKIP_REASON_FAULT_UNAVAILABLE + details = self.SKIP_REASON_FAULT_UNAVAILABLE else: if status_before.correct: status_code = self.STATUS_OK + elif show_diff or interactive: + if status_before.must_be_created: + details = self.display_on_create(copy(status_before.cdict)) + elif status_before.must_be_deleted: + details = self.display_on_delete(copy(status_before.sdict)) + else: + details = self.display_dicts( + copy(status_before.cdict), + copy(status_before.sdict), + # TODO remove sorted() in 5.0 to pass a set + sorted(copy(status_before.keys_to_fix)), + ) if status_code is None: if not interactive: @@ -533,21 +546,34 @@ self.fix(status_before) else: if status_before.must_be_created: - question_text = _("Doesn't exist. Will be created.") + question_text = "\n".join( + f"{bold(key)} {value}" + for key, value in sorted(details.items()) + ) + prompt = _("Create {}?").format(bold(self.id)) elif status_before.must_be_deleted: - question_text = _("Found on node. Will be removed.") + question_text = "\n".join( + f"{bold(key)} {value}" + for key, value in sorted(details.items()) + ) + prompt = _("Delete {}?").format(bold(self.id)) else: - question_text = self.ask( - status_before.display_cdict, - status_before.display_sdict, - status_before.display_keys_to_fix, + display_cdict, display_sdict, display_keys_to_fix = details + question_text = "\n".join( + diff_value( + key, + display_sdict[key], + display_cdict[key], + ) + for key in sorted(display_keys_to_fix) ) + prompt = _("Fix {}?").format(bold(self.id)) if self.comment: question_text += format_comment(self.comment) question = wrap_question( self.id, question_text, - _("Fix {}?").format(bold(self.id)), + prompt, prefix="{x} {node} ".format( node=bold(self.node.name), x=blue("?"), @@ -570,25 +596,12 @@ self.fix(status_before) else: status_code = self.STATUS_SKIPPED - skip_reason = self.SKIP_REASON_INTERACTIVE + details = 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, @@ -598,7 +611,12 @@ status_before=status_before, status_after=status_after, ) - return (status_code, details) + return ( + status_code, + details, + status_before.must_be_created if status_before else None, + status_before.must_be_deleted if status_before else None, + ) def run_local(self, command, **kwargs): result = run_local(command, **kwargs) @@ -616,16 +634,6 @@ }) return result - def ask(self, status_should, status_actual, relevant_keys): - """ - Returns a string asking the user if this item should be - implemented. - """ - result = [] - for key in relevant_keys: - result.append(diff_value(key, status_actual[key], status_should[key])) - return "\n\n".join(result) - def cdict(self): """ Return a statedict that describes the target state of this item @@ -725,7 +733,7 @@ )): if not cached: del self._cache['cached_sdict'] - return ItemStatus(self.cached_cdict, self.cached_sdict, self.display_dicts) + return ItemStatus(self.cached_cdict, self.cached_sdict) def hash(self): return hash_statedict(self.cached_cdict) @@ -738,8 +746,29 @@ return "{}:{}".format(self.ITEM_TYPE_NAME, self.name) def verify(self): - return self.cached_unless_result, self.cached_status + if self.cached_status.must_be_created: + display = self.display_on_create(copy(self.cached_status.cdict)) + elif self.cached_status.must_be_deleted: + display = self.display_on_delete(copy(self.cached_status.sdict)) + else: + display = self.display_dicts( + copy(self.cached_status.cdict), + copy(self.cached_status.sdict), + # TODO remove sorted() in 5.0 to pass a set + sorted(copy(self.cached_status.keys_to_fix)), + ) + return self.cached_unless_result, self.cached_status, display + def display_on_create(self, cdict): + """ + Given a cdict as implemented above, modify it to better suit + interactive presentation when an item is created. + + MAY be overridden by subclasses. + """ + return cdict + + # TODO rename to display_on_fix in 5.0 def display_dicts(self, cdict, sdict, keys): """ Given cdict and sdict as implemented above, modify them to @@ -750,6 +779,15 @@ """ return (cdict, sdict, keys) + def display_on_delete(self, sdict): + """ + Given an sdict as implemented above, modify it to better suit + interactive presentation when an item is deleted. + + MAY be overridden by subclasses. + """ + return sdict + def patch_attributes(self, attributes): """ Allows an item to preprocess the attributes it is initialized diff -Nru bundlewrap-4.4.2/bundlewrap/items/kubernetes.py bundlewrap-4.5.1/bundlewrap/items/kubernetes.py --- bundlewrap-4.4.2/bundlewrap/items/kubernetes.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/kubernetes.py 2021-02-19 09:07:26.000000000 +0000 @@ -7,7 +7,7 @@ from bundlewrap.metadata import metadata_to_json from bundlewrap.items import BUILTIN_ITEM_ATTRIBUTES, Item from bundlewrap.items.files import content_processor_jinja2, content_processor_mako -from bundlewrap.utils.dicts import merge_dict, reduce_dict +from bundlewrap.utils.dicts import diff_value_text, merge_dict, reduce_dict from bundlewrap.utils.ui import io from bundlewrap.utils.text import force_text, mark_for_translation as _ import yaml @@ -57,6 +57,14 @@ indent=4, sort_keys=True, )} + def display_on_create(self, cdict): + cdict['manifest'] = diff_value_text("", "", force_text(cdict['manifest'])).rstrip("\n") + return cdict + + def display_on_delete(self, sdict): + sdict['manifest'] = diff_value_text("", force_text(sdict['manifest']), "").rstrip("\n") + return sdict + def fix(self, status): if status.must_be_deleted: result = self.run_local(self._kubectl + ["delete", self.KIND, self.resource_name]) diff -Nru bundlewrap-4.4.2/bundlewrap/items/pkg_freebsd.py bundlewrap-4.5.1/bundlewrap/items/pkg_freebsd.py --- bundlewrap-4.4.2/bundlewrap/items/pkg_freebsd.py 1970-01-01 00:00:00.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/pkg_freebsd.py 2021-02-19 09:07:26.000000000 +0000 @@ -0,0 +1,92 @@ +from shlex import quote + +from bundlewrap.exceptions import BundleError +from bundlewrap.items import Item +from bundlewrap.utils.text import mark_for_translation as _ + + +def parse_pkg_name(pkgname, line): + # Contains the assumption that version may not contain '-', which is covered + # according to the FreeBSD docs (Section 5.2.4, "PKGNAMEPREFIX and PKGNAMESUFFIX") + installed_package, _sep, installed_version = line.rpartition('-') + assert installed_package != "", _( + "Unexpected FreeBSD package name: {line}").format(line=line) + return installed_package == pkgname, installed_version + + +def pkg_install(node, pkgname, version): + # Setting version to None means "don't specify version". + if version is None: + full_name = pkgname + else: + full_name = pkgname + "-" + version + + return node.run("pkg install -y {}".format(full_name), may_fail=True) + + +def pkg_installed(node, pkgname): + result = node.run( + "pkg info | cut -f 1 -d ' '", + may_fail=True, + ) + for line in result.stdout.decode('utf-8').strip().splitlines(): + found, installed_version = parse_pkg_name(pkgname, line) + if found: + return installed_version + + return False + + +def pkg_remove(node, pkgname): + return node.run("pkg delete -y -R {}".format(quote(pkgname)), may_fail=True) + + +class FreeBSDPkg(Item): + """ + A package installed via pkg install/pkg delete. + """ + BUNDLE_ATTRIBUTE_NAME = "pkg_freebsd" + ITEM_ATTRIBUTES = { + 'installed': True, + 'version': None, + } + ITEM_TYPE_NAME = "pkg_freebsd" + + def __repr__(self): + return "".format( + self.name, + self.attributes['installed'], + ) + + def cdict(self): + cdict = self.attributes.copy() + if cdict['version'] is None or not cdict['installed']: + del cdict['version'] + return cdict + + def fix(self, status): + if self.attributes['installed'] is False: + pkg_remove(self.node, self.name) + else: + pkg_install( + self.node, + self.name, + self.attributes['version'] + ) + + def sdict(self): + version = pkg_installed(self.node, self.name) + return { + 'installed': bool(version), + 'version': version if version else _("none"), + } + + @classmethod + 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, + )) diff -Nru bundlewrap-4.4.2/bundlewrap/items/svc_openbsd.py bundlewrap-4.5.1/bundlewrap/items/svc_openbsd.py --- bundlewrap-4.4.2/bundlewrap/items/svc_openbsd.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/svc_openbsd.py 2021-02-19 09:07:26.000000000 +0000 @@ -71,6 +71,14 @@ def get_canned_actions(self): return { + 'stop': { + 'command': "/etc/rc.d/{0} stop".format(self.name), + 'needed_by': {self.id}, + }, + 'stopstart': { + 'command': "/etc/rc.d/{0} stop && /etc/rc.d/{0} start".format(self.name), + 'needs': {self.id}, + }, 'restart': { 'command': "/etc/rc.d/{} restart".format(self.name), 'needs': { @@ -82,10 +90,6 @@ self.id, }, }, - 'stopstart': { - 'command': "/etc/rc.d/{0} stop && /etc/rc.d/{0} start".format(self.name), - 'needs': {self.id}, - }, } def sdict(self): diff -Nru bundlewrap-4.4.2/bundlewrap/items/svc_systemd.py bundlewrap-4.5.1/bundlewrap/items/svc_systemd.py --- bundlewrap-4.4.2/bundlewrap/items/svc_systemd.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/svc_systemd.py 2021-02-19 09:07:26.000000000 +0000 @@ -39,6 +39,22 @@ def svc_disable(node, svcname): return node.run("systemctl disable -- {}".format(quote(svcname)), may_fail=True) +def svc_mask(node, svcname): + return node.run("systemctl mask -- {}".format(quote(svcname)), may_fail=True) + +def svc_masked(node, svcname): + result = node.run( + "systemctl is-enabled -- {}".format(quote(svcname)), + may_fail=True, + ) + return ( + result.return_code == 1 and + force_text(result.stdout).strip() == "masked" + ) + +def svc_unmask(node, svcname): + return node.run("systemctl unmask -- {}".format(quote(svcname)), may_fail=True) + class SvcSystemd(Item): """ @@ -48,14 +64,16 @@ ITEM_ATTRIBUTES = { 'enabled': True, 'running': True, + 'masked': False, } ITEM_TYPE_NAME = "svc_systemd" def __repr__(self): - return "".format( + return "".format( self.name, self.attributes['enabled'], self.attributes['running'], + self.attributes['masked'], ) def cdict(self): @@ -66,6 +84,12 @@ return cdict def fix(self, status): + if 'masked' in status.keys_to_fix: + if self.attributes['masked']: + svc_mask(self.node, self.name) + else: + svc_unmask(self.node, self.name) + if 'enabled' in status.keys_to_fix: if self.attributes['enabled']: svc_enable(self.node, self.name) @@ -80,6 +104,14 @@ def get_canned_actions(self): return { + 'stop': { + 'command': "systemctl stop -- {}".format(self.name), + 'needed_by': {self.id}, + }, + 'restart': { + 'command': "systemctl restart -- {}".format(self.name), + 'needs': {self.id}, + }, 'reload': { 'command': "systemctl reload -- {}".format(self.name), 'needs': { @@ -91,16 +123,13 @@ self.id, }, }, - 'restart': { - 'command': "systemctl restart -- {}".format(self.name), - 'needs': {self.id}, - }, } def sdict(self): return { 'enabled': svc_enabled(self.node, self.name), 'running': svc_running(self.node, self.name), + 'masked': svc_masked(self.node, self.name), } @classmethod diff -Nru bundlewrap-4.4.2/bundlewrap/items/svc_systemv.py bundlewrap-4.5.1/bundlewrap/items/svc_systemv.py --- bundlewrap-4.4.2/bundlewrap/items/svc_systemv.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/svc_systemv.py 2021-02-19 09:07:26.000000000 +0000 @@ -45,6 +45,14 @@ def get_canned_actions(self): return { + 'stop': { + 'command': "/etc/init.d/{} stop".format(self.name), + 'needed_by': {self.id}, + }, + 'restart': { + 'command': "/etc/init.d/{} restart".format(self.name), + 'needs': {self.id}, + }, 'reload': { 'command': "/etc/init.d/{} reload".format(self.name), 'needs': { @@ -56,10 +64,6 @@ self.id, }, }, - 'restart': { - 'command': "/etc/init.d/{} restart".format(self.name), - 'needs': {self.id}, - }, } def sdict(self): diff -Nru bundlewrap-4.4.2/bundlewrap/items/svc_upstart.py bundlewrap-4.5.1/bundlewrap/items/svc_upstart.py --- bundlewrap-4.4.2/bundlewrap/items/svc_upstart.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/svc_upstart.py 2021-02-19 09:07:26.000000000 +0000 @@ -44,16 +44,13 @@ def get_canned_actions(self): return { - 'reload': { - 'command': "reload {}".format(self.name), - 'needs': { - # make sure we don't restart and reload simultaneously - f"{self.id}:restart", - # with only the dep on restart, we might still end - # up reloading if the service itself is skipped - # because the restart action has cascade_skip False - self.id, - }, + 'stop': { + 'command': "stop {0}".format(self.name), + 'needed_by': {self.id}, + }, + 'stopstart': { + 'command': "stop {0} && start {0}".format(self.name), + 'needs': {self.id}, }, 'restart': { 'command': "restart {}".format(self.name), @@ -66,9 +63,16 @@ self.id, }, }, - 'stopstart': { - 'command': "stop {0} && start {0}".format(self.name), - 'needs': {self.id}, + 'reload': { + 'command': "reload {}".format(self.name), + 'needs': { + # make sure we don't restart and reload simultaneously + f"{self.id}:restart", + # with only the dep on restart, we might still end + # up reloading if the service itself is skipped + # because the restart action has cascade_skip False + self.id, + }, }, } diff -Nru bundlewrap-4.4.2/bundlewrap/items/symlinks.py bundlewrap-4.5.1/bundlewrap/items/symlinks.py --- bundlewrap-4.4.2/bundlewrap/items/symlinks.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/symlinks.py 2021-02-19 09:07:26.000000000 +0000 @@ -41,6 +41,10 @@ cdict[optional_attr] = self.attributes[optional_attr] return cdict + def display_on_create(self, cdict): + del cdict['type'] + return cdict + def fix(self, status): if status.must_be_created or 'type' in status.keys_to_fix: # fixing the type fixes everything diff -Nru bundlewrap-4.4.2/bundlewrap/items/users.py bundlewrap-4.5.1/bundlewrap/items/users.py --- bundlewrap-4.4.2/bundlewrap/items/users.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/items/users.py 2021-02-19 09:07:26.000000000 +0000 @@ -108,7 +108,7 @@ @classmethod def block_concurrent(cls, node_os, node_os_version): # https://github.com/bundlewrap/bundlewrap/issues/367 - if node_os == 'openbsd': + if node_os in ('openbsd', 'freebsd'): return [cls.ITEM_TYPE_NAME] else: return [] @@ -133,20 +133,46 @@ return cdict def fix(self, status): + if self.node.os == 'freebsd': + # FreeBSD implements the user{add,mod,del} commands using pw(8). + command = "pw " + else: + command = "" + if status.must_be_deleted: - self.run("userdel {}".format(self.name), may_fail=True) + command += "userdel {}" + self.run(command.format(self.name), may_fail=True) else: - command = "useradd " if status.must_be_created else "usermod " + command += "useradd " if status.must_be_created else "usermod " + command += f"{self.name} " + + stdin = None for attr, option in sorted(_ATTRIBUTE_OPTIONS.items()): if (attr in status.keys_to_fix or status.must_be_created) and \ self.attributes[attr] is not None: if attr == 'groups': value = ",".join(self.attributes[attr]) + elif attr == 'password_hash' and self.node.os == 'freebsd': + # On FreeBSD, pw useradd/usermod -p sets the password expiry time. + # Using -H we pass the password hash using file descriptor instead. + option = '-H' + value = '0' # FD 0 = stdin + stdin = self.attributes[attr].encode() else: value = str(self.attributes[attr]) command += "{} {} ".format(option, quote(value)) - command += self.name - self.run(command, may_fail=True) + self.run(command, data_stdin=stdin, may_fail=True) + + def display_on_create(self, cdict): + for attr_name, attr_display_name in _ATTRIBUTE_NAMES.items(): + if attr_name == attr_display_name: + # Don't change anything; the `del` below would + # always remove the key entirely! + continue + if attr_name in cdict: + cdict[attr_display_name] = cdict[attr_name] + del cdict[attr_name] + return cdict def display_dicts(self, cdict, sdict, keys): for attr_name, attr_display_name in _ATTRIBUTE_NAMES.items(): @@ -252,8 +278,8 @@ 'hash_method', self.ITEM_ATTRIBUTES['hash_method'], )] - salt = attributes.get('salt', None) - if self.node.os in self.node.OS_FAMILY_BSD: + salt = force_text(attributes.get('salt', None)) + if self.node.os == 'openbsd': attributes['password_hash'] = bcrypt.encrypt( force_text(attributes['password']), rounds=8, # default rounds for OpenBSD accounts diff -Nru bundlewrap-4.4.2/bundlewrap/node.py bundlewrap-4.5.1/bundlewrap/node.py --- bundlewrap-4.4.2/bundlewrap/node.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/node.py 2021-02-19 09:07:26.000000000 +0000 @@ -28,6 +28,7 @@ from .utils import cached_property, error_context, get_file_contents, names from .utils.dicts import ( dict_to_toml, + diff_value, hash_statedict, set_key_at_path, validate_dict, @@ -116,7 +117,16 @@ return ", ".join(output) -def handle_apply_result(node, item, status_code, interactive, details=None): +def handle_apply_result( + node, + item, + status_code, + interactive=False, + details=None, + show_diff=True, + created=None, + deleted=None, +): if status_code == Item.STATUS_SKIPPED and details in ( Item.SKIP_REASON_NO_TRIGGER, Item.SKIP_REASON_UNLESS, @@ -126,10 +136,13 @@ formatted_result = format_item_result( status_code, node.name, - item.bundle.name if item.bundle else "", # dummy items don't have bundles + item.bundle.name, item.id, interactive=interactive, details=details, + show_diff=show_diff, + created=created, + deleted=deleted, ) if formatted_result is not None: if status_code == Item.STATUS_FAILED: @@ -150,6 +163,7 @@ other_peoples_soft_locks=(), workers=1, interactive=False, + show_diff=True, ): item_queue = ItemQueue(node) # the item queue might contain new generated items (canned actions) @@ -173,6 +187,7 @@ 'my_soft_locks': my_soft_locks, 'other_peoples_soft_locks': other_peoples_soft_locks, 'interactive': interactive, + 'show_diff': show_diff, }, } @@ -180,7 +195,7 @@ item_id = task_id.split(":", 1)[1] item = find_item(item_id, item_queue.pending_items) - status_code, details = return_value + status_code, details, created, deleted = return_value if status_code == Item.STATUS_FAILED: for skipped_item in item_queue.item_failed(item): @@ -188,7 +203,7 @@ node, skipped_item, Item.STATUS_SKIPPED, - interactive, + interactive=interactive, details=Item.SKIP_REASON_DEP_FAILED, ) results.append((skipped_item.id, Item.STATUS_SKIPPED, timedelta(0))) @@ -208,7 +223,7 @@ node, skipped_item, Item.STATUS_SKIPPED, - interactive, + interactive=interactive, details=skip_reason, ) results.append((skipped_item.id, Item.STATUS_SKIPPED, timedelta(0))) @@ -220,7 +235,16 @@ ), )) - handle_apply_result(node, item, status_code, interactive, details=details) + handle_apply_result( + node, + item, + status_code, + interactive=interactive, + details=details, + show_diff=show_diff, + created=created, + deleted=deleted, + ) io.progress_advance() results.append((item.id, status_code, duration)) @@ -317,28 +341,59 @@ return output.lstrip('\n') -def format_item_result(result, node, bundle, item, interactive=False, details=None): - if details is True: - details_text = "({})".format(_("create")) - elif details is False: - details_text = "({})".format(_("remove")) - elif details is None: +def format_item_result( + result, + node, + bundle, + item, + interactive=False, + details=None, + show_diff=True, + created=None, + deleted=None, +): + if created or deleted or details is None: details_text = "" elif result == Item.STATUS_SKIPPED: details_text = "({})".format(Item.SKIP_REASON_DESC[details]) else: - details_text = "({})".format(", ".join(sorted(details))) + details_text = "({})".format(", ".join(sorted(details[2]))) if result == Item.STATUS_FAILED: - return "{x} {node} {bundle} {item} {status} {details}".format( - bundle=bold(bundle), - details=details_text, - item=item, - node=bold(node), - status=red(_("failed")), - x=bold(red("✘")), - ) + if created: + status = red(_("failed to create")) + elif deleted: + status = red(_("failed to delete")) + else: + status = red(_("failed")) + if show_diff and not created and not deleted: + output = "{x} {node} {bundle} {item} {status}\n".format( + bundle=bold(bundle), + item=item, + node=bold(node), + status=status, + x=bold(red("✘")), + ) + diff = "\n" + for key in sorted(details[2]): + diff += diff_value(key, details[1][key], details[0][key]) + "\n" + for line in diff.splitlines(): + output += "{x} {line}\n".format( + line=line, + x=red("│"), + ) + output += red("╵") + return output + else: + return "{x} {node} {bundle} {item} {status} {details}".format( + bundle=bold(bundle), + details=details_text, + item=item, + node=bold(node), + status=status, + x=bold(red("✘")), + ) elif result == Item.STATUS_ACTION_SUCCEEDED: - return "{x} {node} {bundle} {item} {status}".format( + return "{x} {node} {bundle} {item} {status}".format( bundle=bold(bundle), item=item, node=bold(node), @@ -346,7 +401,7 @@ x=bold(green("✓")), ) elif result == Item.STATUS_SKIPPED: - return "{x} {node} {bundle} {item} {status} {details}".format( + return "{x} {node} {bundle} {item} {status} {details}".format( bundle=bold(bundle), details=details_text, item=item, @@ -355,14 +410,42 @@ status=yellow(_("skipped")), ) elif result == Item.STATUS_FIXED: - return "{x} {node} {bundle} {item} {status} {details}".format( - bundle=bold(bundle), - details=details_text, - item=item, - node=bold(node), - x=bold(green("✓")), - status=green(_("fixed")), - ) + if created: + status = green(_("created")) + elif deleted: + status = green(_("deleted")) + else: + status = green(_("fixed")) + if not interactive and show_diff: + output = "{x} {node} {bundle} {item} {status}\n".format( + bundle=bold(bundle), + item=item, + node=bold(node), + x=bold(green("✓")), + status=status, + ) + diff = "\n" + if created or deleted: + for key, value in sorted(details.items()): + diff += f"{bold(key)} {value}\n" + else: + for key in sorted(details[2]): + diff += diff_value(key, details[1][key], details[0][key]) + "\n" + for line in diff.splitlines(): + output += "{x} {line}\n".format( + line=line, + x=green("│"), + ) + output += green("╵") + return output + else: + return "{x} {node} {bundle} {item} {status}".format( + bundle=bold(bundle), + item=item, + node=bold(node), + x=bold(green("✓")), + status=status, + ) class Node: @@ -547,6 +630,7 @@ autoonly_selector="", interactive=False, force=False, + show_diff=True, skip_list=tuple(), workers=4, ): @@ -619,6 +703,7 @@ other_peoples_soft_locks=lock.other_peoples_soft_locks, workers=workers, interactive=interactive, + show_diff=show_diff, ) except NodeLockedException as e: if not interactive: @@ -794,14 +879,14 @@ wrapper_outer=self.cmd_wrapper_outer, ) - def verify(self, show_all=False, workers=4): + def verify(self, show_all=False, show_diff=True, workers=4): result = [] start = datetime.now() if not self.items: io.stdout(_("{x} {node} has no items").format(node=bold(self.name), x=yellow("!"))) else: - result = verify_items(self, show_all=show_all, workers=workers) + result = verify_items(self, show_all=show_all, show_diff=show_diff, workers=workers) return { 'good': result.count(True), @@ -846,7 +931,7 @@ setattr(Node, attr, build_attr_property(attr, default)) -def verify_items(node, show_all=False, workers=1): +def verify_items(node, show_all=False, show_diff=True, workers=1): items = [] for item in node.items: if not item.triggered: @@ -924,22 +1009,46 @@ def handle_result(task_id, return_value, duration): io.progress_advance() - unless_result, item_status = return_value + unless_result, item_status, display = return_value node_name, bundle_name, item_id = task_id.split(":", 2) if not unless_result and not item_status.correct: if item_status.must_be_created: - details_text = _("create") + details_text = red(_("missing")) elif item_status.must_be_deleted: - details_text = _("remove") + details_text = red(_("found")) + elif show_diff: + details_text = "" else: - details_text = ", ".join(sorted(item_status.display_keys_to_fix)) - io.stderr("{x} {node} {bundle} {item} ({details})".format( - bundle=bold(bundle_name), - details=details_text, - item=item_id, - node=bold(node_name), - x=red("✘"), - )) + details_text = ", ".join(sorted(display[2])) + if show_diff: + diff = "\n" + if item_status.must_be_created or item_status.must_be_deleted: + for key, value in sorted(display.items()): + diff += f"{bold(key)} {value}\n" + else: + for key in sorted(display[2]): + diff += diff_value(key, display[1][key], display[0][key]) + "\n" + output = "{x} {node} {bundle} {item} {details}\n".format( + bundle=bold(bundle_name), + details=details_text, + item=item_id, + node=bold(node_name), + x=red("✘"), + ) + for line in diff.splitlines(): + output += "{x} {line}\n".format( + line=line, + x=red("│"), + ) + io.stderr(output + red("╵")) + else: + io.stderr("{x} {node} {bundle} {item} {details}".format( + bundle=bold(bundle_name), + details=details_text, + item=item_id, + node=bold(node_name), + x=red("✘"), + )) return False else: if show_all: diff -Nru bundlewrap-4.4.2/bundlewrap/repo.py bundlewrap-4.5.1/bundlewrap/repo.py --- bundlewrap-4.4.2/bundlewrap/repo.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/repo.py 2021-02-19 09:07:26.000000000 +0000 @@ -271,6 +271,11 @@ Creates and returns a repository at path, which must exist and be empty. """ + if listdir(path): + raise ValueError(_("'{}' is not an empty directory".format( + path + ))) + for filename, content in INITIAL_CONTENT.items(): if callable(content): content = content() diff -Nru bundlewrap-4.4.2/bundlewrap/utils/dicts.py bundlewrap-4.5.1/bundlewrap/utils/dicts.py --- bundlewrap-4.4.2/bundlewrap/utils/dicts.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/bundlewrap/utils/dicts.py 2021-02-19 09:07:26.000000000 +0000 @@ -4,6 +4,12 @@ from json import dumps, JSONEncoder from tomlkit import document as toml_document +from tomlkit.items import ( + Bool as TOMLBool, + Float as TOMLFloat, + Integer as TOMLInteger, + String as TOMLString, +) from . import Fault from .text import bold, green, red @@ -58,13 +64,13 @@ only exists in the second one, it is disregarded. """ if sdict1 is None: - return [] + return set() if sdict2 is None: - return sdict1.keys() - differing_keys = [] + return set(sdict1.keys()) + differing_keys = set() for key, value in sdict1.items(): if value != sdict2[key]: - differing_keys.append(key) + differing_keys.add(key) return differing_keys @@ -142,7 +148,7 @@ elif line.startswith("-"): line = red(line) output += line + suffix + "\n" - return output + return output.rstrip("\n") TYPE_DIFFS = { @@ -154,16 +160,23 @@ set: diff_value_list, str: diff_value_text, tuple: diff_value_list, + TOMLBool: diff_value_bool, + TOMLFloat: diff_value_int, + TOMLInteger: diff_value_int, + TOMLString: diff_value_text, } def diff_value(title, value1, value2): - value_type = type(value1) - assert value_type == type(value2), "cannot compare {} with {}".format( - repr(value1), - repr(value2), - ) - diff_func = TYPE_DIFFS[value_type] + diff_func = TYPE_DIFFS[type(value1)] + diff_func2 = TYPE_DIFFS[type(value2)] + if diff_func != diff_func2: + raise TypeError(_("cannot compare {} ({}) with {} ({})").format( + repr(value1), + type(value1), + repr(value2), + type(value2), + )) return diff_func(title, value1, value2) diff -Nru bundlewrap-4.4.2/CHANGELOG.md bundlewrap-4.5.1/CHANGELOG.md --- bundlewrap-4.4.2/CHANGELOG.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/CHANGELOG.md 2021-02-19 09:07:26.000000000 +0000 @@ -1,3 +1,28 @@ +# 4.5.0 + +2021-02-19 + +* fixed actions that set `None` as `expected_return_code` + + +# 4.5.0 + +2021-02-19 + +* added diffs to the default output of `bw apply` and `bw verify` +* added `bw apply --no-diff` +* added `bw verify --no-diff` +* added `pkg_freebsd` +* added canned `stop` actions for services +* added `masked` attribute for `svc_systemd` +* added multiple expected return codes for actions +* improved error message for incompatible types in diff +* fixed group management on FreeBSD +* fixed types from tomlkit not being diffable +* fixed using Faults for user password salts +* fixed `bw repo create` clobbering existing repos + + # 4.4.2 2021-01-22 diff -Nru bundlewrap-4.4.2/debian/changelog bundlewrap-4.5.1/debian/changelog --- bundlewrap-4.4.2/debian/changelog 2021-02-03 13:18:14.000000000 +0000 +++ bundlewrap-4.5.1/debian/changelog 2021-02-19 11:44:26.000000000 +0000 @@ -1,3 +1,16 @@ +bundlewrap (4.5.1-1) unstable; urgency=medium + + * New upstream release + + -- Jonathan Carter Fri, 19 Feb 2021 13:44:26 +0200 + +bundlewrap (4.5.0-1) unstable; urgency=medium + + * New upstream release + * Update copyright years + + -- Jonathan Carter Fri, 19 Feb 2021 10:53:44 +0200 + bundlewrap (4.4.2-1) unstable; urgency=medium * New upstream release diff -Nru bundlewrap-4.4.2/debian/copyright bundlewrap-4.5.1/debian/copyright --- bundlewrap-4.4.2/debian/copyright 2021-02-03 13:14:49.000000000 +0000 +++ bundlewrap-4.5.1/debian/copyright 2021-02-19 08:54:14.000000000 +0000 @@ -3,7 +3,7 @@ Source: https://github.com/bundlewrap/bundlewrap Files: * -Copyright: 2016-2020 Torsten Rehn +Copyright: 2016-2021 Torsten Rehn Comment: Copyrights are assigned to Torsten Rehn (see: CAA.md) Additional author: Peter Hofmann Additional author: Tim Buchwaldt @@ -12,7 +12,7 @@ License: GPL-3 Files: debian/* -Copyright: 2016-2020 Jonathan Carter +Copyright: 2016-2021 Jonathan Carter License: GPL-3 License: GPL-3 diff -Nru bundlewrap-4.4.2/docs/content/guide/dev_item.md bundlewrap-4.5.1/docs/content/guide/dev_item.md --- bundlewrap-4.4.2/docs/content/guide/dev_item.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/guide/dev_item.md 2021-02-19 09:07:26.000000000 +0000 @@ -69,6 +69,15 @@ """ raise NotImplementedError + def display_on_create(self, cdict): + """ + Given a cdict as implemented above, modify it to better suit + interactive presentation when an item is created. + + Implementing this method is optional. + """ + return cdict + def display_dicts(self, cdict, sdict, keys): """ Given cdict and sdict as implemented above, modify them to @@ -79,6 +88,15 @@ """ return (cdict, sdict, keys) + def display_on_delete(self, sdict): + """ + Given an sdict as implemented above, modify it to better suit + interactive presentation when an item is deleted. + + Implementing this method is optional. + """ + return sdict + def fix(self, status): """ Do whatever is necessary to correct this item. The given ItemStatus diff -Nru bundlewrap-4.4.2/docs/content/items/action.md bundlewrap-4.5.1/docs/content/items/action.md --- bundlewrap-4.4.2/docs/content/items/action.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/action.md 2021-02-19 09:07:26.000000000 +0000 @@ -32,7 +32,7 @@ ## expected_return_code -Defaults to `0`. If the return code of your command is anything else, the action is considered failed. You can also set this to `None` and any return code will be accepted. +Defaults to `0`. If the return code of your command is anything else, the action is considered failed. You can also specify a list, set or tuple and the action is considered failed if the command's return code is not contained in that enumeration. You can also set this to `None` and any return code will be accepted.
diff -Nru bundlewrap-4.4.2/docs/content/items/pkg_freebsd.md bundlewrap-4.5.1/docs/content/items/pkg_freebsd.md --- bundlewrap-4.4.2/docs/content/items/pkg_freebsd.md 1970-01-01 00:00:00.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/pkg_freebsd.md 2021-02-19 09:07:26.000000000 +0000 @@ -0,0 +1,37 @@ +# FreeBSD package items + +Handles packages installed by `pkg` on FreeBSD systems. + + pkg_freebsd = { + "foo": { + "installed": True, # default + }, + "bar": { + "installed": True, + "version": "1.0", + }, + "baz": { + "installed": False, + }, + } + +

+ +# Attribute reference + +See also: [The list of generic builtin item attributes](../repo/items.py.md#builtin-item-attributes) + +
+ +## installed + +`True` when the package is expected to be present on the system; `False` if it should be purged. + +
+ + +## version + +Optional version string. Can be used to select one specific version of a package. + +Ignored when `installed` is `False`. diff -Nru bundlewrap-4.4.2/docs/content/items/svc_openbsd.md bundlewrap-4.5.1/docs/content/items/svc_openbsd.md --- bundlewrap-4.4.2/docs/content/items/svc_openbsd.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/svc_openbsd.md 2021-02-19 09:07:26.000000000 +0000 @@ -42,6 +42,13 @@
+## stop + +Stops the service. + +
+ ## stopstart Stops and starts the service. + diff -Nru bundlewrap-4.4.2/docs/content/items/svc_systemd.md bundlewrap-4.5.1/docs/content/items/svc_systemd.md --- bundlewrap-4.4.2/docs/content/items/svc_systemd.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/svc_systemd.md 2021-02-19 09:07:26.000000000 +0000 @@ -6,6 +6,7 @@ "fcron.service": { "enabled": True, # default "running": True, # default + "masked": False, # default }, "sgopherd.socket": { "running": False, @@ -32,6 +33,12 @@
+## masked + +`True` if the service is expected to be masked; `False` if it should be unmasked. `None` makes BundleWrap ignore this setting. + +
+ ## Canned actions See also: [Explanation of how canned actions work](../repo/items.py.md#canned-actions) @@ -45,3 +52,9 @@ ## restart Restarts the service. + +
+ +## stop + +Stops the service. diff -Nru bundlewrap-4.4.2/docs/content/items/svc_systemv.md bundlewrap-4.5.1/docs/content/items/svc_systemv.md --- bundlewrap-4.4.2/docs/content/items/svc_systemv.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/svc_systemv.md 2021-02-19 09:07:26.000000000 +0000 @@ -38,3 +38,9 @@ ## restart Restarts the service. + +
+ +## stop + +Stops the service. diff -Nru bundlewrap-4.4.2/docs/content/items/svc_upstart.md bundlewrap-4.5.1/docs/content/items/svc_upstart.md --- bundlewrap-4.4.2/docs/content/items/svc_upstart.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/items/svc_upstart.md 2021-02-19 09:07:26.000000000 +0000 @@ -41,6 +41,12 @@
+## stop + +Stops the service. + +
+ ## stopstart Stops and then starts the service. This is different from `restart` in that Upstart will pick up changes to the `/etc/init/SERVICENAME.conf` file, while `restart` will continue to use the version of that file that the service was originally started with. See [http://askubuntu.com/a/238069](http://askubuntu.com/a/238069). diff -Nru bundlewrap-4.4.2/docs/content/repo/items.py.md bundlewrap-4.5.1/docs/content/repo/items.py.md --- bundlewrap-4.4.2/docs/content/repo/items.py.md 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/content/repo/items.py.md 2021-02-19 09:07:26.000000000 +0000 @@ -53,6 +53,7 @@ postgres_dbpostgres_dbsManages Postgres databases postgres_rolepostgres_rolesManages Postgres roles pkg_pippkg_pipInstalls and removes Python packages with pip +pkg_freebsdpkg_freebsdInstalls and removes FreeBSD packages with pkg pkg_openbsdpkg_openbsdInstalls and removes OpenBSD packages with pkg_add/pkg_delete svc_openbsdsvc_openbsdStarts and stops services with OpenBSD's rc svc_systemdsvc_systemdStarts and stops services with systemd diff -Nru bundlewrap-4.4.2/docs/mkdocs.yml bundlewrap-4.5.1/docs/mkdocs.yml --- bundlewrap-4.4.2/docs/mkdocs.yml 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/docs/mkdocs.yml 2021-02-19 09:07:26.000000000 +0000 @@ -42,6 +42,7 @@ - k8s_*: items/k8s.md - pkg_apt: items/pkg_apt.md - pkg_dnf: items/pkg_dnf.md + - pkg_freebsd: items/pkg_freebsd.md - pkg_openbsd: items/pkg_openbsd.md - pkg_opkg: items/pkg_opkg.md - pkg_pacman: items/pkg_pacman.md diff -Nru bundlewrap-4.4.2/setup.py bundlewrap-4.5.1/setup.py --- bundlewrap-4.4.2/setup.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/setup.py 2021-02-19 09:07:26.000000000 +0000 @@ -3,7 +3,7 @@ setup( name="bundlewrap", - version="4.4.2", + version="4.5.1", description="Config management with Python", long_description=( "By allowing for easy and low-overhead config management, BundleWrap fills the gap between complex deployments using Chef or Puppet and old school system administration over SSH.\n" diff -Nru bundlewrap-4.4.2/tests/integration/bw_apply_actions.py bundlewrap-4.5.1/tests/integration/bw_apply_actions.py --- bundlewrap-4.4.2/tests/integration/bw_apply_actions.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/tests/integration/bw_apply_actions.py 2021-02-19 09:07:26.000000000 +0000 @@ -99,3 +99,40 @@ }, ) run("bw apply localhost", path=str(tmpdir)) + + +def test_action_return_codes(tmpdir): + make_repo( + tmpdir, + bundles={ + "test": { + 'items': { + 'actions': { + "single-code": { + 'command': "true", + 'expected_return_code': 0, + }, + "multi-code-list": { + 'command': "false", + 'expected_return_code': [1], + }, + "multi-code-tuple": { + 'command': "false", + 'expected_return_code': (1,), + }, + "multi-code-set": { + 'command': "false", + 'expected_return_code': {1}, + } + }, + }, + }, + }, + nodes={ + "localhost": { + 'bundles': ["test"], + 'os': host_os(), + }, + }, + ) + run("bw apply localhost", path=str(tmpdir)) diff -Nru bundlewrap-4.4.2/tests/integration/bw_apply_files.py bundlewrap-4.5.1/tests/integration/bw_apply_files.py --- bundlewrap-4.4.2/tests/integration/bw_apply_files.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/tests/integration/bw_apply_files.py 2021-02-19 09:07:26.000000000 +0000 @@ -256,5 +256,5 @@ """) stdout, stderr, rcode = run("bw apply localhost", path=str(tmpdir)) assert rcode == 0 - assert b"file:/tmp/bw_test_faultunavailable skipped (Fault unavailable)" in stdout + assert b"file:/tmp/bw_test_faultunavailable skipped (Fault unavailable)" in stdout assert not exists("/tmp/bw_test_faultunavailable") diff -Nru bundlewrap-4.4.2/tests/unit/pkg_freebsd.py bundlewrap-4.5.1/tests/unit/pkg_freebsd.py --- bundlewrap-4.4.2/tests/unit/pkg_freebsd.py 1970-01-01 00:00:00.000000000 +0000 +++ bundlewrap-4.5.1/tests/unit/pkg_freebsd.py 2021-02-19 09:07:26.000000000 +0000 @@ -0,0 +1,25 @@ +from bundlewrap.items.pkg_freebsd import parse_pkg_name +from pytest import raises + + +def test_not_found(): + found, version = parse_pkg_name("tree", "zsh-5.8") + assert found is False + + +def test_version(): + found, version = parse_pkg_name("tree", "tree-1.8.0") + assert found is True + assert version == "1.8.0" + + +def test_version_with_epoch(): + found, version = parse_pkg_name( + "zsh-syntax-highlighting", "zsh-syntax-highlighting-0.7.1,1") + assert found is True + assert version == "0.7.1,1" + + +def test_illegal_no_version(): + with raises(AssertionError): + parse_pkg_name("tree", "tree") diff -Nru bundlewrap-4.4.2/tests/unit/pkg_openbsd.py bundlewrap-4.5.1/tests/unit/pkg_openbsd.py --- bundlewrap-4.4.2/tests/unit/pkg_openbsd.py 2021-01-22 16:01:21.000000000 +0000 +++ bundlewrap-4.5.1/tests/unit/pkg_openbsd.py 2021-02-19 09:07:26.000000000 +0000 @@ -1,4 +1,5 @@ from bundlewrap.items.pkg_openbsd import parse_pkg_name + from pytest import raises