Hello community,

here is the log from the commit of package crmsh for openSUSE:Factory checked 
in at 2017-12-14 11:03:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Thu Dec 14 11:03:28 2017 rev:135 rq:556723 version:4.0.0+git.1513179435.e1d17d7b

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2017-12-06 
08:58:47.340546096 +0100
+++ /work/SRC/openSUSE:Factory/.crmsh.new/crmsh.changes 2017-12-14 
11:03:29.958189541 +0100
@@ -1,0 +2,15 @@
+Wed Dec 13 16:15:40 UTC 2017 - kgronl...@suse.com
+
+- Update to version 4.0.0+git.1513179435.e1d17d7b:
+  * high: scripts: Fix Python 3 migration issues in health, check-uptime 
(bsc#1071519)
+
+-------------------------------------------------------------------
+Tue Dec 12 15:44:48 UTC 2017 - kgronl...@suse.com
+
+- Update to version 4.0.0+git.1513011384.5aebf8a4:
+  * high: parse: Support new alert syntax (#280) (bsc#1069129)
+  * high: parse: Support new container bundles (fate#323415)
+  * low: hb_report: return "" to avoid TypeError
+  * low: ui_configure: use filter_keys replace any_startswith
+
+-------------------------------------------------------------------

Old:
----
  crmsh-4.0.0+git.1512406036.adc26906.tar.bz2

New:
----
  crmsh-4.0.0+git.1513179435.e1d17d7b.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.LGgdfp/_old  2017-12-14 11:03:30.970140690 +0100
+++ /var/tmp/diff_new_pack.LGgdfp/_new  2017-12-14 11:03:30.970140690 +0100
@@ -36,7 +36,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0+
 Group:          %{pkg_group}
-Version:        4.0.0+git.1512406036.adc26906
+Version:        4.0.0+git.1513179435.e1d17d7b
 Release:        0
 Url:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.LGgdfp/_old  2017-12-14 11:03:31.006138952 +0100
+++ /var/tmp/diff_new_pack.LGgdfp/_new  2017-12-14 11:03:31.006138952 +0100
@@ -1,4 +1,4 @@
 <servicedata>
 <service name="tar_scm">
             <param name="url">git://github.com/ClusterLabs/crmsh.git</param>
-          <param 
name="changesrevision">adc269069314c9a6ad2b0b378745d9161604097a</param></service></servicedata>
\ No newline at end of file
+          <param 
name="changesrevision">e1d17d7b1a3b6b4a3ca0211d72d8102b150c0c65</param></service></servicedata>
\ No newline at end of file

++++++ crmsh-4.0.0+git.1512406036.adc26906.tar.bz2 -> 
crmsh-4.0.0+git.1513179435.e1d17d7b.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/cibconfig.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/cibconfig.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/cibconfig.py  2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/cibconfig.py  2017-12-13 
16:37:15.000000000 +0100
@@ -686,7 +686,7 @@
         'alerts': 'alert',
         }
 
-    idless = set(['operations', 'fencing-topology'])
+    idless = set(['operations', 'fencing-topology', 'network', 'docker', 
'rkt', 'storage'])
     isref = set(['resource_ref', 'obj_ref', 'crmsh-ref'])
 
     def needs_id(node):
@@ -966,6 +966,17 @@
         elif idref is not None:
             ret += "%s " % (nvpair_format("$id-ref", idref))
 
+        if node.tag in ["docker", "network"]:
+            for item in node.keys():
+                ret += "%s " % nvpair_format(item, node.get(item))
+        if node.tag == "primitive":
+            ret += node.get('id')
+        for _type in ["port-mapping", "storage-mapping"]:
+            for c in node.iterchildren(_type):
+                ret += "%s " % _type
+                for item in c.keys():
+                    ret += "%s " % nvpair_format(item, c.get(item))
+
         score = node.get("score")
         if score:
             ret += "%s: " % (clidisplay.score(score))
@@ -1630,6 +1641,29 @@
             child_rsc.repr_gv(sg_obj, from_grp=True)
 
 
+class CibBundle(CibObject):
+    '''
+    bundle type resource
+    '''
+    set_names = {
+        "instance_attributes": "params",
+        "meta_attributes": "meta",
+        "docker": "docker",
+        "network": "network",
+        "storage": "storage",
+        "primitive": "primitive",
+        "meta": "meta"
+    }
+
+    def _repr_cli_head(self, format_mode):
+        s = clidisplay.keyword(self.obj_type)
+        ident = clidisplay.ident(self.obj_id)
+        return "%s %s" % (s, ident)
+
+    def _repr_cli_child(self, c, format_mode):
+        return self._attr_set_str(c)
+
+
 def _check_if_constraint_ref_is_child(obj):
     """
     Used by check_sanity for constraints to verify
@@ -2104,6 +2138,17 @@
     def _repr_cli_child(self, c, format_mode):
         if c.tag in self.set_names:
             return self._attr_set_str(c)
+        elif c.tag == "select":
+            r = ["select"]
+            for sel in c.iterchildren():
+                if not sel.tag.startswith('select_'):
+                    continue
+                r.append(sel.tag.lstrip('select_'))
+                if sel.tag == 'select_attributes':
+                    r.append('{')
+                    r.extend(sel.xpath('attribute/@name'))
+                    r.append('}')
+            return ' '.join(r)
         elif c.tag == "recipient":
             r = ["to"]
             is_complex = self._is_complex()
@@ -2161,6 +2206,7 @@
     "clone": ("clone", CibContainer, "resources"),
     "master": ("ms", CibContainer, "resources"),
     "template": ("rsc_template", CibPrimitive, "resources"),
+    "bundle": ("bundle", CibBundle, "resources"),
     "rsc_location": ("location", CibLocation, "constraints"),
     "rsc_colocation": ("colocation", CibSimpleConstraint, "constraints"),
     "rsc_order": ("order", CibSimpleConstraint, "constraints"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/constants.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/constants.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/constants.py  2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/constants.py  2017-12-13 
16:37:15.000000000 +0100
@@ -83,6 +83,7 @@
     "group": "group",
     "clone": "clone",
     "master": "ms",
+    "bundle": "bundle",
     "rsc_location": "location",
     "rsc_colocation": "colocation",
     "rsc_order": "order",
@@ -223,6 +224,7 @@
     "clone-max", "clone-node-max", "notify", "globally-unique", "ordered", 
     "interleave", "master-max", "master-node-max", "description",
 )
+bundle_meta_attributes = common_meta_attributes
 alert_meta_attributes = (
     "timeout", "timestamp-format"
 )
@@ -338,4 +340,137 @@
                             "cluster-name")
 pcmk_version = ""  # set later
 
+container_type = ["docker", "rkt"]
+container_helptxt = {
+    "docker": {
+        "image": """image:(string)
+    Docker image tag(required)""",
+
+        "replicas": """replicas:(integer)
+    Default:Value of masters if that is positive, else 1
+    A positive integer specifying the number of container instances to 
launch""",
+
+        "replicas-per-host": """replicas-per-host:(integer)
+    Default:1
+    A positive integer specifying the number of container instances allowed to
+    run on a single node""",
+
+        "masters": """masters:(integer)
+    Default:0
+    A non-negative integer that, if positive, indicates that the containerized
+    service should be treated as a multistate service, with this many replicas
+    allowed to run the service in the master role""",
+
+        "run-command": """run-command:(string)
+    Default:/usr/sbin/pacemaker_remoted if bundle contains a primitive, 
otherwise none
+    This command will be run inside the container when launching it ("PID 1").
+    If the bundle contains a primitive, this command must start 
pacemaker_remoted
+    (but could, for example, be a script that does other stuff, too).""",
+
+        "options": """options:(string)
+    Extra command-line options to pass to docker run"""
+    },
+
+    "network": {
+        "ip-range-start": """ip-range-start:(IPv4 address)
+    If specified, Pacemaker will create an implicit ocf:heartbeat:IPaddr2 
resource
+    for each container instance, starting with this IP address, using up to 
replicas
+    sequential addresses. These addresses can be used from the host’s network 
to
+    reach the service inside the container, though it is not visible within the
+    container itself. Only IPv4 addresses are currently supported.""",
+
+        "host-netmask": """host-netmask:(integer)
+    Default:32
+    If ip-range-start is specified, the IP addresses are created with this CIDR
+    netmask (as a number of bits).""",
+
+        "host-interface": """host-interface:(string)
+    If ip-range-start is specified, the IP addresses are created on this host
+    interface (by default, it will be determined from the IP address).""",
+
+        "control-port": """control-port:(integer)
+    Default: 3121
+    If the bundle contains a primitive, the cluster will use this integer TCP 
port
+    for communication with Pacemaker Remote inside the container. Changing 
this is
+    useful when the container is unable to listen on the default port, for 
example,
+    when the container uses the host’s network rather than ip-range-start (in 
which
+    case replicas-per-host must be 1), or when the bundle may run on a 
Pacemaker
+    Remote node that is already listening on the default port. Any 
PCMK_remote_port
+    environment variable set on the host or in the container is ignored for 
bundle
+    connections.""",
+
+        "port-mapping": {
+            "id": """id:(string)
+    A unique name for the port mapping (required)""",
+
+            "port": """port:(integer)
+    If this is specified, connections to this TCP port number on the host 
network
+    (on the container’s assigned IP address, if ip-range-start is specified) 
will
+    be forwarded to the container network. Exactly one of port or range must be
+    specified in a port-mapping.""",
+
+            "internal-port": """internal-port:(integer)
+    Default: value of port
+    If port and this are specified, connections to port on the host’s network 
will
+    be forwarded to this port on the container network.""",
+
+                "range": """range:(first_port-last_port)
+    If this is specified, connections to these TCP port numbers (expressed as
+    first_port-last_port) on the host network (on the container’s assigned IP 
address,
+    if ip-range-start is specified) will be forwarded to the same ports in the 
container
+    network. Exactly one of port or range must be specified in a 
port-mapping."""
+        }
+    },
+
+    "storage": {
+        "id": """id:(string)
+    A unique name for the storage mapping (required)""",
+
+        "source-dir": """source-dir:(string)
+    The absolute path on the host’s filesystem that will be mapped into the 
container.
+    Exactly one of source-dir and source-dir-root must be specified in a 
storage-mapping.""",
+
+        "source-dir-root": """source-dir-root:(string)
+    The start of a path on the host’s filesystem that will be mapped into the 
container,
+    using a different subdirectory on the host for each container instance. 
The subdirectory
+    will be named the same as the bundle host name, as described in the note 
for ip-range-start.
+    Exactly one of source-dir and source-dir-root must be specified in a 
storage-mapping.""",
+
+           "target-dir": """target-dir:(string)
+    The path name within the container where the host storage will be mapped 
(required)""",
+
+            "options": """options:(string)
+    File system mount options to use when mapping the storage"""
+    },
+
+    "rkt": {
+        "image": """image:(string)
+    Container image tag (required)""",
+
+        "replicas": """replicas:(integer)
+    Default:Value of masters if that is positive, else 1
+    A positive integer specifying the number of container instances to 
launch""",
+
+        "replicas-per-host": """replicas-per-host:(interval)
+    Default:1
+    A positive integer specifying the number of container instances allowed to
+    run on a single node""",
+
+        "masters": """masters:(integer)
+    Default:0
+    A non-negative integer that, if positive, indicates that the containerized
+    service should be treated as a multistate service, with this many replicas
+    allowed to run the service in the master role""",
+
+        "run-command": """run-command:(string)
+    Default:/usr/sbin/pacemaker_remoted if bundle contains a primitive, 
otherwise none
+    This command will be run inside the container when launching it ("PID 1").
+    If the bundle contains a primitive, this command must start 
pacemaker_remoted
+    (but could, for example, be a script that does other stuff, too).""",
+
+        "options": """options:(string)
+    Extra command-line options to pass to rkt run"""
+    }
+}
+
 # vim:ts=4:sw=4:et:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/parse.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/parse.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/parse.py      2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/parse.py      2017-12-13 
16:37:15.000000000 +0100
@@ -615,6 +615,10 @@
         oplist = olist([op for op in name_map if op.lower() in ('operations', 
'op')])
         for op in oplist:
             del name_map[op]
+        bundle_list = olist([op for op in name_map if op.lower()
+                            in ('docker', 'rkt', 'network', 'port-mapping', 
'storage', 'primitive')])
+        for bl in bundle_list:
+            del name_map[bl]
         initial = True
         while self.has_tokens():
             t = self.current_token().lower()
@@ -622,7 +626,11 @@
                 initial = False
                 if t in oplist:
                     self.match_operations(out, t == 'operations')
+                if t in bundle_list:
+                    self.match_container(out, t)
                 else:
+                    if bundle_list:
+                        terminator = ['network', 'storage', 'primitive']
                     for attr_list in self.match_attr_lists(name_map, 
terminator=terminator):
                         out.append(attr_list)
             elif initial:
@@ -634,6 +642,35 @@
             else:
                 break
 
+    def match_container(self, out, _type):
+        container_node = None
+        self.match(_type)
+        all_attrs = self.match_nvpairs(minpairs=0, terminator=['network', 
'storage', 'meta', 'primitive'])
+
+        if _type != "primitive":
+            exist_node = out.find(_type)
+            if exist_node is None:
+                container_node = xmlutil.new(_type)
+            else:
+                container_node = exist_node
+
+            child_flag = False
+            for nvp in all_attrs:
+                if nvp.get('name') in ['port-mapping', 'storage-mapping']:
+                    inst_attrs = xmlutil.child(container_node, nvp.get('name'))
+                    child_flag = True
+                    continue
+                if child_flag:
+                    inst_attrs.set(nvp.get('name'), nvp.get('value'))
+                else:
+                    container_node.set(nvp.get('name'), nvp.get('value'))
+            out.append(container_node)
+
+        else:
+            if len(all_attrs) != 1 or all_attrs[0].get('value'):
+                self.err("Expected primitive reference, got {}".format(", 
".join("{}={}".format(nvp.get('name'), nvp.get('value') or "") for nvp in 
all_attrs)))
+            xmlutil.child(out, 'crmsh-ref', id=all_attrs[0].get('name'))
+
     def match_op(self, out, pfx='op'):
         """
         op <optype> [<n>=<v> ...]
@@ -733,7 +770,7 @@
     return out
 
 
-@parser_for('primitive', 'group', 'clone', 'ms', 'master', 'rsc_template')
+@parser_for('primitive', 'group', 'clone', 'ms', 'master', 'rsc_template', 
'bundle')
 class ResourceParser(BaseParser):
     def match_ra_type(self, out):
         "[<class>:[<provider>:]]<type>"
@@ -884,6 +921,19 @@
             xmlutil.child(out, 'crmsh-ref', id=child)
         return out
 
+    def parse_bundle(self):
+        out = xmlutil.new('bundle')
+        out.set('id', self.match_identifier())
+        xmlutil.maybe_set(out, 'description', self.try_match_description())
+        self.match_arguments(out, {'docker': 'docker',
+                                   'rkt': 'rkt',
+                                   'network': 'network',
+                                   'port-mapping': 'port-mapping',
+                                   'storage': 'storage',
+                                   'meta': 'meta_attributes',
+                                   'primitive': 'primitive'})
+        return out
+
 
 @parser_for('location', 'colocation', 'collocation', 'order', 'rsc_ticket')
 class ConstraintParser(BaseParser):
@@ -1474,11 +1524,35 @@
     if desc is not None:
         out.attrib['description'] = desc
     rcount = 1
+    root_selector = [None]
+
+    def wrap_select(tag):
+        if tag[0] is None:
+            tag[0] = xmlutil.child(out, 'select')
+        return tag[0]
+
     while self.has_tokens():
         if self.current_token() in ('attributes', 'meta'):
             self.match_arguments(out, {'attributes': 'instance_attributes',
                                        'meta': 'meta_attributes'},
-                                 terminator=['attributes', 'meta', 'to'])
+                                 terminator=['attributes', 'meta', 'to', 
'select'])
+            continue
+        if self.current_token() == 'select':
+            selector_types = ('nodes', 'fencing', 'resources', 'attributes')
+            self.match('select')
+            root_selector[0] = None
+            while self.current_token() in selector_types:
+                selector = self.match_identifier()
+                if selector == 'attributes':
+                    if not self.try_match('{'):
+                        self.rewind()
+                        break
+                seltag = xmlutil.child(wrap_select(root_selector), 
'select_{}'.format(selector))
+                if selector == 'attributes':
+                    while self.current_token() != '}':
+                        name = self.match_identifier()
+                        xmlutil.child(seltag, 'attribute', name=name)
+                    self.match('}')
             continue
         self.match('to')
         rid = '%s-recipient-%s' % (alertid, rcount)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/ui_configure.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/ui_configure.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/ui_configure.py       
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/ui_configure.py       
2017-12-13 16:37:15.000000000 +0100
@@ -55,6 +55,7 @@
 _top_rsc_id_list = compl.call(cib_factory.top_rsc_id_list)
 _node_id_list = compl.call(cib_factory.node_id_list)
 _rsc_template_list = compl.call(cib_factory.rsc_template_list)
+_container_type = compl.choice(constants.container_type)
 
 
 def _advanced_completer(args):
@@ -64,18 +65,19 @@
     key_words = ["meta", "params"]
     completing = args[-1]
     resource_type = args[0]
+    return_list = []
     if completing.endswith('='):
         # TODO add some help messages
         return []
     keyw = last_keyword(args, key_words)
     if keyw and keyw == "meta":
         if resource_type == "group":
-            return [s+'=' for s in constants.group_meta_attributes] + key_words
+            return_list = utils.filter_keys(constants.group_meta_attributes, 
args)
         if resource_type == "clone":
-            return [s+'=' for s in constants.clone_meta_attributes] + key_words
+            return_list = utils.filter_keys(constants.clone_meta_attributes, 
args)
         if resource_type in ["ms", "master"]:
-            return [s+'=' for s in constants.ms_meta_attributes] + key_words
-    return key_words
+            return_list = utils.filter_keys(constants.ms_meta_attributes, args)
+    return return_list + key_words
 
 
 def _list_resource(args):
@@ -200,8 +202,7 @@
         return []
     elif '=' in completing:
         return []
-    return [s+'=' for s in agent.params(completion=True)
-            if utils.any_startswith(args, s+'=') is None]
+    return utils.filter_keys(agent.params(completion=True), args)
 
 
 def _prim_meta_completer(agent, args):
@@ -210,8 +211,7 @@
         return ['meta']
     if '=' in completing:
         return []
-    return [s+'=' for s in constants.rsc_meta_attributes
-            if utils.any_startswith(args, s+'=') is None]
+    return utils.filter_keys(constants.rsc_meta_attributes, args)
 
 
 def _prim_op_completer(agent, args):
@@ -304,6 +304,149 @@
     return completers_set[last_keyw](agent, args) + keywords
 
 
+def container_helptxt(params, helptxt, topic):
+    for item in reversed(params):
+        if item in ["storage", "network", "docker", "rkt"]:
+            return helptxt[item][topic] + "\n"
+        if item == "port-mapping":
+            return helptxt["network"][item][topic] + "\n"
+
+
+def _container_remove_exist_keywords(args, _keywords):
+    for item in ["network", "primitive"]:
+        if item in args:
+            _keywords.remove(item)
+
+
+def _container_network_completer(args, _help, _keywords):
+    key_words = ["network", "port-mapping"]
+    completing = args[-1]
+    token = args[-2]
+    if completing.endswith("="):
+        return []
+    if completing in key_words:
+        return [completing]
+
+    tmp = list(_help["network"].keys())
+    # port-mapping is element, not a network option
+    tmp.remove("port-mapping")
+    network_keys = utils.filter_keys(tmp, args)
+    # bundle contain just one <network>/<primitive> element
+    _container_remove_exist_keywords(args, _keywords)
+
+    last_keyw = last_keyword(args, key_words)
+    if last_keyw == "network":
+        if token == "network":
+            return network_keys
+        else:
+            # complete port-mapping or other parts
+            return network_keys + ["port-mapping"] + _keywords
+
+    if last_keyw == "port-mapping":
+        mapping_required = ["id"]
+        mapping_params = args[utils.rindex(args, "port-mapping"):]
+        mapping_keys = 
utils.filter_keys(_help["network"]["port-mapping"].keys(), mapping_params)
+        if token == "port-mapping":
+            return mapping_keys
+        # required options must be completed
+        for s in mapping_required:
+            if utils.any_startswith(mapping_params, s+'=') is None:
+                return mapping_keys
+        # complete port-mapping or other parts
+        return mapping_keys + ["port-mapping"] + _keywords
+
+
+def _container_storage_completer(args, _help, _keywords):
+    completing = args[-1]
+    if completing.endswith("="):
+        return []
+    if completing == "storage":
+        return [completing]
+    if args[-2] == "storage":
+        return ["storage-mapping"]
+
+    storage_required = ["id", "target-dir"]
+    # get last storage part
+    mapping_params = args[utils.rindex(args, "storage-mapping"):]
+    storage_keys = utils.filter_keys(_help["storage"].keys(), mapping_params)
+
+    # required options must be completed
+    for s in storage_required:
+        if utils.any_startswith(mapping_params, s+"=") is None:
+            return storage_keys
+    # bundle contain just one <network>/<primitive> element
+    _container_remove_exist_keywords(args, _keywords)
+    # complete storage or other parts
+    return storage_keys + _keywords
+
+
+def _container_primitive_completer(args, _help, _keywords):
+    completing = args[-1]
+    if completing == "primitive":
+        return [completing]
+
+    _id_list = cib_factory.f_prim_free_id_list()
+    if _id_list is None:
+        return []
+    # bundle contain just one <network>/<primitive> element
+    _container_remove_exist_keywords(args, _keywords)
+    if args[-3] == "primitive" and args[-2] in _id_list:
+        return _keywords
+    return _id_list
+
+
+def _container_meta_completer(args, helptxt, _keywords):
+    completing = args[-1]
+    if completing.endswith("="):
+        return []
+    if completing == "meta":
+        return [completing]
+
+    # bundle contain just one <network>/<primitive> element
+    _container_remove_exist_keywords(args, _keywords)
+
+    return utils.filter_keys(constants.bundle_meta_attributes, args) + 
_keywords
+
+
+def container_complete_complex(args):
+    '''
+    Complete five parts:
+    container options, network, storage, primitive and meta
+    '''
+    container_options_required = ["image"]
+    completing = args[-1]
+    container_type = args[2]
+
+    completers_set = {
+        "network": _container_network_completer,
+        "storage": _container_storage_completer,
+        "primitive": _container_primitive_completer,
+        "meta": _container_meta_completer
+    }
+    keywords = list(completers_set.keys())
+    last_keyw = last_keyword(args, keywords)
+
+    # to show help messages
+    if completing.endswith('='):
+        if len(completing) > 1 and options.interactive:
+            topic = completing[:-1]
+            CompletionHelp.help(topic, container_helptxt(args, 
constants.container_helptxt, topic))
+        return []
+
+    container_options = 
utils.filter_keys(constants.container_helptxt[container_type].keys(), args)
+
+    # required options must be completed
+    for s in container_options_required:
+        if utils.any_startswith(args, s+'=') is None:
+            return container_options
+
+    if last_keyw is None:
+        return container_options + keywords
+
+    # to complete network, storage, primitive and meta
+    return completers_set[last_keyw](args, constants.container_helptxt, 
keywords)
+
+
 class CibConfig(command.UI):
     '''
     The configuration class
@@ -761,6 +904,14 @@
                 tmp.insert(idx+1, "role=%s" % item.split('_')[1])
         return self.__conf_object(context.get_command_name(), *tuple(tmp))
 
+    @command.completers_repeating(compl.attr_id, _container_type, 
container_complete_complex)
+    def do_bundle(self, context, *args):
+        """usage: bundle <bundle id> <container type> [<container option>...]
+        network [<network option>...]
+        storage [<storage option>...]
+        primitive <resource id> {[<class>:[<provider>:]]<type>|@<template>}"""
+        return self.__conf_object(context.get_command_name(), *args)
+
     @command.skill_level('administrator')
     @command.completers_repeating(compl.attr_id, _f_prim_free_id_list, 
_advanced_completer)
     def do_group(self, context, *args):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/utils.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/utils.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/crmsh/utils.py      2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/crmsh/utils.py      2017-12-13 
16:37:15.000000000 +0100
@@ -42,6 +42,11 @@
         return s
 
 
+def filter_keys(key_list, args, sign="="):
+    """Return list item which not be completed yet"""
+    return [s+sign for s in key_list if any_startswith(args, s+sign) is None]
+
+
 def any_startswith(iterable, prefix):
     """Return first element in iterable which startswith prefix, or None."""
     for element in iterable:
@@ -50,6 +55,10 @@
     return None
 
 
+def rindex(iterable, value):
+    return len(iterable) - iterable[::-1].index(value) - 1
+
+
 def memoize(function):
     "Decorator to invoke a function once only for any argument"
     memoized = {}
@@ -1676,14 +1685,8 @@
         if rc == 0:
             return [x for x in [getname(line.split()) for line in outp] if x 
and x != '(null)']
 
-        CIB_DIR = config.path.crm_config
-        cib_file = r"%s/%s" % (CIB_DIR, "cib.xml")
-        if not os.path.isfile(cib_file):
-            raise ValueError("Error listing cluster nodes: cib.xml not exists")
-
         from . import xmlutil
         node_list = []
-        os.environ['CIB_file'] = cib_file
         cib = xmlutil.cibdump2elem()
         if cib is None:
             return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.0.0+git.1512406036.adc26906/data-manifest 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/data-manifest
--- old/crmsh-4.0.0+git.1512406036.adc26906/data-manifest       2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/data-manifest       2017-12-13 
16:37:15.000000000 +0100
@@ -77,6 +77,8 @@
 test/testcases/basicset
 test/testcases/bugs
 test/testcases/bugs.exp
+test/testcases/bundle
+test/testcases/bundle.exp
 test/testcases/commit
 test/testcases/commit.exp
 test/testcases/common.excl
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.0.0+git.1512406036.adc26906/doc/crm.8.adoc 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/doc/crm.8.adoc
--- old/crmsh-4.0.0+git.1512406036.adc26906/doc/crm.8.adoc      2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/doc/crm.8.adoc      2017-12-13 
16:37:15.000000000 +0100
@@ -2998,6 +2998,7 @@
 alert <id> <path> \
   [attributes <nvpair> ...] \
   [meta <nvpair> ...] \
+  [select [nodes | fencing | resources | attributes '{' <attribute> ... '}' ] 
...] \
   [to [{] <recipient>
     [attributes <nvpair> ...] \
     [meta <nvpair> ...] [}] \
@@ -3012,6 +3013,42 @@
 alert alert-2 /srv/pacemaker/example_alert.sh \
     meta timeout=60s \
     to { /var/log/cluster-alerts.log }
+
+alert alert-3 /srv/pacemaker/example_alert.sh \
+    select fencing \
+    to { /var/log/fencing-alerts.log }
+
+...............
+
+[[cmdhelp_configure_bundle,Container bundle]]
+==== `bundle`
+
+A bundle is a single resource specifying the settings, networking
+requirements, and storage requirements for any number of containers
+generated from the same container image.
+
+Pacemaker bundles support Docker (since version 1.1.17) and rkt (since
+version 1.1.18) container technologies.
+
+A bundle must contain exactly one +docker+ or +rkt+ element.
+
+The bundle definition may contain a reference to a primitive
+resource which defining the resource running inside the
+container.
+
+Example:
+...............
+
+primitive httpd-apache ocf:heartbeat:apache
+
+bundle httpd \
+    docker image=pcmk:httpd replicas=3 \
+    network ip-range-start=10.10.10.123 host-netmask=24 \
+    port-mapping port=80 \
+    storage \
+        storage-mapping target-dir=/var/www/html source-dir=/srv/www 
options=rw \
+    primitive httpd-apache
+
 ...............
 
 [[cmdhelp_configure_cib,CIB shadow management]]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/doc/website-v1/scripts.adoc 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/doc/website-v1/scripts.adoc
--- old/crmsh-4.0.0+git.1512406036.adc26906/doc/website-v1/scripts.adoc 
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/doc/website-v1/scripts.adoc 
2017-12-13 16:37:15.000000000 +0100
@@ -477,30 +477,30 @@
 
 [source,python]
 ----
-#!/usr/bin/env python
+#!/usr/bin/python3
 import crm_script as crm
 try:
     uptime = open('/proc/uptime').read().split()[0]
     crm.exit_ok(uptime)
-except:
-    crm.exit_fail("Couldn't open /proc/uptime")
+except Exception as e:
+    crm.exit_fail("Couldn't open /proc/uptime: %s" % (e))
 ----
 
 `report.py`:
 
 [source,python]
 ----
-#!/usr/bin/env python
+#!/usr/bin/python3
 import crm_script as crm
 show_all = crm.is_true(crm.param('show_all'))
-uptimes = crm.output(1).items()
-max_uptime = 0, ''
+uptimes = list(crm.output(1).items())
+max_uptime = '', 0
 for host, uptime in uptimes:
-    if uptime > max_uptime[0]:
-        max_uptime = uptime, host
+    if float(uptime) > max_uptime[1]:
+        max_uptime = host, float(uptime)
 if show_all:
-    print "Uptimes: %s" % (', '.join("%s: %s" % v for v in uptimes))
-print "Longest uptime is %s seconds on host %s" % max_uptime
+    print("Uptimes: %s" % (', '.join("%s: %s" % v for v in uptimes)))
+print("Longest uptime is %s seconds on host %s" % (max_uptime[1], 
max_uptime[0]))
 ----
 
 See below for more details on the helper library `crm_script`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/hb_report/utillib.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/hb_report/utillib.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/hb_report/utillib.py        
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/hb_report/utillib.py        
2017-12-13 16:37:15.000000000 +0100
@@ -1353,14 +1353,14 @@
 
     if not FROM_LINE:
         log_warning("couldn't find line for time %d; corrupt log file?" % 
from_time)
-        return
+        return ""
 
     TO_LINE = ""
     if to_time != 0:
         TO_LINE = findln_by_time(sourcef, to_time)
         if not TO_LINE:
             log_warning("couldn't find line for time %d; corrupt log file?" % 
to_time)
-            return
+            return ""
 
     log_debug("including segment [%s-%s] from %s" % (FROM_LINE, TO_LINE, 
sourcef))
     return dump_log(sourcef, FROM_LINE, TO_LINE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/scripts/check-uptime/fetch.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/check-uptime/fetch.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/scripts/check-uptime/fetch.py       
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/check-uptime/fetch.py       
2017-12-13 16:37:15.000000000 +0100
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-from __future__ import unicode_literals
+#!/usr/bin/python3
 import crm_script
 try:
     uptime = open('/proc/uptime').read().split()[0]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/scripts/check-uptime/report.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/check-uptime/report.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/scripts/check-uptime/report.py      
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/check-uptime/report.py      
2017-12-13 16:37:15.000000000 +0100
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-from __future__ import print_function
-from __future__ import unicode_literals
+#!/usr/bin/python3
 import crm_script
 show_all = crm_script.is_true(crm_script.param('show_all'))
 uptimes = list(crm_script.output(1).items())
-max_uptime = '', 0
+max_uptime = '', 0.0
 for host, uptime in uptimes:
-    if uptime > max_uptime[1]:
-        max_uptime = host, uptime
+    if float(uptime) > max_uptime[1]:
+        max_uptime = host, float(uptime)
 if show_all:
     print("Uptimes: %s" % (', '.join("%s: %s" % v for v in uptimes)))
 print("Longest uptime is %s seconds on host %s" % (max_uptime[1], 
max_uptime[0]))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/collect.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/collect.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/collect.py   
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/collect.py   
2017-12-13 16:37:15.000000000 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/python3
 from __future__ import unicode_literals
 from builtins import str
 import os
@@ -13,21 +13,25 @@
             'haproxy', 'hawk', 'libdlm', 'libqb', 'ocfs2', 'ocfs2-tools',
             'pacemaker', 'pacemaker-mgmt', 'resource-agents', 'sbd']
 
+
 def rpm_info():
     return crm_script.rpmcheck(PACKAGES)
 
+
 def logrotate_info():
     return {}
 
+
 def get_user():
     return pwd.getpwuid(os.getuid()).pw_name
 
+
 def sys_info():
     sysname, nodename, release, version, machine = os.uname()
-    #The first three columns measure CPU and IO utilization of the
-    #last one, five, and 15 minute periods. The fourth column shows
-    #the number of currently running processes and the total number of
-    #processes. The last column displays the last process ID used.
+    # The first three columns measure CPU and IO utilization of the
+    # last one, five, and 15 minute periods. The fourth column shows
+    # the number of currently running processes and the total number of
+    # processes. The last column displays the last process ID used.
     system, node, release, version, machine, processor = platform.uname()
     distname, distver, distid = platform.linux_distribution()
     hostname = os.uname()[1]
@@ -51,6 +55,7 @@
             'loadavg': loadavg[2]  # 15 minute average
             }
 
+
 def disk_info():
     rc, out, err = crm_script.call(['df'], shell=False)
     if rc == 0:
@@ -64,6 +69,7 @@
         return disk_use
     return []
 
+
 # configurations out of sync
 
 FILES = [
@@ -81,7 +87,7 @@
     for f in FILES:
         if os.path.isfile(f):
             try:
-                ret[f] = hashlib.sha1(open(f).read()).hexdigest()
+                ret[f] = 
hashlib.sha1(open(f).read().encode('utf-8')).hexdigest()
             except IOError as e:
                 ret[f] = "error: %s" % (e)
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/hahealth.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/hahealth.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/hahealth.py  
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/hahealth.py  
2017-12-13 16:37:15.000000000 +0100
@@ -1,16 +1,18 @@
-#!/usr/bin/env python
-from __future__ import unicode_literals
+#!/usr/bin/python3
 import os
 import crm_script as crm
 
-if not os.path.isfile('/usr/sbin/crm'):
+
+if not os.path.isfile('/usr/sbin/crm') and not os.path.isfile('/usr/bin/crm'):
     # crm not installed
     crm.exit_ok({'status': 'crm not installed'})
 
+
 def get_from_date():
     rc, out, err = crm.call("date '+%F %H:%M' --date='1 day ago'", shell=True)
     return out.strip()
 
+
 def create_report():
     cmd = ['crm', 'report',
            '-f', get_from_date(),
@@ -18,13 +20,16 @@
     rc, out, err = crm.call(cmd, shell=False)
     return rc == 0
 
+
 if not create_report():
     crm.exit_ok({'status': 'Failed to create report'})
 
+
 def extract_report():
     rc, out, err = crm.call(['tar', 'xjf', 'health-report.tar.bz2'], 
shell=False)
     return rc == 0
 
+
 if not extract_report():
     crm.exit_ok({'status': 'Failed to extract report'})
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/report.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/report.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/scripts/health/report.py    
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/scripts/health/report.py    
2017-12-13 16:37:15.000000000 +0100
@@ -1,6 +1,5 @@
-#!/usr/bin/env python
-from __future__ import print_function
-from __future__ import unicode_literals
+#!/usr/bin/python3
+import os
 import crm_script
 data = crm_script.get_input()
 health_report = data[1]
@@ -12,12 +11,15 @@
 warnings = []
 errors = []
 
+
 def warn(fmt, *args):
     warnings.append(fmt % args)
 
+
 def error(fmt, *args):
     errors.append(fmt % args)
 
+
 # sort {package: {version: [host]}}
 rpm_versions = {}
 
@@ -89,7 +91,8 @@
     check('release', 'Kernel release differs')
     check('distname', 'Distribution differs')
     check('distver', 'Distribution version differs')
-    #check('version', 'Kernel version differs')
+    # check('version', 'Kernel version differs')
+
 
 def compare_files(systems):
     keys = set()
@@ -101,6 +104,7 @@
             info = ', '.join('%s: %s' % (h, files.get(filename)) for h, files 
in systems)
             warn("%s: %s" % ("Files differ", info))
 
+
 compare_system((h, info['system']) for h, info in health_report.items())
 compare_files((h, info['files']) for h, info in health_report.items())
 
@@ -126,6 +130,5 @@
 if not errors and not warnings:
     print("No issues found.")
 
-import os
 workdir = os.path.dirname(crm_script.__file__)
 print("\nINFO: health-report in directory \"%s\"" % workdir)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/list-undocumented-commands.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/list-undocumented-commands.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/list-undocumented-commands.py  
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/list-undocumented-commands.py  
2017-12-13 16:37:15.000000000 +0100
@@ -1,9 +1,7 @@
-#!/usr/bin/env python
+#!/usr/bin/python3
 #
 # Script to discover and report undocumented commands.
 
-from __future__ import print_function
-from __future__ import unicode_literals
 from crmsh.ui_root import Root
 from crmsh import help
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/basicset 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/basicset
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/basicset     
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/basicset     
2017-12-13 16:37:15.000000000 +0100
@@ -1,4 +1,5 @@
 confbasic
+bundle
 confbasic-xml
 edit
 rset
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/bundle 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/bundle
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/bundle       
1970-01-01 01:00:00.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/bundle       
2017-12-13 16:37:15.000000000 +0100
@@ -0,0 +1,20 @@
+show Basic configure
+node node1
+delete node1
+node node1 \
+       attributes mem=16G
+node node2 utilization cpu=4
+primitive st stonith:ssh \
+       params hostlist='node1 node2' \
+       meta target-role="Started" \
+       op start requires=nothing timeout=60s \
+       op monitor interval=60m timeout=60s
+primitive st2 stonith:ssh \
+       params hostlist='node1 node2'
+bundle id=bundle-test1 docker image=test network ip-range-start=10.10.10.123 
port-mapping id=port1 port=80 storage storage-mapping id=storage1 
target-dir=test source-dir=test meta target-role=Stopped
+primitive id=dummy ocf:heartbeat:Dummy op monitor interval=10 meta 
target-role=Stopped
+bundle id=bundle-test2 docker image=test network ip-range-start=10.10.10.123 
primitive dummy meta target-role=Stopped priority=1
+property stonith-enabled=true
+_test
+verify
+.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/bundle.exp 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/bundle.exp
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/testcases/bundle.exp   
1970-01-01 01:00:00.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/testcases/bundle.exp   
2017-12-13 16:37:15.000000000 +0100
@@ -0,0 +1,52 @@
+.TRY Basic configure
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: delete node1
+.INP: node node1       attributes mem=16G
+.INP: node node2 utilization cpu=4
+.INP: primitive st stonith:ssh         params hostlist='node1 node2'   meta 
target-role="Started"      op start requires=nothing timeout=60s   op monitor 
interval=60m timeout=60s
+.INP: primitive st2 stonith:ssh        params hostlist='node1 node2'
+.INP: bundle id=bundle-test1 docker image=test network 
ip-range-start=10.10.10.123 port-mapping id=port1 port=80 storage 
storage-mapping id=storage1 target-dir=test source-dir=test meta 
target-role=Stopped
+.INP: primitive id=dummy ocf:heartbeat:Dummy op monitor interval=10 meta 
target-role=Stopped
+.INP: bundle id=bundle-test2 docker image=test network 
ip-range-start=10.10.10.123 primitive dummy meta target-role=Stopped priority=1
+.INP: property stonith-enabled=true
+.INP: _test
+ERROR: 15: object bundle-test2 does not reference its child dummy
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.INP: show
+node node1 \
+       attributes mem=16G
+node node2 \
+       utilization cpu=4
+primitive dummy Dummy \
+       op monitor interval=10 \
+       meta target-role=Stopped
+primitive st stonith:ssh \
+       params hostlist="node1 node2" \
+       meta target-role=Started \
+       op start requires=nothing timeout=60s interval=0 \
+       op monitor interval=60m timeout=60s
+primitive st2 stonith:ssh \
+       params hostlist="node1 node2"
+property cib-bootstrap-options: \
+       stonith-enabled=true
+bundle bundle-test1 \
+       docker image=test \
+       network ip-range-start=10.10.10.123 port-mapping id=port1 port=80 \
+       storage storage-mapping id=storage1 target-dir=test source-dir=test \
+       meta target-role=Stopped
+bundle bundle-test2 \
+       docker image=test \
+       network ip-range-start=10.10.10.123 \
+       primitive dummy \
+       meta target-role=Stopped priority=1
+.INP: commit
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/unittests/test_cliformat.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/unittests/test_cliformat.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/unittests/test_cliformat.py    
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/unittests/test_cliformat.py    
2017-12-13 16:37:15.000000000 +0100
@@ -330,3 +330,11 @@
 @with_setup(setup_func, teardown_func)
 def test_alerts_5():
     roundtrip('alert alert5 "/a/path" to { "/another/path" } meta timeout=30s')
+
+@with_setup(setup_func, teardown_func)
+def test_alerts_6():
+    roundtrip('alert alert6 "/a/path" select fencing attributes { standby } to 
{ "/another/path" } meta timeout=30s')
+
+@with_setup(setup_func, teardown_func)
+def test_alerts_7():
+    roundtrip('alert alert7 "/a/path" select fencing attributes foo=bar to { 
"/another/path" } meta timeout=30s')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/test/unittests/test_parse.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/unittests/test_parse.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/test/unittests/test_parse.py        
2017-12-04 17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/test/unittests/test_parse.py        
2017-12-13 16:37:15.000000000 +0100
@@ -347,6 +347,15 @@
             'rsc_ticket ticket-B_storage ticket-B: drbd-a:Master 
drbd-b:Master')
         self.assertEqual(out.get('id'), 'ticket-B_storage')
 
+    def test_bundle(self):
+        out = self._parse('bundle httpd docker image=pcmk:httpd replicas=3 
network ip-range-start=10.10.10.123 host-netmask=24 port-mapping port=80 
storage storage-mapping target-dir=/var/www/html source-dir=/srv/www options=rw 
primitive httpd-apache')
+        self.assertEqual(out.get('id'), 'httpd')
+        self.assertEqual(['pcmk:httpd'], out.xpath('/bundle/docker/@image'))
+        self.assertEqual(['httpd-apache'], out.xpath('/bundle/crmsh-ref/@id'))
+
+        out = self._parse('bundle httpd docker image=pcmk:httpd primitive 
httpd-apache apache')
+        self.assertFalse(out)
+
     def test_op(self):
         out = self._parse('monitor apache:Master 10s:20s')
         self.assertEqual(out.get('rsc'), 'apache')
@@ -502,6 +511,16 @@
         self.assertEqual(['10s'],
                          
out.xpath('/alert/recipient/meta_attributes/nvpair[@name="timeout"]/@value'))
 
+    def test_alerts_selectors(self):
+        "Test alerts w/ selectors (1.1.17+)"
+        out = self._parse('alert alert3 /tmp/foo.sh select nodes fencing 
attributes { standby shutdown } to { /tmp/bar.log meta timeout=10s }')
+        self.assertEqual(out.get('id'), 'alert3')
+        self.assertEqual(1, len(out.xpath('/alert/select/select_nodes')))
+        self.assertEqual(1, len(out.xpath('/alert/select/select_fencing')))
+        self.assertEqual(['standby', 'shutdown'],
+                         
out.xpath('/alert/select/select_attributes/attribute/@name'))
+
+
     def _parse_lines(self, lines):
         out = []
         for line in lines2cli(lines):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.0.0+git.1512406036.adc26906/utils/crm_script.py 
new/crmsh-4.0.0+git.1513179435.e1d17d7b/utils/crm_script.py
--- old/crmsh-4.0.0+git.1512406036.adc26906/utils/crm_script.py 2017-12-04 
17:47:16.000000000 +0100
+++ new/crmsh-4.0.0+git.1513179435.e1d17d7b/utils/crm_script.py 2017-12-13 
16:37:15.000000000 +0100
@@ -96,7 +96,7 @@
     debug("crm_script(call): %s" % (cmd))
     p = proc.Popen(cmd, shell=shell, stdin=None, stdout=proc.PIPE, 
stderr=proc.PIPE)
     out, err = p.communicate()
-    return p.returncode, out.strip(), err.strip()
+    return p.returncode, out.decode('utf-8').strip(), 
err.decode('utf-8').strip()
 
 
 def use_sudo():


Reply via email to