[Python-apps-commits] r14175 - in packages/bundlewrap/trunk (26 files)

highvoltage-guest at users.alioth.debian.org highvoltage-guest at users.alioth.debian.org
Thu Jul 6 18:52:09 UTC 2017


    Date: Thursday, July 6, 2017 @ 18:52:08
  Author: highvoltage-guest
Revision: 14175

New upstream version 2.9.0

Modified:
  packages/bundlewrap/trunk/CHANGELOG.md
  packages/bundlewrap/trunk/bundlewrap/__init__.py
  packages/bundlewrap/trunk/bundlewrap/cmdline/apply.py
  packages/bundlewrap/trunk/bundlewrap/cmdline/lock.py
  packages/bundlewrap/trunk/bundlewrap/cmdline/metadata.py
  packages/bundlewrap/trunk/bundlewrap/cmdline/parser.py
  packages/bundlewrap/trunk/bundlewrap/cmdline/run.py
  packages/bundlewrap/trunk/bundlewrap/concurrency.py
  packages/bundlewrap/trunk/bundlewrap/deps.py
  packages/bundlewrap/trunk/bundlewrap/items/actions.py
  packages/bundlewrap/trunk/bundlewrap/lock.py
  packages/bundlewrap/trunk/bundlewrap/node.py
  packages/bundlewrap/trunk/bundlewrap/operations.py
  packages/bundlewrap/trunk/bundlewrap/repo.py
  packages/bundlewrap/trunk/bundlewrap/utils/__init__.py
  packages/bundlewrap/trunk/debian/bw.1
  packages/bundlewrap/trunk/debian/changelog
  packages/bundlewrap/trunk/debian/copyright
  packages/bundlewrap/trunk/docs/content/guide/cli.md
  packages/bundlewrap/trunk/docs/content/guide/quickstart.md
  packages/bundlewrap/trunk/docs/content/items/action.md
  packages/bundlewrap/trunk/docs/content/repo/hooks.md
  packages/bundlewrap/trunk/docs/content/repo/nodes.py.md
  packages/bundlewrap/trunk/setup.py
  packages/bundlewrap/trunk/tests/integration/bw_apply_actions.py
  packages/bundlewrap/trunk/tests/integration/bw_metadata.py

Modified: packages/bundlewrap/trunk/CHANGELOG.md
===================================================================
--- packages/bundlewrap/trunk/CHANGELOG.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/CHANGELOG.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,3 +1,14 @@
+# 2.19.0
+
+2017-07-05
+
+* actions can now receive data over stdin
+* added `Node.magic_number`
+* added `bw apply --resume-file`
+* added hooks for `bw lock`
+* added `bw metadata --table`
+
+
 # 2.18.1
 
 2017-06-01

Modified: packages/bundlewrap/trunk/bundlewrap/__init__.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/__init__.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/__init__.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-VERSION = (2, 18, 1)
+VERSION = (2, 19, 0)
 VERSION_STRING = ".".join([str(v) for v in VERSION])

Modified: packages/bundlewrap/trunk/bundlewrap/cmdline/apply.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/cmdline/apply.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/cmdline/apply.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -6,6 +6,7 @@
 
 from ..concurrency import WorkerPool
 from ..exceptions import ItemDependencyLoop
+from ..utils import SkipList
 from ..utils.cmdline import get_target_nodes
 from ..utils.plot import explain_item_dependency_loop
 from ..utils.table import ROW_SEPARATOR, render_table
@@ -39,6 +40,7 @@
 
     start_time = datetime.now()
     results = []
+    skip_list = SkipList(args['resume_file'])
 
     def tasks_available():
         return bool(pending_nodes)
@@ -52,14 +54,16 @@
                 'autoskip_selector': args['autoskip'],
                 'force': args['force'],
                 'interactive': args['interactive'],
+                'skip_list': skip_list,
                 'workers': args['item_workers'],
                 'profiling': args['profiling'],
             },
         }
 
     def handle_result(task_id, return_value, duration):
-        if return_value is None:  # node skipped because it had no items
+        if return_value is None:  # node skipped
             return
+        skip_list.add(task_id)
         results.append(return_value)
         if args['profiling']:
             total_time = 0.0
@@ -95,6 +99,7 @@
         next_task,
         handle_result=handle_result,
         handle_exception=handle_exception,
+        cleanup=skip_list.dump,
         pool_id="apply",
         workers=args['node_workers'],
     )

Modified: packages/bundlewrap/trunk/bundlewrap/cmdline/lock.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/cmdline/lock.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/cmdline/lock.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -19,7 +19,7 @@
         if list(node.items):
             _targets.append(node)
         else:
