Blame SOURCES/bz1734687-01-pcs-resource-bundle-reset-fails-if.patch

1cadf2
From 833d54bec5e3ee6e49f654b4afdf053ac583062a Mon Sep 17 00:00:00 2001
1cadf2
From: Ivan Devat <idevat@redhat.com>
1cadf2
Date: Thu, 20 Jun 2019 11:44:46 +0200
1cadf2
Subject: [PATCH] bz1734687-01-pcs-resource-bundle-reset-fails-if
1cadf2
1cadf2
---
1cadf2
 pcs/cli/common/console_report.py              |  15 ++
1cadf2
 pcs/cli/common/test/test_console_report.py    |  14 +-
1cadf2
 pcs/cli/resource/parse_args.py                |  21 ++
1cadf2
 pcs/common/report_codes.py                    |   1 +
1cadf2
 pcs/lib/cib/resource/bundle.py                | 197 +++++++--------
1cadf2
 pcs/lib/commands/resource.py                  |  27 ++-
1cadf2
 .../test/resource/test_bundle_reset.py        | 226 ++++++++++++++++--
1cadf2
 pcs/lib/reports.py                            |  10 +
1cadf2
 pcs/lib/xml_tools.py                          |  15 ++
1cadf2
 pcs/pcs.8                                     |   2 +-
1cadf2
 pcs/resource.py                               |  29 ++-
1cadf2
 pcs/test/cib_resource/test_bundle.py          |   2 +-
1cadf2
 pcs/usage.py                                  |   2 +-
1cadf2
 13 files changed, 409 insertions(+), 152 deletions(-)
1cadf2
1cadf2
diff --git a/pcs/cli/common/console_report.py b/pcs/cli/common/console_report.py
1cadf2
index 945b83f6..3b882e3c 100644
1cadf2
--- a/pcs/cli/common/console_report.py
1cadf2
+++ b/pcs/cli/common/console_report.py
1cadf2
@@ -67,6 +67,11 @@ def format_fencing_level_target(target_type, target_value):
1cadf2
         return "{0}={1}".format(target_value[0], target_value[1])
1cadf2
     return target_value
1cadf2
 
1cadf2
+def format_list(a_list):
1cadf2
+    return ", ".join([
1cadf2
+        "'{0}'".format(x) for x in sorted(a_list)
1cadf2
+    ])
1cadf2
+
1cadf2
 def format_file_role(role):
1cadf2
     return _file_role_translation.get(role, role)
1cadf2
 
1cadf2
@@ -1458,6 +1463,16 @@ CODE_TO_MESSAGE_BUILDER_MAP = {
1cadf2
     codes.SYSTEM_WILL_RESET:
1cadf2
         "System will reset shortly"
1cadf2
     ,
1cadf2
+    codes.RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE: lambda info:
1cadf2
+        (
1cadf2
+            "Bundle '{bundle_id}' uses unsupported container type, therefore "
1cadf2
+            "it is not possible to set its container options. Supported "
1cadf2
+            "container types are: {_container_types}"
1cadf2
+        ).format(
1cadf2
+            _container_types=format_list(info["supported_container_types"]),
1cadf2
+            **info
1cadf2
+        )
1cadf2
+    ,
1cadf2
     codes.RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE: lambda info:
1cadf2
         (
1cadf2
             "Resource '{inner_resource_id}' will not be accessible by the "
1cadf2
diff --git a/pcs/cli/common/test/test_console_report.py b/pcs/cli/common/test/test_console_report.py
1cadf2
index ba7b4dbe..83d2b667 100644
1cadf2
--- a/pcs/cli/common/test/test_console_report.py
1cadf2
+++ b/pcs/cli/common/test/test_console_report.py
1cadf2
@@ -2133,7 +2133,6 @@ class SbdWatchdogNotSupported(NameBuildTest):
1cadf2
             }
1cadf2
         )
1cadf2
 
1cadf2
-
1cadf2
 class SbdWatchdogTestError(NameBuildTest):
1cadf2
     code = codes.SBD_WATCHDOG_TEST_ERROR
1cadf2
     def test_success(self):
1cadf2
@@ -2144,6 +2143,19 @@ class SbdWatchdogTestError(NameBuildTest):
1cadf2
             }
1cadf2
         )
1cadf2
 
1cadf2
+class ResourceBundleUnsupportedContainerType(NameBuildTest):
1cadf2
+    code = codes.RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE
1cadf2
+    def test_success(self):
1cadf2
+        self.assert_message_from_report(
1cadf2
+            (
1cadf2
+                "Bundle 'bundle id' uses unsupported container type, therefore "
1cadf2
+                "it is not possible to set its container options. Supported "
1cadf2
+                "container types are: 'a', 'b', 'c'"
1cadf2
+            ),
1cadf2
+            reports.resource_bundle_unsupported_container_type(
1cadf2
+                "bundle id", ["b", "a", "c"]
1cadf2
+            ),
1cadf2
+        )
1cadf2
 
1cadf2
 class ResourceInBundleNotAccessible(NameBuildTest):
1cadf2
     code = codes.RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE
1cadf2
diff --git a/pcs/cli/resource/parse_args.py b/pcs/cli/resource/parse_args.py
1cadf2
index 122a8f43..ea3db9ca 100644
1cadf2
--- a/pcs/cli/resource/parse_args.py
1cadf2
+++ b/pcs/cli/resource/parse_args.py
1cadf2
@@ -102,6 +102,27 @@ def parse_bundle_create_options(arg_list):
1cadf2
     }
1cadf2
     return parts
1cadf2
 
1cadf2
+def parse_bundle_reset_options(arg_list):
1cadf2
+    """
1cadf2
+    Commandline options: no options
1cadf2
+    """
1cadf2
+    groups = _parse_bundle_groups(arg_list)
1cadf2
+    container_options = groups.get("container", [])
1cadf2
+    parts = {
1cadf2
+        "container": prepare_options(container_options),
1cadf2
+        "network": prepare_options(groups.get("network", [])),
1cadf2
+        "port_map": [
1cadf2
+            prepare_options(port_map)
1cadf2
+            for port_map in groups.get("port-map", [])
1cadf2
+        ],
1cadf2
+        "storage_map": [
1cadf2
+            prepare_options(storage_map)
1cadf2
+            for storage_map in groups.get("storage-map", [])
1cadf2
+        ],
1cadf2
+        "meta": prepare_options(groups.get("meta", []))
1cadf2
+    }
1cadf2
+    return parts
1cadf2
+
1cadf2
 def _split_bundle_map_update_op_and_options(
1cadf2
     map_arg_list, result_parts, map_name
1cadf2
 ):
1cadf2
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
1cadf2
index f304d531..42825846 100644
1cadf2
--- a/pcs/common/report_codes.py
1cadf2
+++ b/pcs/common/report_codes.py
1cadf2
@@ -190,6 +190,7 @@ QDEVICE_USED_BY_CLUSTERS = "QDEVICE_USED_BY_CLUSTERS"
1cadf2
 REQUIRED_OPTION_IS_MISSING = "REQUIRED_OPTION_IS_MISSING"
1cadf2
 REQUIRED_OPTION_OF_ALTERNATIVES_IS_MISSING = "REQUIRED_OPTION_OF_ALTERNATIVES_IS_MISSING"
1cadf2
 RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE = "RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE"
1cadf2
+RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE = "RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE"
1cadf2
 RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE = "RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE"
1cadf2
 RESOURCE_CLEANUP_ERROR = "RESOURCE_CLEANUP_ERROR"
1cadf2
 RESOURCE_DOES_NOT_RUN = "RESOURCE_DOES_NOT_RUN"
1cadf2
diff --git a/pcs/lib/cib/resource/bundle.py b/pcs/lib/cib/resource/bundle.py
1cadf2
index 349ca72c..31a359c0 100644
1cadf2
--- a/pcs/lib/cib/resource/bundle.py
1cadf2
+++ b/pcs/lib/cib/resource/bundle.py
1cadf2
@@ -20,6 +20,7 @@ from pcs.lib.pacemaker.values import sanitize_id
1cadf2
 from pcs.lib.xml_tools import (
1cadf2
     get_sub_element,
1cadf2
     update_attributes_remove_empty,
1cadf2
+    reset_element,
1cadf2
 )
1cadf2
 
1cadf2
 TAG = "bundle"
1cadf2
@@ -84,15 +85,13 @@ def validate_new(
1cadf2
             ]
1cadf2
         )
1cadf2
         +
1cadf2
-        validate_reset(
1cadf2
-            id_provider,
1cadf2
-            container_type,
1cadf2
-            container_options,
1cadf2
-            network_options,
1cadf2
-            port_map,
1cadf2
-            storage_map,
1cadf2
-            force_options
1cadf2
-        )
1cadf2
+        _validate_container(container_type, container_options, force_options)
1cadf2
+        +
1cadf2
+        _validate_network_options_new(network_options, force_options)
1cadf2
+        +
1cadf2
+        _validate_port_map_list(port_map, id_provider, force_options)
1cadf2
+        +
1cadf2
+        _validate_storage_map_list(storage_map, id_provider, force_options)
1cadf2
     )
1cadf2
 