-            io.stdout(_("{x} {node}  has no items").format(node=bold(node.name), x=yellow("!")))
+            io.stdout(_("{x} {node}  has no items").format(node=bold(node.name), x=yellow("»")))
     return _targets
 
 
@@ -157,6 +157,7 @@
 
     def handle_result(task_id, return_value, duration):
         locks_on_node[task_id] = return_value
+        repo.hooks.lock_show(repo, repo.get_node(task_id), return_value)
 
     def handle_exception(task_id, exception, traceback):
         msg = "{}: {}".format(task_id, exception)

Modified: packages/bundlewrap/trunk/bundlewrap/cmdline/metadata.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/cmdline/metadata.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/cmdline/metadata.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,20 +1,47 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+from decimal import Decimal
 from json import dumps
 
 from ..metadata import MetadataJSONEncoder, value_at_key_path
-from ..utils.cmdline import get_node
-from ..utils.text import force_text
-from ..utils.ui import io
+from ..utils import Fault
+from ..utils.cmdline import get_node, get_target_nodes
+from ..utils.table import ROW_SEPARATOR, render_table
+from ..utils.text import bold, force_text, mark_for_translation as _, red
+from ..utils.ui import io, page_lines
 
 
 def bw_metadata(repo, args):
-    node = get_node(repo, args['node'], adhoc_nodes=args['adhoc_nodes'])
-    for line in dumps(
-        value_at_key_path(node.metadata, args['keys']),
-        cls=MetadataJSONEncoder,
-        indent=4,
-        sort_keys=True,
-    ).splitlines():
-        io.stdout(force_text(line))
+    if args['table']:
+        if not args['keys']:
+            io.stdout(_("{x} at least one key is required with --table").format(x=red("!!!")))
+            exit(1)
+        target_nodes = get_target_nodes(repo, args['target'], adhoc_nodes=args['adhoc_nodes'])
+        key_paths = [path.strip().split(" ") for path in " ".join(args['keys']).split(",")]
+        table = [[bold(_("node"))] + [bold(" ".join(path)) for path in key_paths], ROW_SEPARATOR]
+        for node in target_nodes:
+            values = []
+            for key_path in key_paths:
+                try:
+                    value = value_at_key_path(node.metadata, key_path)
+                except KeyError:
+                    value = red(_("<missing>"))
+                if isinstance(value, (dict, list, tuple)):
+                    value = ", ".join([str(item) for item in value])
+                elif isinstance(value, set):
+                    value = ", ".join(sorted(value))
+                elif isinstance(value, (bool, float, int, Decimal, Fault)):
+                    value = str(value)
+                values.append(value)
+            table.append([bold(node.name)] + values)
+        page_lines(render_table(table))
+    else:
+        node = get_node(repo, args['target'], adhoc_nodes=args['adhoc_nodes'])
+        for line in dumps(
+            value_at_key_path(node.metadata, args['keys']),
+            cls=MetadataJSONEncoder,
+            indent=4,
+            sort_keys=True,
+        ).splitlines():
+            io.stdout(force_text(line))

Modified: packages/bundlewrap/trunk/bundlewrap/cmdline/parser.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/cmdline/parser.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/cmdline/parser.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -151,6 +151,18 @@
         dest='summary',
         help=_("don't show stats summary"),
     )
+    parser_apply.add_argument(
+        "-r",
+        "--resume-file",
+        default=None,
+        dest='resume_file',
+        help=_(
+            "path to a file that a list of completed nodes will be added to; "
+            "if the file already exists, any nodes therein will be skipped"
+        ),
+        metavar=_("PATH"),
+        type=str,
+    )
 
     # bw debug
     help_debug = _("Start an interactive Python shell for this repository")
@@ -410,7 +422,7 @@
     )
     parser_metadata.set_defaults(func=bw_metadata)
     parser_metadata.add_argument(
-        'node',
+        'target',
         metavar=_("NODE"),
         type=str,
         help=_("node to print JSON-formatted metadata for"),
@@ -423,6 +435,17 @@
         type=str,
         help=_("print only partial metadata from the given space-separated key path"),
     )
+    parser_metadata.add_argument(
+        "-t",
+        "--table",
+        action='store_true',
+        dest='table',
+        help=_(
+            "show a table of selected metadata values from multiple nodes instead; "
+            "allows for multiple comma-separated paths in KEY; "
+            "allows for node selectors in NODE (e.g. 'NODE1,NODE2,GROUP1,bundle:BUNDLE1...')"
+        ),
+    )
 
     # bw nodes
     help_nodes = _("List all nodes in this repository")
@@ -724,6 +747,18 @@
                "(defaults to {})").format(bw_run_p_default),
         type=int,
     )
+    parser_run.add_argument(
+        "-r",
+        "--resume-file",
+        default=None,
+        dest='resume_file',
+        help=_(
+            "path to a file that a list of completed nodes will be added to; "
+            "if the file already exists, any nodes therein will be skipped"
+        ),
+        metavar=_("PATH"),
+        type=str,
+    )
 
     # bw stats
     help_stats = _("Show some statistics about your repository")

Modified: packages/bundlewrap/trunk/bundlewrap/cmdline/run.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/cmdline/run.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/cmdline/run.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -5,6 +5,7 @@
 
 from ..concurrency import WorkerPool
 from ..exceptions import NodeLockedException
+from ..utils import SkipList
 from ..utils.cmdline import get_target_nodes
 from ..utils.text import mark_for_translation as _
 from ..utils.text import bold, error_summary, green, red, yellow
@@ -12,11 +13,15 @@
 from ..utils.ui import io
 
 
-def run_on_node(node, command, may_fail, ignore_locks, log_output):
+def run_on_node(node, command, may_fail, ignore_locks, log_output, skip_list):
     if node.dummy:
-        io.stdout(_("{x}  {node} is a dummy node").format(node=bold(node.name), x=yellow("!")))
-        return
+        io.stdout(_("{x} {node}  is a dummy node").format(node=bold(node.name), x=yellow("»")))
+        return None
 
+    if node.name in skip_list:
+        io.stdout(_("{x} {node}  skipped by --resume-file").format(node=bold(node.name), x=yellow("»")))
+        return None
+
     node.repo.hooks.node_run_start(
         node.repo,
         node,
@@ -59,6 +64,7 @@
             node=bold(node.name),
             x=red("✘"),
         ))
+    return result.return_code
 
 
 def bw_run(repo, args):
@@ -74,6 +80,8 @@
     )
     start_time = datetime.now()
 
+    skip_list = SkipList(args['resume_file'])
+
     def tasks_available():
         return bool(pending_nodes)
 
@@ -88,9 +96,14 @@
                 args['may_fail'],
                 args['ignore_locks'],
                 True,
+                skip_list,
             ),
         }
 
+    def handle_result(task_id, return_value, duration):
+        if return_value == 0:
+            skip_list.add(task_id)
+
     def handle_exception(task_id, exception, traceback):
         if isinstance(exception, NodeLockedException):
             msg = _(
@@ -111,7 +124,9 @@
     worker_pool = WorkerPool(
         tasks_available,
         next_task,
+        handle_result=handle_result,
         handle_exception=handle_exception,
+        cleanup=skip_list.dump,
         pool_id="run",
         workers=args['node_workers'],
     )

Modified: packages/bundlewrap/trunk/bundlewrap/concurrency.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/concurrency.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/concurrency.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -23,6 +23,7 @@
         next_task,
         handle_result=None,
         handle_exception=None,
+        cleanup=None,
         pool_id=None,
         workers=4,
     ):
@@ -33,6 +34,7 @@
         self.next_task = next_task
         self.handle_result = handle_result
         self.handle_exception = handle_exception
+        self.cleanup = cleanup
 
         self.number_of_workers = workers
         self.idle_workers = set(range(self.number_of_workers))
@@ -169,6 +171,8 @@
             return processed_results
         finally:
             io.debug(_("shutting down worker pool {pool}").format(pool=self.pool_id))
+            if self.cleanup:
+                self.cleanup()
             self.executor.shutdown()
             io.debug(_("worker pool {pool} has been shut down").format(pool=self.pool_id))
 

Modified: packages/bundlewrap/trunk/bundlewrap/deps.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/deps.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/deps.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -212,7 +212,7 @@
 
             try:
                 target_item = items[target_item_id]
-            except NoSuchItem:
+            except KeyError:
                 raise BundleError(_(
                     "{item} in bundle '{bundle}' triggers unknown item '{target_item}'"
                 ).format(

Modified: packages/bundlewrap/trunk/bundlewrap/items/actions.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/items/actions.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/items/actions.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -5,6 +5,7 @@
 
 from bundlewrap.exceptions import ActionFailure, BundleError
 from bundlewrap.items import format_comment, Item
+from bundlewrap.utils import Fault
 from bundlewrap.utils.ui import io
 from bundlewrap.utils.text import mark_for_translation as _
 from bundlewrap.utils.text import blue, bold, wrap_question
@@ -17,6 +18,7 @@
     BUNDLE_ATTRIBUTE_NAME = 'actions'
     ITEM_ATTRIBUTES = {
         'command': None,
+        'data_stdin': None,
         'expected_stderr': None,
         'expected_stdout': None,
         'expected_return_code': 0,
@@ -68,7 +70,10 @@
                 ))
                 return (self.STATUS_SKIPPED, ["unless"])
 
-        question_body = self.attributes['command']
+        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)
 