1cadf2
 def append_new(
1cadf2
@@ -129,14 +128,14 @@ def append_new(
1cadf2
     return bundle_element
1cadf2
 
1cadf2
 def validate_reset(
1cadf2
-    id_provider, container_type, container_options, network_options,
1cadf2
-    port_map, storage_map, force_options=False
1cadf2
+    id_provider, bundle_el, container_options, network_options, port_map,
1cadf2
+    storage_map, force_options=False
1cadf2
 ):
1cadf2
     """
1cadf2
     Validate bundle parameters, return list of report items
1cadf2
 
1cadf2
     IdProvider id_provider -- elements' ids generator and uniqueness checker
1cadf2
-    string container_type -- bundle container type
1cadf2
+    etree bundle_el -- the bundle to be reset
1cadf2
     dict container_options -- container options
1cadf2
     dict network_options -- network options
1cadf2
     list of dict port_map -- list of port mapping options
1cadf2
@@ -144,7 +143,7 @@ def validate_reset(
1cadf2
     bool force_options -- return warnings instead of forceable errors
1cadf2
     """
1cadf2
     return (
1cadf2
-        _validate_container(container_type, container_options, force_options)
1cadf2
+        _validate_container_reset(bundle_el, container_options, force_options)
1cadf2
         +
1cadf2
         _validate_network_options_new(network_options, force_options)
1cadf2
         +
1cadf2
@@ -153,71 +152,40 @@ def validate_reset(
1cadf2
         _validate_storage_map_list(storage_map, id_provider, force_options)
1cadf2
     )
1cadf2
 
1cadf2
-def reset(
1cadf2
-    bundle_element, id_provider, bundle_id, container_type, container_options,
1cadf2
-    network_options, port_map, storage_map, meta_attributes
1cadf2
-):
1cadf2
+def validate_reset_to_minimal(bundle_element):
1cadf2
     """
1cadf2
-    Remove configuration of bundle_element and create new one.
1cadf2
+    Validate removing configuration of bundle_element and keep the minimal one.
1cadf2
 
1cadf2
     etree bundle_element -- the bundle element that will be reset
1cadf2
-    IdProvider id_provider -- elements' ids generator
1cadf2
-    string bundle_id -- id of the bundle
1cadf2
-    string container_type -- bundle container type
1cadf2
-    dict container_options -- container options
1cadf2
-    dict network_options -- network options
1cadf2
-    list of dict port_map -- list of port mapping options
1cadf2
-    list of dict storage_map -- list of storage mapping options
1cadf2
-    dict meta_attributes -- meta attributes
1cadf2
     """
1cadf2
-    # pylint: disable=too-many-arguments
1cadf2
+    if not _is_supported_container(_get_container_element(bundle_element)):
1cadf2
+        return [_get_report_unsupported_container(bundle_element)]
1cadf2
+    return []
1cadf2
 
1cadf2
-    # Old bundle configuration is removed and re-created. We aren't trying
1cadf2
-    # to keep ids:
1cadf2
-    # * It doesn't make sense to reference these ids.
1cadf2
-    # * Newly created ids are based on (are prefixed by) the bundle element id,
1cadf2
-    #   which does not change. Therefore, it is VERY HIGHLY probable the newly
1cadf2
-    #   created ids will be the same as the original ones.
1cadf2
-    elements_without_reset_impact = []
1cadf2
+def reset_to_minimal(bundle_element):
1cadf2
+    """
1cadf2
+    Remove configuration of bundle_element and keep the minimal one.
1cadf2
 
1cadf2
+    etree bundle_element -- the bundle element that will be reset
1cadf2
+    """
1cadf2
     # Elements network, storage and meta_attributes must be kept even if they
1cadf2
     # are without children.
1cadf2
     # See https://bugzilla.redhat.com/show_bug.cgi?id=1642514
1cadf2
-    #
1cadf2
-    # The only scenario that makes sense is that these elements are empty
1cadf2
-    # and no attributes or children are requested for them. So we collect only
1cadf2
-    # deleted tags and we will ensure creation minimal relevant elements at
1cadf2
-    # least.
1cadf2
-    indelible_tags = []
1cadf2
-    for child in list(bundle_element):
1cadf2
-        if child.tag in ["network", "storage", META_ATTRIBUTES_TAG]:
1cadf2
-            indelible_tags.append(child.tag)
1cadf2
-        elif child.tag != "docker":
1cadf2
-            # Only primitive should be found here, currently.
1cadf2
-            # The order of various element tags has no practical impact so we
1cadf2
-            # don't care about it here.
1cadf2
-            elements_without_reset_impact.append(child)
1cadf2
-        bundle_element.remove(child)
1cadf2
+    # Element of container type is required.
1cadf2
 
1cadf2
-    _append_container(bundle_element, container_type, container_options)
1cadf2
-    if network_options or port_map or "network" in indelible_tags:
1cadf2
-        _append_network(
1cadf2
-            bundle_element,
1cadf2
-            id_provider,
1cadf2
-            bundle_id,
1cadf2
-            network_options,
1cadf2
-            port_map,
1cadf2
-        )
1cadf2
-    if storage_map or "storage" in indelible_tags:
1cadf2
-        _append_storage(bundle_element, id_provider, bundle_id, storage_map)
1cadf2
-    if meta_attributes or META_ATTRIBUTES_TAG in indelible_tags:
1cadf2
-        append_new_meta_attributes(
1cadf2
-            bundle_element,
1cadf2
-            meta_attributes,
1cadf2
-            id_provider,
1cadf2
-        )
1cadf2
-    for element in elements_without_reset_impact:
1cadf2
-        bundle_element.append(element)
1cadf2
+    # There can be other elements beside bundle configuration (e.g. primitive).
1cadf2
+    # These elements stay untouched.
1cadf2
+    # Like any function that manipulates with cib, this also assumes prior
1cadf2
+    # validation that container is supported.
1cadf2
+    for child in list(bundle_element):
1cadf2
+        if child.tag in ["network", "storage"]:
1cadf2
+            reset_element(child)
1cadf2
+        if child.tag == META_ATTRIBUTES_TAG:
1cadf2
+            reset_element(child, keep_attrs=["id"])
1cadf2
+        if child.tag == "docker":
1cadf2
+            # docker elements requires the "image" attribute to
1cadf2
+            # be set.
1cadf2
+            reset_element(child, keep_attrs=["image"])
1cadf2
 
1cadf2
 def validate_update(
1cadf2
     id_provider, bundle_el, container_options, network_options,
1cadf2
@@ -237,55 +205,26 @@ def validate_update(
1cadf2
     list of string storage_map_remove -- list of storage mapping ids to remove
1cadf2
     bool force_options -- return warnings instead of forceable errors
1cadf2
     """
1cadf2
-    report_list = []
1cadf2
-
1cadf2
-    container_el = _get_container_element(bundle_el)
1cadf2
-    if container_el is not None and container_el.tag == "docker":
1cadf2
-        # TODO call the proper function once more container types are
1cadf2
-        # supported by pacemaker
1cadf2
-        report_list.extend(
1cadf2
-            _validate_container_docker_options_update(
1cadf2
-                container_el,
1cadf2
-                container_options,
1cadf2
-                force_options
1cadf2
-            )
1cadf2
-        )
1cadf2
-
1cadf2
-    network_el = bundle_el.find("network")
1cadf2
-    if network_el is None:
1cadf2
-        report_list.extend(
1cadf2
-            _validate_network_options_new(network_options, force_options)
1cadf2
-        )
1cadf2
-    else:
1cadf2
-        report_list.extend(
1cadf2
-            _validate_network_options_update(
1cadf2
-                bundle_el,
1cadf2
-                network_el,
1cadf2
-                network_options,
1cadf2
-                force_options
1cadf2
-            )
1cadf2
-        )
1cadf2
-
1cadf2
     # TODO It will probably be needed to split the following validators to
1cadf2
     # create and update variants. It should be done once the need exists and
1cadf2
     # not sooner.
1cadf2
-    report_list.extend(
1cadf2
+    return (
1cadf2
+        _validate_container_update(bundle_el, container_options, force_options)
1cadf2
+        +
1cadf2
+        _validate_network_update(bundle_el, network_options, force_options)
1cadf2
+        +
1cadf2
         _validate_port_map_list(port_map_add, id_provider, force_options)
1cadf2
-    )
1cadf2
-    report_list.extend(
1cadf2
+        +
1cadf2
         _validate_storage_map_list(storage_map_add, id_provider, force_options)
1cadf2
-    )
1cadf2
-    report_list.extend(
1cadf2
+        +
1cadf2
         _validate_map_ids_exist(
1cadf2
             bundle_el, "port-mapping", "port-map", port_map_remove
1cadf2
         )
1cadf2
-    )
1cadf2
-    report_list.extend(
1cadf2
+        +
1cadf2
         _validate_map_ids_exist(
1cadf2
             bundle_el, "storage-mapping", "storage-map", storage_map_remove
1cadf2
         )
1cadf2
     )
1cadf2
-    return report_list
1cadf2
 
1cadf2
 def update(
1cadf2
     id_provider, bundle_el, container_options, network_options,
1cadf2
@@ -402,6 +341,19 @@ def get_inner_resource(bundle_el):
1cadf2
         return resources[0]
1cadf2
     return None
1cadf2
 
1cadf2
+def _is_supported_container(container_el):
1cadf2
+    return (
1cadf2
+        container_el is not None
1cadf2
+        and
1cadf2
+        container_el.tag == "docker"
1cadf2
+    )
1cadf2
+
1cadf2
+def _get_report_unsupported_container(bundle_el):
1cadf2
+    return reports.resource_bundle_unsupported_container_type(
1cadf2
+        bundle_el.get("id"),
1cadf2
+        ["docker"],
1cadf2
+    )
1cadf2
+
1cadf2
 def _validate_container(container_type, container_options, force_options=False):
1cadf2
     if container_type != "docker":
1cadf2
         return [
1cadf2
@@ -411,7 +363,10 @@ def _validate_container(container_type, container_options, force_options=False):
1cadf2
                 ["docker"],
1cadf2
             )
1cadf2
         ]
1cadf2
+    return _validate_container_options(container_options, force_options)
1cadf2
+
1cadf2
 
1cadf2
+def _validate_container_options(container_options, force_options=False):
1cadf2
     validators = [
1cadf2
         validate.is_required("image", "container"),
1cadf2
         validate.value_not_empty("image", "image name"),
1cadf2
@@ -434,6 +389,30 @@ def _validate_container(container_type, container_options, force_options=False):
1cadf2
         )
1cadf2
     )
1cadf2
 
1cadf2
+def _validate_container_reset(bundle_el, container_options, force_options):
1cadf2
+    # Unlike in the case of update, in reset empty options are not necessary
1cadf2
+    # valid - user MUST set everything (including required options e.g. image).
1cadf2
+    if (
1cadf2
+        container_options
1cadf2
+        and
1cadf2
+        not _is_supported_container(_get_container_element(bundle_el))
1cadf2
+    ):
1cadf2
+        return [_get_report_unsupported_container(bundle_el)]
1cadf2
+    return _validate_container_options(container_options, force_options)
1cadf2
+
1cadf2
+def _validate_container_update(bundle_el, options, force_options):
1cadf2
+    # Validate container options only if they are being updated. Empty options
1cadf2
+    # are valid - user DOESN'T NEED to change anything.
1cadf2
+    if not options:
1cadf2
+        return []
1cadf2
+
1cadf2
+    container_el = _get_container_element(bundle_el)
1cadf2
+    if not _is_supported_container(container_el):
1cadf2
+        return [_get_report_unsupported_container(bundle_el)]
1cadf2
+    return _validate_container_docker_options_update(
1cadf2
+        container_el, options, force_options
1cadf2
+    )
1cadf2
+
1cadf2
 def _validate_container_docker_options_update(
1cadf2
     docker_el, options, force_options
1cadf2
 ):
1cadf2
@@ -502,6 +481,14 @@ def _is_pcmk_remote_acccessible_after_update(network_el, options):
1cadf2
 
1cadf2
     return not (case1 or case2 or case3)
1cadf2
 
1cadf2
+def _validate_network_update(bundle_el, options, force_options):
1cadf2
+    network_el = bundle_el.find("network")
1cadf2
+    if network_el is None:
1cadf2
+        return _validate_network_options_new(options, force_options)
1cadf2
+    return _validate_network_options_update(
1cadf2
+        bundle_el, network_el, options, force_options
1cadf2
+    )
1cadf2
+
1cadf2
 def _validate_network_options_update(
1cadf2
     bundle_el, network_el, options, force_options
1cadf2
 ):
1cadf2
diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py
1cadf2
index de5cfb4e..f34fef4b 100644
1cadf2
--- a/pcs/lib/commands/resource.py
1cadf2
+++ b/pcs/lib/commands/resource.py
1cadf2
@@ -580,7 +580,7 @@ def bundle_create(
1cadf2
             resource.common.disable(bundle_element)
1cadf2
 
1cadf2
 def bundle_reset(
1cadf2
-    env, bundle_id, container_type, container_options=None,
1cadf2
+    env, bundle_id, container_options=None,
1cadf2
     network_options=None, port_map=None, storage_map=None, meta_attributes=None,
1cadf2
     force_options=False,
1cadf2
     ensure_disabled=False,
1cadf2
@@ -592,7 +592,6 @@ def bundle_reset(
1cadf2
 
1cadf2
     LibraryEnvironment env -- provides communication with externals
1cadf2
     string bundle_id -- id of the bundle to reset
1cadf2
-    string container_type -- container engine name (docker, lxc...)
1cadf2
     dict container_options -- container options
1cadf2
     dict network_options -- network options
1cadf2
     list of dict port_map -- a list of port mapping options
1cadf2
@@ -619,11 +618,17 @@ def bundle_reset(
1cadf2
         ),
1cadf2
         required_cib_version=Version(2, 8, 0),
1cadf2
     ) as resources_section:
1cadf2
+        bundle_element = _find_bundle(resources_section, bundle_id)
1cadf2
+        env.report_processor.process_list(
1cadf2
+            resource.bundle.validate_reset_to_minimal(bundle_element)
1cadf2
+        )
1cadf2
+        resource.bundle.reset_to_minimal(bundle_element)
1cadf2
+
1cadf2
         id_provider = IdProvider(resources_section)
1cadf2
         env.report_processor.process_list(
1cadf2
             resource.bundle.validate_reset(
1cadf2
                 id_provider,
1cadf2
-                container_type,
1cadf2
+                bundle_element,
1cadf2
                 container_options,
1cadf2
                 network_options,
1cadf2
                 port_map,
1cadf2
@@ -633,23 +638,21 @@ def bundle_reset(
1cadf2
             )
1cadf2
         )
1cadf2
 
1cadf2
-        bundle_element = _find_bundle(resources_section, bundle_id)
1cadf2
-        resource.bundle.reset(
1cadf2
-            bundle_element,
1cadf2
+        resource.bundle.update(
1cadf2
             id_provider,
1cadf2
-            bundle_id,
1cadf2
-            container_type,
1cadf2
+            bundle_element,
1cadf2
             container_options,
1cadf2
             network_options,
1cadf2
-            port_map,
1cadf2
-            storage_map,
1cadf2
-            meta_attributes,
1cadf2
+            port_map_add=port_map,
1cadf2
+            port_map_remove=[],
1cadf2
+            storage_map_add=storage_map,
1cadf2
+            storage_map_remove=[],
1cadf2
+            meta_attributes=meta_attributes,
1cadf2
         )
1cadf2
 
1cadf2
         if ensure_disabled:
1cadf2
             resource.common.disable(bundle_element)
1cadf2
 
1cadf2
-
1cadf2
 def bundle_update(
1cadf2
     env, bundle_id, container_options=None, network_options=None,
1cadf2
     port_map_add=None, port_map_remove=None, storage_map_add=None,
1cadf2
diff --git a/pcs/lib/commands/test/resource/test_bundle_reset.py b/pcs/lib/commands/test/resource/test_bundle_reset.py
1cadf2
index 8fbeae78..bdea4b39 100644
1cadf2
--- a/pcs/lib/commands/test/resource/test_bundle_reset.py
1cadf2
+++ b/pcs/lib/commands/test/resource/test_bundle_reset.py
1cadf2
@@ -15,6 +15,7 @@ from pcs.lib.commands.test.resource.bundle_common import(
1cadf2
     WaitMixin,
1cadf2
 )
1cadf2
 from pcs.lib.errors import  ReportItemSeverity as severities
1cadf2
+from pcs.test.tools import fixture
1cadf2
 
1cadf2
 class BaseMixin(FixturesMixin):
1cadf2
     bundle_id = "B1"
1cadf2
@@ -24,16 +25,13 @@ class BaseMixin(FixturesMixin):
1cadf2
     def initial_resources(self):
1cadf2
         return self.fixture_resources_bundle_simple
1cadf2
 
1cadf2
-    def bundle_reset(
1cadf2
-        self, bundle_id=None, container_type=None, **params
1cadf2
-    ):
1cadf2
+    def bundle_reset(self, bundle_id=None, **params):
1cadf2
         if "container_options" not in params:
1cadf2
             params["container_options"] = {"image": self.image}
1cadf2
 
1cadf2
         bundle_reset(
1cadf2
             self.env_assist.get_env(),
1cadf2
             bundle_id=bundle_id or self.bundle_id,
1cadf2
-            container_type=container_type or self.container_type,
1cadf2
             **params
1cadf2
         )
1cadf2
 
1cadf2
@@ -44,6 +42,8 @@ class Minimal(BaseMixin, SetUpMixin, TestCase):
1cadf2
     container_type = "docker"
1cadf2
 
1cadf2
     def test_success_zero_change(self):
1cadf2
+        # Resets a bundle with only an image set to a bundle with the same
1cadf2
+        # image set and no other options.
1cadf2
         self.config.env.push_cib(resources=self.initial_resources)
1cadf2
         self.bundle_reset()
1cadf2
 
1cadf2
@@ -87,6 +87,18 @@ class Minimal(BaseMixin, SetUpMixin, TestCase):
1cadf2
             expected_in_processor=False,
1cadf2
         )
1cadf2
 
1cadf2
+    def test_no_options_set(self):
1cadf2
+        self.env_assist.assert_raise_library_error(
1cadf2
+            lambda: bundle_reset(self.env_assist.get_env(), self.bundle_id),
1cadf2
+            [
1cadf2
+                fixture.error(
1cadf2
+                    report_codes.REQUIRED_OPTION_IS_MISSING,
1cadf2
+                    option_names=["image"],
1cadf2
+                    option_type="container",
1cadf2
+                ),
1cadf2
+            ]
1cadf2
+        )
1cadf2
+
1cadf2
 class Full(BaseMixin, SetUpMixin, TestCase):
1cadf2
     container_type = "docker"
1cadf2
     fixture_primitive = """
1cadf2
@@ -98,24 +110,11 @@ class Full(BaseMixin, SetUpMixin, TestCase):
1cadf2
         return """
1cadf2
             <resources>
1cadf2
                 <bundle id="{bundle_id}">
1cadf2
-                    <meta_attributes id="{bundle_id}-meta_attributes">
1cadf2
-                        
1cadf2
-                            name="target-role"
1cadf2
-                            value="Stopped"
1cadf2
-                        />
1cadf2
-                    </meta_attributes>
1cadf2
                     <{container_type}
1cadf2
                         image="{image}"
1cadf2
-                        replicas="0"
1cadf2
-                        replicas-per-host="0"
1cadf2
+                        replicas="1"
1cadf2
+                        replicas-per-host="1"
1cadf2
                     />
1cadf2
-                    <meta_attributes id="{bundle_id}-meta_attributes">
1cadf2
-                        
1cadf2
-                            id="{bundle_id}-meta_attributes-is-managed"
1cadf2
-                            name="is-managed"
1cadf2
-                            value="false"
1cadf2
-                        />
1cadf2
-                    </meta_attributes>
1cadf2
                     
1cadf2
                         control-port="12345"
1cadf2
                         host-interface="eth0"
1cadf2
@@ -140,6 +139,13 @@ class Full(BaseMixin, SetUpMixin, TestCase):
1cadf2
                             target-dir="/tmp/{container_type}2b"
1cadf2
                         />
1cadf2
                     </storage>
1cadf2
+                    <meta_attributes id="{bundle_id}-meta_attributes">
1cadf2
+                        
1cadf2
+                            id="{bundle_id}-meta_attributes-target-role"
1cadf2
+                            name="target-role"
1cadf2
+                            value="Stopped"
1cadf2
+                        />
1cadf2
+                    </meta_attributes>
1cadf2
                     {fixture_primitive}
1cadf2
                 </bundle>
1cadf2
             </resources>
1cadf2
@@ -211,8 +217,8 @@ class Full(BaseMixin, SetUpMixin, TestCase):
1cadf2
                             
1cadf2
                                 id="{bundle_id}-storage-map"
1cadf2
                                 options="extra options 2"
1cadf2
-                                source-dir="/tmp/{container_type}2a"
1cadf2
-                                target-dir="/tmp/{container_type}2b"
1cadf2
+                                source-dir="/tmp/{container_type}2aa"
1cadf2
+                                target-dir="/tmp/{container_type}2bb"
1cadf2
                             />
1cadf2
                         </storage>
1cadf2
                         <meta_attributes id="{bundle_id}-meta_attributes">
1cadf2
@@ -251,14 +257,93 @@ class Full(BaseMixin, SetUpMixin, TestCase):
1cadf2
             storage_map=[
1cadf2
                 {
1cadf2
                     "options": "extra options 2",
1cadf2
-                    "source-dir": "/tmp/{0}2a".format(self.container_type),
1cadf2
-                    "target-dir": "/tmp/{0}2b".format(self.container_type),
1cadf2
+                    "source-dir": "/tmp/{0}2aa".format(self.container_type),
1cadf2
+                    "target-dir": "/tmp/{0}2bb".format(self.container_type),
1cadf2
                 },
1cadf2
             ],
1cadf2
             meta_attributes={
1cadf2
                 "target-role": "Started",
1cadf2
             }
1cadf2
         )
1cadf2
+
1cadf2
+    def test_success_keep_map_ids(self):
1cadf2
+        self.config.env.push_cib(replace={
1cadf2
+            ".//resources/bundle/network":
1cadf2
+                """
1cadf2
+                    
1cadf2
+                        control-port="12345"
1cadf2
+                        host-interface="eth0"
1cadf2
+                        host-netmask="24"
1cadf2
+                        ip-range-start="192.168.100.200"
1cadf2
+                    >
1cadf2
+                        
1cadf2
+                            id="{bundle_id}-port-map-1001"
1cadf2
+                            internal-port="3002"
1cadf2
+                            port="3000"
1cadf2
+                        />
1cadf2
+                        
1cadf2
+                            id="{bundle_id}-port-map-3000-3300"
1cadf2
+                            range="4000-4400"
1cadf2
+                        />
1cadf2
+                    </network>
1cadf2
+                """.format(bundle_id=self.bundle_id, )
1cadf2
+            ,
1cadf2
+            ".//resources/bundle/storage":
1cadf2
+                """
1cadf2
+                    <storage>
1cadf2
+                        
1cadf2
+                            id="{bundle_id}-storage-map"
1cadf2
+                            options="extra options 2"
1cadf2
+                            source-dir="/tmp/docker/2aa"
1cadf2
+                            target-dir="/tmp/docker/2bb"
1cadf2
+                        />
1cadf2
+                    </storage>
1cadf2
+                """.format(bundle_id=self.bundle_id)
1cadf2
+            ,
1cadf2
+        })
1cadf2
+
1cadf2
+        # Every value is kept as before except port_map and storage_map.
1cadf2
+        self.bundle_reset(
1cadf2
+            container_options={
1cadf2
+                "image": self.image,
1cadf2
+                "replicas": "1",
1cadf2
+                "replicas-per-host": "1",
1cadf2
+            },
1cadf2
+            network_options={
1cadf2
+                "control-port": "12345",
1cadf2
+                "host-interface": "eth0",
1cadf2
+                "host-netmask": "24",
1cadf2
+                "ip-range-start": "192.168.100.200",
1cadf2
+            },
1cadf2
+            port_map=[
1cadf2
+                {
1cadf2
+                    "id": "{bundle_id}-port-map-1001"
1cadf2
+                        .format(bundle_id=self.bundle_id)
1cadf2
+                    ,
1cadf2
+                    "internal-port": "3002",
1cadf2
+                    "port": "3000",
1cadf2
+                },
1cadf2
+                {
1cadf2
+                    "id": "{bundle_id}-port-map-3000-3300"
1cadf2
+                        .format(bundle_id=self.bundle_id)
1cadf2
+                    ,
1cadf2
+                    "range": "4000-4400",
1cadf2
+                },
1cadf2
+            ],
1cadf2
+            storage_map=[
1cadf2
+                {
1cadf2
+                    "id": "{bundle_id}-storage-map"
1cadf2
+                        .format(bundle_id=self.bundle_id)
1cadf2
+                    ,
1cadf2
+                    "options": "extra options 2",
1cadf2
+                    "source-dir": "/tmp/docker/2aa",
1cadf2
+                    "target-dir": "/tmp/docker/2bb",
1cadf2
+                },
1cadf2
+            ],
1cadf2
+            meta_attributes={
1cadf2
+                "target-role": "Stopped",
1cadf2
+            }
1cadf2
+        )
1cadf2
 class Parametrized(
1cadf2
     BaseMixin, ParametrizedContainerMixin, UpgradeMixin, TestCase
1cadf2
 ):
1cadf2
@@ -275,9 +360,104 @@ class ResetWithStorageMap(BaseMixin, StorageMapMixin, TestCase):
1cadf2
 
1cadf2
 class ResetWithMetaMap(BaseMixin, MetaMixin, TestCase):
1cadf2
     container_type = "docker"
1cadf2
+    def test_success(self):
1cadf2
+        # When there is no meta attributes the new one are put on the first
1cadf2
+        # possition (since reset now uses update internally). This is the reason
1cadf2
+        # for overriding of this MetaMixin test.
1cadf2
+        self.config.env.push_cib(
1cadf2
+            resources="""
1cadf2
+                <resources>
1cadf2
+                    <bundle id="{bundle_id}">
1cadf2
+                        <meta_attributes id="{bundle_id}-meta_attributes">
1cadf2
+                            
1cadf2
+                                name="is-managed" value="false" />
1cadf2
+                            
1cadf2
+                                name="target-role" value="Stopped" />
1cadf2
+                        </meta_attributes>
1cadf2
+                        <{container_type} image="{image}" />
1cadf2
+                    </bundle>
1cadf2
+                </resources>
1cadf2
+            """
1cadf2
+            .format(
1cadf2
+                container_type=self.container_type,
1cadf2
+                bundle_id=self.bundle_id,
1cadf2
+                image=self.image,
1cadf2
+            )
1cadf2
+        )
1cadf2
+        self.run_bundle_cmd(
1cadf2
+            meta_attributes={
1cadf2
+                "target-role": "Stopped",
1cadf2
+                "is-managed": "false",
1cadf2
+            }
1cadf2
+        )
1cadf2
 
1cadf2
 class ResetWithAllOptions(BaseMixin, AllOptionsMixin, TestCase):
1cadf2
     container_type = "docker"
1cadf2
 
1cadf2
 class ResetWithWait(BaseMixin, WaitMixin, TestCase):
1cadf2
     container_type = "docker"
1cadf2
+
1cadf2
+class ResetUnknownContainerType(BaseMixin, SetUpMixin, TestCase):
1cadf2
+    container_type = "unknown"
1cadf2
+    def test_error_or_unknown_container(self):
1cadf2
+        self.env_assist.assert_raise_library_error(
1cadf2
+            lambda: bundle_reset(self.env_assist.get_env(), self.bundle_id),
1cadf2
+            [
1cadf2
+                fixture.error(
1cadf2
+                    report_codes.RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE,
1cadf2
+                    bundle_id="B1",
1cadf2
+                    supported_container_types=["docker"],
1cadf2
+                ),
1cadf2
+            ]
1cadf2
+        )
1cadf2
+
1cadf2
+class NoMetaIdRegenerationDocker(BaseMixin, SetUpMixin, TestCase):
1cadf2
+    container_type = "docker"
1cadf2
+    @property
1cadf2
+    def initial_resources(self):
1cadf2
+        return """
1cadf2
+            <resources>
1cadf2
+                <bundle id="{bundle_id}">
1cadf2
+                    
1cadf2
+                        image="{image}"
1cadf2
+                        replicas="1"
1cadf2
+                        replicas-per-host="1"
1cadf2
+                    />
1cadf2
+                    <meta_attributes id="CUSTOM_ID">
1cadf2
+                        
1cadf2
+                            id="ANOTHER_ID-target-role"
1cadf2
+                            name="target-role"
1cadf2
+                            value="Stopped"
1cadf2
+                        />
1cadf2
+                    </meta_attributes>
1cadf2
+                </bundle>
1cadf2
+            </resources>
1cadf2
+        """.format(
1cadf2
+            container_type=self.container_type,
1cadf2
+            bundle_id=self.bundle_id,
1cadf2
+            image=self.image,
1cadf2
+        )
1cadf2
+    def test_dont_regenerate_meta_attributes_id(self):
1cadf2
+        self.config.env.push_cib(replace={
1cadf2
+            ".//resources/bundle/meta_attributes":
1cadf2
+                """
1cadf2
+                    <meta_attributes id="CUSTOM_ID">
1cadf2
+                        
1cadf2
+                            id="CUSTOM_ID-target-role"
1cadf2
+                            name="target-role"
1cadf2
+                            value="Stopped"
1cadf2
+                        />
1cadf2
+                    </meta_attributes>
1cadf2
+                """
1cadf2
+            ,
1cadf2
+        })
1cadf2
+        self.bundle_reset(
1cadf2
+            container_options={
1cadf2
+                "image": self.image,
1cadf2
+                "replicas": "1",
1cadf2
+                "replicas-per-host": "1",
1cadf2
+            },
1cadf2
+            meta_attributes={
1cadf2
+                "target-role": "Stopped",
1cadf2
+            }
1cadf2
+        )
1cadf2
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
1cadf2
index 92764551..045e8eca 100644
1cadf2
--- a/pcs/lib/reports.py
1cadf2
+++ b/pcs/lib/reports.py
1cadf2
@@ -2947,6 +2947,16 @@ def system_will_reset():
1cadf2
         report_codes.SYSTEM_WILL_RESET,
1cadf2
     )
1cadf2
 
1cadf2
+def resource_bundle_unsupported_container_type(
1cadf2
+    bundle_id, supported_container_types
1cadf2
+):
1cadf2
+    return ReportItem.error(
1cadf2
+        report_codes.RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE,
1cadf2
+        info=dict(
1cadf2
+            bundle_id=bundle_id,
1cadf2
+            supported_container_types=sorted(supported_container_types),
1cadf2
+        ),
1cadf2
+    )
1cadf2
 
1cadf2
 def resource_in_bundle_not_accessible(
1cadf2
     bundle_id, inner_resource_id,
1cadf2
diff --git a/pcs/lib/xml_tools.py b/pcs/lib/xml_tools.py
1cadf2
index acd30a71..8d59377c 100644
1cadf2
--- a/pcs/lib/xml_tools.py
1cadf2
+++ b/pcs/lib/xml_tools.py
1cadf2
@@ -129,3 +129,18 @@ def remove_when_pointless(element, attribs_important=True):
1cadf2
 
1cadf2
     if not is_element_useful:
1cadf2
         element.getparent().remove(element)
1cadf2
+
1cadf2
+def reset_element(element, keep_attrs=None):
1cadf2
+    """
1cadf2
+    Remove all subelements and all attributes (except mentioned in keep_attrs)
1cadf2
+    of given element.
1cadf2
+
1cadf2
+    lxml.etree.element element -- element to reset
1cadf2
+    list keep_attrs -- names of attributes thas should be kept
1cadf2
+    """
1cadf2
+    keep_attrs = keep_attrs or []
1cadf2
+    for child in list(element):
1cadf2
+        element.remove(child)
1cadf2
+    for key in element.attrib.keys():
1cadf2
+        if key not in keep_attrs:
1cadf2
+            del element.attrib[key]
1cadf2
diff --git a/pcs/pcs.8 b/pcs/pcs.8
1cadf2
index 0ec4359a..5ecb7dab 100644
1cadf2
--- a/pcs/pcs.8
1cadf2
+++ b/pcs/pcs.8
1cadf2
@@ -168,7 +168,7 @@ Configure a resource or group as a multi\-state (master/slave) resource.  If \fB
1cadf2
 bundle create <bundle id> container <container type> [<container options>] [network <network options>] [port\-map <port options>]... [storage\-map <storage options>]... [meta <meta options>] [\fB\-\-disabled\fR] [\fB\-\-wait\fR[=n]]
1cadf2
 Create a new bundle encapsulating no resources. The bundle can be used either as it is or a resource may be put into it at any time. If \fB\-\-disabled\fR is specified, the bundle is not started automatically. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the bundle to start and then return 0 on success or 1 on error. If 'n' is not specified it defaults to 60 minutes.
1cadf2
 .TP
1cadf2
-bundle reset <bundle id> container <container type> [<container options>] [network <network options>] [port\-map <port options>]... [storage\-map <storage options>]... [meta <meta options>] [\fB\-\-disabled\fR] [\fB\-\-wait\fR[=n]]
1cadf2
+bundle reset <bundle id> [container <container options>] [network <network options>] [port\-map <port options>]... [storage\-map <storage options>]... [meta <meta options>] [\fB\-\-disabled\fR] [\fB\-\-wait\fR[=n]]
1cadf2
 Configure specified bundle with given options. Unlike bundle update, this command resets the bundle according given options - no previous options are kept. Resources inside the bundle are kept as they are. If \fB\-\-disabled\fR is specified, the bundle is not started automatically. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the bundle to start and then return 0 on success or 1 on error. If 'n' is not specified it defaults to 60 minutes.
1cadf2
 .TP
1cadf2
 bundle update <bundle id> [container <container options>] [network <network options>] [port\-map (add <port options>) | (remove <id>...)]... [storage\-map (add <storage options>) | (remove <id>...)]... [meta <meta options>] [\fB\-\-wait\fR[=n]]
1cadf2
diff --git a/pcs/resource.py b/pcs/resource.py
1cadf2
index f615f682..dea30f49 100644
1cadf2
--- a/pcs/resource.py
1cadf2
+++ b/pcs/resource.py
1cadf2
@@ -24,6 +24,7 @@ from pcs.cli.common.errors import CmdLineInputError
1cadf2
 from pcs.cli.common.parse_args import prepare_options
1cadf2
 from pcs.cli.resource.parse_args import (
1cadf2
     parse_bundle_create_options,
1cadf2
+    parse_bundle_reset_options,
1cadf2
     parse_bundle_update_options,
1cadf2
     parse_create as parse_create_args,
1cadf2
 )
1cadf2
@@ -2896,7 +2897,23 @@ def resource_bundle_create_cmd(lib, argv, modifiers):
1cadf2
       * --wait
1cadf2
       * -f - CIB file
1cadf2
     """
1cadf2
-    _resource_bundle_configure(lib.resource.bundle_create, argv, modifiers)
1cadf2
+    if not argv:
1cadf2
+        raise CmdLineInputError()
1cadf2
+
1cadf2
+    bundle_id = argv[0]
1cadf2
+    parts = parse_bundle_create_options(argv[1:])
1cadf2
+    lib.resource.bundle_create(
1cadf2
+        bundle_id,
1cadf2
+        parts["container_type"],
1cadf2
+        container_options=parts["container"],
1cadf2
+        network_options=parts["network"],
1cadf2
+        port_map=parts["port_map"],
1cadf2
+        storage_map=parts["storage_map"],
1cadf2
+        meta_attributes=parts["meta"],
1cadf2
+        force_options=modifiers["force"],
1cadf2
+        ensure_disabled=modifiers["disabled"],
1cadf2
+        wait=modifiers["wait"]
1cadf2
+    )
1cadf2
 
1cadf2
 def resource_bundle_reset_cmd(lib, argv, modifiers):
1cadf2
     """
1cadf2
@@ -2906,17 +2923,13 @@ def resource_bundle_reset_cmd(lib, argv, modifiers):
1cadf2
       * --wait
1cadf2
       * -f - CIB file
1cadf2
     """
1cadf2
-    _resource_bundle_configure(lib.resource.bundle_reset, argv, modifiers)
1cadf2
-
1cadf2
-def _resource_bundle_configure(call_lib, argv, modifiers):
1cadf2
-    if len(argv) < 1:
1cadf2
+    if not argv:
1cadf2
         raise CmdLineInputError()
1cadf2
 
1cadf2
     bundle_id = argv[0]
1cadf2
-    parts = parse_bundle_create_options(argv[1:])
1cadf2
-    call_lib(
1cadf2
+    parts = parse_bundle_reset_options(argv[1:])
1cadf2
+    lib.resource.bundle_reset(
1cadf2
         bundle_id,
1cadf2
-        parts["container_type"],
1cadf2
         container_options=parts["container"],
1cadf2
         network_options=parts["network"],
1cadf2
         port_map=parts["port_map"],
1cadf2
diff --git a/pcs/test/cib_resource/test_bundle.py b/pcs/test/cib_resource/test_bundle.py
1cadf2
index 708de645..d5ce702a 100644
1cadf2
--- a/pcs/test/cib_resource/test_bundle.py
1cadf2
+++ b/pcs/test/cib_resource/test_bundle.py
1cadf2
@@ -64,7 +64,7 @@ class BundleReset(BundleCreateCommon):
1cadf2
             "resource bundle create B2 container docker image=pcs:test"
1cadf2
         )
1cadf2
         self.assert_effect(
1cadf2
-            "resource bundle reset B1 container docker image=pcs:new",
1cadf2
+            "resource bundle reset B1 container image=pcs:new",
1cadf2
             """
1cadf2
                 <resources>
1cadf2
                     <bundle id="B1">
1cadf2
diff --git a/pcs/usage.py b/pcs/usage.py
1cadf2
index 80ba9168..4cdfc3ac 100644
1cadf2
--- a/pcs/usage.py
1cadf2
+++ b/pcs/usage.py
1cadf2
@@ -450,7 +450,7 @@ Commands:
1cadf2
         to start and then return 0 on success or 1 on error. If 'n' is not
1cadf2
         specified it defaults to 60 minutes.
1cadf2
 
1cadf2
-    bundle reset <bundle id> container <container type> [<container options>]
1cadf2
+    bundle reset <bundle id> [container <container options>]
1cadf2
             [network <network options>] [port-map <port options>]...
1cadf2
             [storage-map <storage options>]... [meta <meta options>]
1cadf2
             [--disabled] [--wait[=n]]
1cadf2
-- 
1cadf2
2.21.0
1cadf2