@@ -128,6 +133,17 @@
         return status_code
 
     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}  running...").format(
             bundle=self.bundle.name,
             item=self.id,
@@ -135,6 +151,7 @@
         )):
             result = self.bundle.node.run(
                 self.attributes['command'],
+                data_stdin=data_stdin,
                 may_fail=True,
             )
 

Modified: packages/bundlewrap/trunk/bundlewrap/lock.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/lock.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/lock.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -162,6 +162,8 @@
         node.run("mkdir -p " + quote(SOFT_LOCK_PATH))
         node.upload(local_path, SOFT_LOCK_FILE.format(id=lock_id), mode='0644')
 
+    node.repo.hooks.lock_add(node.repo, node, lock_id, item_selectors, expiry_timestamp, comment)
+
     return lock_id
 
 
@@ -199,3 +201,4 @@
         node=node.name,
     ))
     node.run("rm {}".format(SOFT_LOCK_FILE.format(id=lock_id)))
+    node.repo.hooks.lock_remove(node.repo, node, lock_id)

Modified: packages/bundlewrap/trunk/bundlewrap/node.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/node.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/node.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -2,6 +2,7 @@
 from __future__ import unicode_literals
 
 from datetime import datetime, timedelta
+from hashlib import md5
 from os import environ
 from sys import exit
 from threading import Lock
@@ -526,6 +527,10 @@
                 for item in bundle.items:
                     yield item
 
+    @cached_property
+    def magic_number(self):
+        return int(md5(self.name.encode('UTF-8')).hexdigest(), 16)
+
     @property
     def _static_items(self):
         for bundle in self.bundles:
@@ -537,17 +542,22 @@
         autoskip_selector="",
         interactive=False,
         force=False,
+        skip_list=tuple(),
         workers=4,
         profiling=False,
     ):
         if not list(self.items):
-            io.stdout(_("{x} {node}  has no items").format(node=bold(self.name), x=yellow("!")))
+            io.stdout(_("{x} {node}  has no items").format(node=bold(self.name), x=yellow("»")))
             return None
 
         if self.covered_by_autoskip_selector(autoskip_selector):
-            io.debug(_("skipping {}, matches autoskip selector").format(self.name))
+            io.stdout(_("{x} {node}  skipped by --skip").format(node=bold(self.name), x=yellow("»")))
             return None
 
+        if self.name in skip_list:
+            io.stdout(_("{x} {node}  skipped by --resume-file").format(node=bold(self.name), x=yellow("»")))
+            return None
+
         start = datetime.now()
 
         io.stdout(_("{x} {node}  {started} at {time}").format(
@@ -660,7 +670,7 @@
         """
         return self.repo._metadata_for_node(self.name, partial=True)
 
-    def run(self, command, may_fail=False, log_output=False):
+    def run(self, command, data_stdin=None, may_fail=False, log_output=False):
         if log_output:
             def log_function(msg):
                 io.stdout("{x} {node}  {msg}".format(
@@ -696,6 +706,7 @@
             self.hostname,
             command,
             add_host_keys=add_host_keys,
+            data_stdin=data_stdin,
             ignore_failure=may_fail,
             log_function=log_function,
             wrapper_inner=self.cmd_wrapper_inner,

Modified: packages/bundlewrap/trunk/bundlewrap/operations.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/operations.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/operations.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -90,6 +90,7 @@
     hostname,
     command,
     add_host_keys=False,
+    data_stdin=None,
     ignore_failure=False,
     log_function=None,
     wrapper_inner="{}",
@@ -138,6 +139,9 @@
     )
     io._ssh_pids.append(ssh_process.pid)
 
+    if data_stdin is not None:
+        ssh_process.stdin.write(data_stdin)
+
     quit_event = Event()
     stdout_thread = Thread(
         args=(stdout_lb, stdout_fd_r, quit_event, True),

Modified: packages/bundlewrap/trunk/bundlewrap/repo.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/repo.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/repo.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -39,18 +39,21 @@
 FILENAME_REQUIREMENTS = "requirements.txt"
 
 HOOK_EVENTS = (
+    'action_run_end',
     'action_run_start',
-    'action_run_end',
+    'apply_end',
     'apply_start',
-    'apply_end',
+    'item_apply_end',
     'item_apply_start',
-    'item_apply_end',
+    'lock_add',
+    'lock_remove',
+    'lock_show',
+    'node_apply_end',
     'node_apply_start',
-    'node_apply_end',
+    'node_run_end',
     'node_run_start',
-    'node_run_end',
+    'run_end',
     'run_start',
-    'run_end',
     'test',
     'test_node',
 )

Modified: packages/bundlewrap/trunk/bundlewrap/utils/__init__.py
===================================================================
--- packages/bundlewrap/trunk/bundlewrap/utils/__init__.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/bundlewrap/utils/__init__.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -300,6 +300,31 @@
     return hasher.hexdigest()
 
 
+class SkipList(object):
+    """
+    Used to maintain a list of nodes that have already been visited.
+    """
+    def __init__(self, path):
+        self.path = path
+        if path and exists(path):
+            with open(path) as f:
+                self._list_items = set(f.read().strip().split("\n"))
+        else:
+            self._list_items = set()
+
+    def __contains__(self, item):
+        return item in self._list_items
+
+    def add(self, item):
+        if self.path:
+            self._list_items.add(item)
+
+    def dump(self):
+        if self.path:
+            with open(self.path, 'w') as f:
+                f.write("\n".join(sorted(self._list_items)) + "\n")
+
+
 @contextmanager
 def tempfile():
     handle, path = mkstemp()

Modified: packages/bundlewrap/trunk/debian/bw.1
===================================================================
--- packages/bundlewrap/trunk/debian/bw.1	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/debian/bw.1	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,30 +1,16 @@
-.\"                                      Hey, EMACS: -*- nroff -*-
 .\" (C) Copyright 2016 Jonathan Carter <jcarter at linux.com>,
-.\"
-.\" First parameter, NAME, should be all caps
-.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
-.\" other parameters are allowed: see man(7), man(1)
+
 .TH Bundlewrap 1 "September 16 2016"
-.\" Please adjust this date whenever revising the manpage.
-.\"
-.\" Some roff macros, for reference:
-.\" .nh        disable hyphenation
-.\" .hy        enable hyphenation
-.\" .ad l      left justify
-.\" .ad b      justify to both left and right margins
-.\" .nf        disable filling
-.\" .fi        enable filling
-.\" .br        insert line break
-.\" .sp <n>    insert n+1 empty lines
-.\" for manpage-specific macros, see man(7)
+
 .SH NAME
 bundlewrap \- Decentralized configuration management system with Python
 .SH SYNOPSIS
 .B bundlewrap
-.RI [ options ] " files" ...
+.RI [-h] [-a] [-A] [-d] [-r DIRECTORY] [--version]
 .br
-.B bar
-.RI [ options ] " files" ...
+.B {apply,debug,groups,hash,items,lock,metadata,nodes,plot,repo,run,stats,test,verify,zen}
+.RI  ...
+
 .SH DESCRIPTION
 BundleWrap fills the gap between complex deployments using Chef or
 Puppet and old school system administration over SSH. You do not need
@@ -38,30 +24,82 @@
 everything is configured the way it's supposed to be. You won't have to
 install anything on managed servers.
 
-.B bundlewrap
-and
-.B bar
-commands.
+.SH OPTIONAL ARGUMENTS
+
 .PP
-.\" TeX users may be more comfortable with the \fB<whatever>\fP and
-.\" \fI<whatever>\fP escape sequences to invode bold face and italics,
-.\" respectively.
-\fBbundlewrap\fP is a program that...
-.SH OPTIONS
-These programs follow the usual GNU command line syntax, with long
-options starting with two dashes (`-').
-A summary of options is included below.
-For a complete description, see the Info files.
-.TP
 .B \-h, \-\-help
-Show summary of options.
+show this help message and exit
 .TP
-.B \-v, \-\-version
-Show version of program.
-.SH SEE ALSO
-.BR bar (1),
-.BR baz (1).
-.br
-The programs are documented fully by
-.IR "The Rise and Fall of a Fooish Bar" ,
-available via the Info system.
+.B \-a, \-\-add-host-keys
+set StrictHostKeyChecking=no instead of yes for SSH
+.TP
+.B \-A, \-\-adhoc-nodes
+treat unknown node names as adhoc 'virtual' nodes that
+receive configuration only through groups whose
+member_patterns match the node name given on the
+command line (which also has to be a resolvable
+hostname)
+.TP
+.B \-d, \-\-debug
+print debugging info (implies -v)
+.TP
+.B \-r DIRECTORY, \-\-repo-path DIRECTORY
+Look for repository at this path (defaults to current
+working directory)
+.TP
+.B  \-\-version
+show program's version number and exit
+
+.SH SUBCOMMANDS:
+
+.PP
+use 'bw <subcommand> --help' for more info
+.TP
+.B apply
+Applies the configuration defined in your repository to your nodes
+.TP
+.B debug
+Start an interactive Python shell for this repository
+.TP
+.B groups
+Lists groups in this repository (deprecated, use `bw nodes -a`)
+.TP
+.B hash
+Shows a SHA1 hash that summarizes the entire configuration for this repo, node, group, or item.
+.TP
+.B items
+List and preview items for a specific node
+.TP
+.B lock
+Manage locks on nodes used to prevent collisions between BundleWrap users
+.TP
+.B metadata
+View a JSON representation of a node's metadata
+.TP
+.B nodes
+List all nodes in this repository
+.TP
+.B plot
+Generates DOT output that can be piped into `dot -Tsvg -ooutput.svg`. The resulting output.svg can be viewed using most browsers.
+.TP
+.B repo
+Various subcommands to manipulate your repository
+.TP
+.B run
+Run a one-off command on a number of nodes
+.TP
+.B stats
+Show some statistics about your repository
+.TP
+.B test
+Test your repository for consistency (you can use this with a CI tool like Jenkins)
+.TP
+.B verify
+Inspect the health or 'correctness' of a node without changing it
+
+.SH REPORTING BUGS
+Bundlewrap bug tracker: https://github.com/bundlewrap/bundlewrap/issues
+
+.SH AUTHORS
+This manual page was written by Jonathan Carter <jcarter at linux.com>
+Bundlewrap was written by Torsten Rehn <torsten at rehn.email>

Modified: packages/bundlewrap/trunk/debian/changelog
===================================================================
--- packages/bundlewrap/trunk/debian/changelog	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/debian/changelog	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,3 +1,11 @@
+bundlewrap (2.19.0-1) unstable; urgency=medium
+
+  * New upstream release
+  * Update copyright years
+  * Update man page
+
+ -- Jonathan Carter <jcarter at linux.com>  Thu, 06 Jul 2017 20:27:00 +0200
+
 bundlewrap (2.18.1-2) unstable; urgency=medium
 
   * Update standards-version to 4.0.0

Modified: packages/bundlewrap/trunk/debian/copyright
===================================================================
--- packages/bundlewrap/trunk/debian/copyright	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/debian/copyright	2017-07-06 18:52:08 UTC (rev 14175)
@@ -3,7 +3,7 @@
 Source: https://github.com/bundlewrap/bundlewrap
 
 Files: *
-Copyright: Torsten Rehn <torsten at rehn.email>
+Copyright: 2016-2017 Torsten Rehn <torsten at rehn.email>
 Comment: Copyrights are assigned to Torsten Rehn (see: CAA.md)
  Additional author: Peter Hofmann <scm at uninformativ.de>
  Additional author: Tim Buchwaldt <tim at buchwaldt.ws>
@@ -12,7 +12,7 @@
 License: GPL-3
 
 Files: debian/*
-Copyright: 2016 Jonathan Carter <jcarter at linux.com>
+Copyright: 2016-2017 Jonathan Carter <jcarter at linux.com>
 License: GPL-3
 
 License: GPL-3

Modified: packages/bundlewrap/trunk/docs/content/guide/cli.md
===================================================================
--- packages/bundlewrap/trunk/docs/content/guide/cli.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/docs/content/guide/cli.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -2,7 +2,7 @@
 
 The `bw` utility is BundleWrap's command line interface.
 
-<div class="alert">This page is not meant as a complete reference. It provides a starting point to explore the various subcommands. If you're looking for details, <code>--help</code> is your friend.</div>
+<div class="alert alert-info">This page is not meant as a complete reference. It provides a starting point to explore the various subcommands. If you're looking for details, <code>--help</code> is your friend.</div>
 
 ## bw apply
 

Modified: packages/bundlewrap/trunk/docs/content/guide/quickstart.md
===================================================================
--- packages/bundlewrap/trunk/docs/content/guide/quickstart.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/docs/content/guide/quickstart.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -30,11 +30,11 @@
 
 The contents should be fairly self-explanatory, but you can always check the [docs](../repo/layout.md) on these files if you want to go deeper.
 
-<div class="alert">It is highly recommended to use git or a similar tool to keep track of your repository. You may want to start doing that right away.</div>
+<div class="alert alert-info">It is highly recommended to use git or a similar tool to keep track of your repository. You may want to start doing that right away.</div>
 
 At this point you will want to edit `nodes.py` and maybe change "localhost" to the hostname of a system you have passwordless (including sudo) SSH access to.
 
-<div class="alert">BundleWrap will honor your <code>~/.ssh/config</code>, so if <code>ssh mynode.example.com sudo id</code> works without any password prompts in your terminal, you're good to go.</div>
+<div class="alert alert-info">BundleWrap will honor your <code>~/.ssh/config</code>, so if <code>ssh mynode.example.com sudo id</code> works without any password prompts in your terminal, you're good to go.</div>
 
 
 Run a command
@@ -44,7 +44,7 @@
 
 <pre><code class="nohighlight">bw -a run node-1 "uptime"</code></pre>
 
-<div class="alert">The <code>-a</code> switch tells bw to automatically trust unknown SSH host keys (when you're connecting to a new node). By default, only known host keys will be accepted.</div>
+<div class="alert alert-info">The <code>-a</code> switch tells bw to automatically trust unknown SSH host keys (when you're connecting to a new node). By default, only known host keys will be accepted.</div>
 
 You should see something like this:
 

Modified: packages/bundlewrap/trunk/docs/content/items/action.md
===================================================================
--- packages/bundlewrap/trunk/docs/content/items/action.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/docs/content/items/action.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -24,6 +24,12 @@
 
 <br>
 
+### data_stdin
+
+You can pipe data directly to the command running on the node. To do so, use this attribute. If it's a string or unicode object, it will always be encoded as UTF-8. Alternatively, you can use raw bytes.
+
+<br>
+
 ### 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.

Modified: packages/bundlewrap/trunk/docs/content/repo/hooks.md
===================================================================
--- packages/bundlewrap/trunk/docs/content/repo/hooks.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/docs/content/repo/hooks.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -14,7 +14,7 @@
     def node_apply_start(repo, node, interactive=False, **kwargs):
         post_message("Starting apply on {}, everything is gonna be OK!".format(node.name))
 
-<div class="alert">Always define your hooks with `**kwargs` so we can pass in more information in future updates without breaking your hook.</div>
+<div class="alert alert-warning">Always define your hooks with <code>**kwargs</code> so we can pass in more information in future updates without breaking your hook.</div>
 
 <br>
 
@@ -112,6 +112,48 @@
 
 ---
 
+**`lock_add(repo, node, lock_id, items, expiry, comment, **kwargs)`**
+
+Called each time a soft lock is added to a node.
+
+`repo` The current repository (instance of `bundlewrap.repo.Repository`).
+
+`node` The current node (instance of `bundlewrap.node.Node`).
+
+`lock_id` The random ID of the lock.
+
+`items` List of item selector strings.
+
+`expiry` UNIX timestamp of lock expiry time (int).
+
+`comment` As entered by user.
+
+---
+
+**`lock_remove(repo, node, lock_id, **kwargs)`**
+
+Called each time a soft lock is removed from a node.
+
+`repo` The current repository (instance of `bundlewrap.repo.Repository`).
+
+`node` The current node (instance of `bundlewrap.node.Node`).
+
+`lock_id` The random ID of the lock.
+
+---
+
+**`lock_show(repo, node, lock_info, **kwargs)`**
+
+Called each time `bw lock show` finds a lock on a node.
+
+`repo` The current repository (instance of `bundlewrap.repo.Repository`).
+
+`node` The current node (instance of `bundlewrap.node.Node`).
+
+`lock_info` A dict contain the lock details.
+
+---
+
 **`node_apply_start(repo, node, interactive=False, **kwargs)`**
 
 Called each time a `bw apply` command reaches a new node.

Modified: packages/bundlewrap/trunk/docs/content/repo/nodes.py.md
===================================================================
--- packages/bundlewrap/trunk/docs/content/repo/nodes.py.md	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/docs/content/repo/nodes.py.md	2017-07-06 18:52:08 UTC (rev 14175)
@@ -66,11 +66,21 @@
 
 A string used as a DNS name when connecting to this node. May also be an IP address.
 
-<div class="alert">The username and SSH private key for connecting to the node cannot be configured in BundleWrap. If you need to customize those, BundleWrap will honor your <code>~/.ssh/config</code>.</div>
+<div class="alert alert-info">The username and SSH private key for connecting to the node cannot be configured in BundleWrap. If you need to customize those, BundleWrap will honor your <code>~/.ssh/config</code>.</div>
 
 Cannot be set at group level.
 
 
+### magic_number
+
+A large number derived from the node's name. This number is very likely to be unique for your entire repository. You can, for example, use this number to easily "jitter" cronjobs:
+
+    '{} {} * * * root /my/script'.format(
+        node.magic_number % 60,
+        node.magic_number % 2 + 4,
+    )
+
+
 ### metadata
 
 This can be a dictionary of arbitrary data (some type restrictions apply). You can access it from your templates as `node.metadata`. Use this to attach custom data (such as a list of IP addresses that should be configured on the target node) to the node. Note that you can also define metadata at the [group level](groups.py.md#metadata), but node metadata has higher priority.

Modified: packages/bundlewrap/trunk/setup.py
===================================================================
--- packages/bundlewrap/trunk/setup.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/setup.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -16,7 +16,7 @@
 
 setup(
     name="bundlewrap",
-    version="2.18.1",
+    version="2.19.0",
     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"

Modified: packages/bundlewrap/trunk/tests/integration/bw_apply_actions.py
===================================================================
--- packages/bundlewrap/trunk/tests/integration/bw_apply_actions.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/tests/integration/bw_apply_actions.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -46,3 +46,51 @@
         },
     )
     run("bw apply localhost", path=str(tmpdir))
+
+
+def test_action_pipe_binary(tmpdir):
+    make_repo(
+        tmpdir,
+        bundles={
+            "test": {
+                'actions': {
+                    "pipe": {
+                        'command': "cat",
+                        'data_stdin': b"hello\000world",
+                        'expected_stdout': b"hello\000world",
+                    },
+                },
+            },
+        },
+        nodes={
+            "localhost": {
+                'bundles': ["test"],
+                'os': host_os(),
+            },
+        },
+    )
+    run("bw apply localhost", path=str(tmpdir))
+
+
+def test_action_pipe_utf8(tmpdir):
+    make_repo(
+        tmpdir,
+        bundles={
+            "test": {
+                'actions': {
+                    "pipe": {
+                        'command': "cat",
+                        'data_stdin': "hello 🐧\n",
+                        'expected_stdout': "hello 🐧\n",
+                    },
+                },
+            },
+        },
+        nodes={
+            "localhost": {
+                'bundles': ["test"],
+                'os': host_os(),
+            },
+        },
+    )
+    run("bw apply localhost", path=str(tmpdir))

Modified: packages/bundlewrap/trunk/tests/integration/bw_metadata.py
===================================================================
--- packages/bundlewrap/trunk/tests/integration/bw_metadata.py	2017-07-05 13:24:50 UTC (rev 14174)
+++ packages/bundlewrap/trunk/tests/integration/bw_metadata.py	2017-07-06 18:52:08 UTC (rev 14175)
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
 from json import loads
 from os.path import join
 
@@ -120,3 +123,54 @@
 """)
     stdout, stderr, rcode = run("bw metadata node1", path=str(tmpdir))
     assert rcode == 1
+
+
+def test_table(tmpdir):
+    make_repo(
+        tmpdir,
+        nodes={
+            "node1": {
+                'metadata': {
+                    "foo_dict": {
+                        "bar": "baz",
+                    },
+                    "foo_list": ["bar", 1],
+                    "foo_int": 47,
+                    "foo_umlaut": "föö",
+                },
+            },
+            "node2": {
+                'metadata': {
+                    "foo_dict": {
+                        "baz": "bar",
+                    },
+                    "foo_list": [],
+                    "foo_int": -3,
+                    "foo_umlaut": "füü",
+                },
+            },
+        },
+        groups={
+            "all": {
+                'members': ["node1", "node2"],
+            },
+        },
+    )
+    stdout, stderr, rcode = run("BW_TABLE_STYLE=grep bw metadata --table all foo_dict bar, foo_list, foo_int, foo_umlaut", path=str(tmpdir))
+    assert stdout.decode('utf-8') == """node\tfoo_dict bar\tfoo_list\tfoo_int\tfoo_umlaut
+node1\tbaz\tbar, 1\t47\tföö
+node2\t<missing>\t\t-3\tfüü
+"""
+    assert stderr == b""
+    assert rcode == 0
+
+
+def test_table_no_key(tmpdir):
+    make_repo(
+        tmpdir,
+        nodes={
+            "node1": {},
+        },
+    )
+    stdout, stderr, rcode = run("bw metadata --table node1", path=str(tmpdir))
+    assert rcode == 1




More information about the Python-apps-commits mailing list