Add designate-manage pool scenario tests

So far we did not test designate-manage pool commands with real end-user
scenarios.

This patch adds designate-manage pool scenario tests.

Change-Id: Iaf848350a502b8f1697b495ee645a09a00974f24
diff --git a/designate_tempest_plugin/config.py b/designate_tempest_plugin/config.py
index 4f880e2..2a8e25e 100644
--- a/designate_tempest_plugin/config.py
+++ b/designate_tempest_plugin/config.py
@@ -98,6 +98,11 @@
                      'the new keystone default roles? This configuration '
                      'value should be same as designate.conf: '
                      '[oslo_policy].enforce_new_defaults option.'),
+    cfg.BoolOpt('test_multipool_with_delete_opt',
+                default=False,
+                help="Is multipool feature being tested with --delete option?"
+                     "If it is, it might delete pools that were created in "
+                     "other tests."),
 ]
 
 # Extending this enforce_scope group defined in tempest
diff --git a/designate_tempest_plugin/tests/api/v2/test_pool.py b/designate_tempest_plugin/tests/api/v2/test_pool.py
index 0e7753e..ba19623 100644
--- a/designate_tempest_plugin/tests/api/v2/test_pool.py
+++ b/designate_tempest_plugin/tests/api/v2/test_pool.py
@@ -14,6 +14,7 @@
 
 from operator import itemgetter
 
+import testtools
 from oslo_log import log as logging
 from tempest import config
 from tempest.lib import decorators
@@ -51,6 +52,10 @@
         cls.admin_client = cls.os_admin.dns_v2.PoolClient()
 
     @decorators.idempotent_id('69257f7c-b3d5-4e1b-998e-0677ad12f125')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_create_pool(self):
         pool_data = {
                       "name": "Example Pool",
@@ -79,6 +84,10 @@
             project_id=pool_data["project_id"])
 
     @decorators.idempotent_id('e80eb70a-8ee5-40eb-b06e-599597a8ab7e')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_show_pool(self):
         LOG.info('Create a pool')
         _, pool = self.admin_client.create_pool(project_id="1")
@@ -106,6 +115,10 @@
             headers=self.all_projects_header)
 
     @decorators.idempotent_id('d8c4c377-5d88-452d-a4d2-c004d72e1abe')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_delete_pool(self):
         LOG.info('Create a pool')
         _, pool = self.admin_client.create_pool(project_id="1")
@@ -128,6 +141,10 @@
             'PoolClient', 'delete_pool', expected_allowed, False, pool['id'])
 
     @decorators.idempotent_id('77c85b40-83b2-4c17-9fbf-e6d516cfce90')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_list_pools(self):
         LOG.info('Create a pool')
         _, pool = self.admin_client.create_pool(project_id="1")
@@ -150,6 +167,10 @@
             headers=self.all_projects_header)
 
     @decorators.idempotent_id('fdcc84ce-af65-4af6-a5fc-6c50acbea0f0')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_update_pool(self):
         LOG.info('Create a pool')
         _, pool = self.admin_client.create_pool(project_id="1")
@@ -302,6 +323,10 @@
                 ns_records=[{"hostname": "ns1.example.org.", "priority": -1}])
 
     @decorators.idempotent_id('cc378e4c-ac05-11eb-ae06-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     # Note: Update pool API is deprecated for removal.
     def test_update_pool_with_invalid_name(self):
         LOG.info('Create a pool')
@@ -317,6 +342,10 @@
                 headers=self.all_projects_header, extra_headers=True)
 
     @decorators.idempotent_id('2e496596-ac07-11eb-ae06-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_update_pool_with_invalid_hostname_in_ns_records(self):
         # Note: Update pool API is deprecated for removal.
         LOG.info('Create a pool')
@@ -332,6 +361,10 @@
                 headers=self.all_projects_header, extra_headers=True)
 
     @decorators.idempotent_id('3e934624-ac07-11eb-ae06-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_update_pool_with_invalid_priority_in_ns_records(self):
         # Note: Update pool API is deprecated for removal.
         LOG.info('Create a pool')
diff --git a/designate_tempest_plugin/tests/api/v2/test_tsigkey.py b/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
index db92ca3..9522395 100644
--- a/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
+++ b/designate_tempest_plugin/tests/api/v2/test_tsigkey.py
@@ -11,7 +11,7 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-
+import testtools
 from oslo_log import log as logging
 from tempest import config
 from tempest.lib.common.utils import data_utils
@@ -300,6 +300,10 @@
             0, len(listed_tsigkeys), 'Failed, no tsigkey should be listed')
 
     @decorators.idempotent_id('e8bcf80a-d8b4-11eb-b95a-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_list_tsigkey_filter_by_scope(self):
 
         LOG.info('Create tsigkey for a pool')
@@ -348,6 +352,10 @@
             'Failed, no tsigkey is expected to be listed')
 
     @decorators.idempotent_id('794554f0-d8b8-11eb-b95a-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     def test_list_tsigkey_filter_by_algorithm(self):
 
         LOG.info('Create tsigkey for a pool')
@@ -658,6 +666,10 @@
             tsigkey_data['secret'], tsigkey_data['scope'])
 
     @decorators.idempotent_id('0dfbc2f8-d8bb-11eb-b95a-74e5f9e2a801')
+    @testtools.skipIf(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
     @decorators.skip_because(bug="1934120")
     def test_create_tsigkey_for_pool_with_scope_zone(self):
         pool = self.pool_admin_client.create_pool()[1]
diff --git a/designate_tempest_plugin/tests/resources/__init__.py b/designate_tempest_plugin/tests/resources/__init__.py
new file mode 100644
index 0000000..f4f5c82
--- /dev/null
+++ b/designate_tempest_plugin/tests/resources/__init__.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2024 Red Hat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import os
+
+# The idea here is that anything that needs resources can do:
+#
+# from designate.tests import resources
+# my_resource_path = os.path.join(resources.path, my_resource_folder)
+path = os.path.dirname(os.path.realpath(__file__))
diff --git a/designate_tempest_plugin/tests/resources/pools_yaml/multiple-pools.yaml b/designate_tempest_plugin/tests/resources/pools_yaml/multiple-pools.yaml
new file mode 100644
index 0000000..2fe047d
--- /dev/null
+++ b/designate_tempest_plugin/tests/resources/pools_yaml/multiple-pools.yaml
@@ -0,0 +1,59 @@
+---
+- name: pool-2
+  id: cf2e8eab-76cd-4162-bf76-8aeee3556de0
+  description: BIND9 Pool-2
+  attributes:
+    internal: true
+  ns_records:
+    - hostname: ns1-1.example.org.
+      priority: 1
+    - hostname: ns1-2.example.org.
+      priority: 2
+  nameservers:
+    - host: 192.0.2.2
+      port: 1053
+    # - host: 192.0.2.3
+    #   port: 53
+  targets:
+    - type: bind9
+      description: BIND9 Server 1
+      masters:
+        - host: 192.0.2.1
+          port: 5354
+      options:
+        host: 192.0.2.2
+        port: 1053
+        rndc_host: 192.0.2.2
+        rndc_port: 1953
+        rndc_key_file: /etc/designate/rndc.key
+        tsigkey_name: multiple-pools-pool-2
+
+- name: default
+  description: Default BIND9 Pool
+
+  attributes: {}
+
+  ns_records:
+    - hostname: ns1-1.example.org.
+      priority: 1
+    - hostname: ns1-2.example.org.
+      priority: 2
+
+  nameservers:
+    - host: 192.0.2.2
+      port: 53
+    # - host: 192.0.2.3
+    #   port: 53
+
+  targets:
+    - type: bind9
+      description: Default BIND9 Server
+      masters:
+        - host: 192.0.2.1
+          port: 5354
+      options:
+        host: 192.0.2.2
+        port: 53
+        rndc_host: 192.0.2.2
+        rndc_port: 953
+        rndc_key_file: /etc/designate/rndc.key
diff --git a/designate_tempest_plugin/tests/resources/pools_yaml/other-pools.yaml b/designate_tempest_plugin/tests/resources/pools_yaml/other-pools.yaml
new file mode 100644
index 0000000..5e931d2
--- /dev/null
+++ b/designate_tempest_plugin/tests/resources/pools_yaml/other-pools.yaml
@@ -0,0 +1,61 @@
+---
+- name: other_pool2
+  id: f2e9d8c7-6b5a-4f3e-8d2c-1b7a9e4d3f2e
+  description: The first BIND9 pool of the other multipools file
+  attributes:
+    type: internal
+
+  ns_records:
+    - hostname: ns1-1.example.org.
+      priority: 1
+    - hostname: ns1-2.example.org.
+      priority: 2
+
+  nameservers:
+    - host: 192.0.2.2
+      port: 1053
+    # - host: 192.0.2.3
+    #   port: 53
+
+  targets:
+    - type: bind9
+      description: BIND9 Server 1
+      masters:
+        - host: 192.0.2.1
+          port: 5354
+      options:
+        host: 192.0.2.2
+        port: 1053
+        rndc_host: 192.0.2.2
+        rndc_port: 1953
+        rndc_key_file: /etc/designate/rndc.key
+        tsigkey_name: other-pools-pool-2
+
+- name: default
+  description: Default BIND9 Pool
+  attributes: {}
+
+  ns_records:
+    - hostname: ns1-1.example.org.
+      priority: 1
+    - hostname: ns1-2.example.org.
+      priority: 2
+
+  nameservers:
+    - host: 192.0.2.2
+      port: 53
+    # - host: 192.0.2.3
+    #   port: 53
+
+  targets:
+    - type: bind9
+      description: Default BIND9 Server
+      masters:
+        - host: 192.0.2.1
+          port: 5354
+      options:
+        host: 192.0.2.2
+        port: 53
+        rndc_host: 192.0.2.2
+        rndc_port: 953
+        rndc_key_file: /etc/designate/rndc.key
\ No newline at end of file
diff --git a/designate_tempest_plugin/tests/scenario/v2/test_designate_multipool.py b/designate_tempest_plugin/tests/scenario/v2/test_designate_multipool.py
new file mode 100644
index 0000000..c38ef28
--- /dev/null
+++ b/designate_tempest_plugin/tests/scenario/v2/test_designate_multipool.py
@@ -0,0 +1,246 @@
+# Copyright 2024 Red Hat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import os
+import random
+
+import testtools
+import yaml
+from oslo_log import log as logging
+from tempest import config
+import subprocess
+
+from tempest.lib import decorators
+
+from designate_tempest_plugin.tests import base
+from designate_tempest_plugin.tests import resources
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class DesignateManageTest(base.BaseDnsV2Test):
+    credentials = ["admin", 'primary', 'system_admin']
+    managed_resource = None
+
+    @classmethod
+    def skip_checks(cls) -> None:
+        super().skip_checks()
+        if CONF.dns_feature_enabled.designate_manage_path:
+            cls.designate_manage_cmd = (
+                CONF.dns_feature_enabled.designate_manage_path)
+        else:
+            raise cls.skipException('designate-manage path was not found. '
+                                    'Skipping this test class')
+
+    @classmethod
+    def _run_designate_manage_command(cls,
+                                      managed_resource: str,
+                                      command: str,
+                                      *args) -> str:
+        """Runs the designate-manage command with the provided arguments.
+
+        :param managed_resource: (str): The resource managed by the
+         designate-manage command. For example: pool
+        :param command: (str): The command to run. For example: update
+        :return: The command output
+        """
+        managed_resource = managed_resource or cls.managed_resource
+
+        commands_list = [cls.designate_manage_cmd, managed_resource]
+        if command and isinstance(command, tuple):
+            commands_list.extend(command)
+        else:
+            commands_list.append(command)
+        if args and isinstance(args, tuple):
+            commands_list.extend(args[0])
+        try:
+            output = subprocess.check_output(commands_list,
+                    stderr=subprocess.STDOUT,
+                    text=True)
+            return output
+        except subprocess.CalledProcessError as e:
+            LOG.error(e.output)
+
+
+def _get_pools_path(name: str) -> str:
+    return os.path.join(resources.path, 'pools_yaml', name)
+
+
+@testtools.skipUnless(CONF.dns_feature_enabled.test_multipool_with_delete_opt,
+                      'Multipools feature is being tested with --delete '
+                      'option. It might delete pools that were created in '
+                      'other tests.')
+class DesignateManagePoolTest(DesignateManageTest):
+    managed_resource = 'pool'
+    file_attributes_to_num_of_appearances = {  # per each Pool
+        'also_notifies:': 1, 'attributes:': 1, 'description:': 2, 'id:': 1,
+        'name:': 2, 'nameservers:': 1, 'ns_records:': 1, 'targets:': 1
+    }
+    MULTIPOOLS_FILE_PATH = "/etc/designate/multiple-pools.yaml"
+
+    @classmethod
+    def resource_setup(cls):
+        testtools.skipUnless(os.path.exists(cls.MULTIPOOLS_FILE_PATH),
+        f"multiple-pools configuration file {cls.MULTIPOOLS_FILE_PATH} was "
+        "not found, skipping the test")
+        cls._update_pools_file(cls.MULTIPOOLS_FILE_PATH)
+
+    @classmethod
+    def _update_pools_file(cls, pools_file_path):
+        if not pools_file_path.startswith('/'):
+            pools_file_path = _get_pools_path(name=pools_file_path)
+        cls._run_designate_manage_pool_command(
+            'update',
+            '--file',
+            pools_file_path,
+            '--delete')
+
+    def tearDown(self):
+        super(DesignateManagePoolTest, self).tearDown()
+        self._update_pools_file(self.MULTIPOOLS_FILE_PATH)
+
+    @staticmethod
+    def _load_config(filename) -> str:
+        with open(filename) as stream:
+            return yaml.safe_load(stream)
+
+    @classmethod
+    def _run_designate_manage_pool_command(cls, command: str, *args) -> str:
+        return super(
+            DesignateManagePoolTest, cls)._run_designate_manage_command(
+            'pool', command, args)
+
+    @decorators.idempotent_id('ed42f367-e5ba-40d7-a08d-366ad787d21d')
+    def test_pool_show_config(self):
+        self._update_pools_file(self.MULTIPOOLS_FILE_PATH)
+        pool_config = self._run_designate_manage_pool_command(
+            command='show_config').split('\n\n')[0]
+        self.assertIn('BIND Pool', pool_config)
+
+    @decorators.idempotent_id('ed42f367-e5ba-40d7-a08d-366ad787d21e')
+    def test_pool_show_config_all(self):
+        self._update_pools_file(pools_file_path='multiple-pools.yaml')
+        pool_config = self._run_designate_manage_pool_command(
+            'show_config', '--all').split('\n\n')[0]
+
+        pool_config_list = pool_config.split('\n')
+
+        for attribute in self.file_attributes_to_num_of_appearances:
+            num_of_occurrences = sum(attribute in s for s in pool_config_list)
+            file_attributes_to_num_of_appearances = {
+                'also_notifies:': 2, 'attributes:': 2, 'description:': 4,
+                'id:': 2, 'name:': 7, 'nameservers:': 2, 'ns_records:': 2,
+                'targets:': 2
+            }
+            err_msg = (f'{attribute} was supposed to appear '
+                       f'{file_attributes_to_num_of_appearances[attribute]} '
+                       'times on the designate-manage output, but '
+                       f'it appeared {num_of_occurrences} times.')
+            self.assertEqual(
+                file_attributes_to_num_of_appearances[attribute],
+                num_of_occurrences, err_msg)
+
+    @decorators.idempotent_id('ed42f367-e5ba-40d7-a08d-366ad787d220')
+    def test_pool_update_multiple_pools_without_delete(self):
+
+        # Updating to multiple-pools.yaml with --delete
+        self._update_pools_file(pools_file_path='multiple-pools.yaml')
+
+        # Updating to other-pools.yaml without --delete. There should be 5
+        # pools after the update
+        pools_yaml_path = _get_pools_path(name='other-pools.yaml')
+        self._run_designate_manage_pool_command(
+            'update',
+            '--file',
+            pools_yaml_path,
+        )
+
+        pool_config = self._run_designate_manage_pool_command(
+            'show_config', '--all').split('\n\n')[0]
+
+        pool_config_list = pool_config.split('\n')
+        for attribute in self.file_attributes_to_num_of_appearances:
+            num_of_occurrences = sum(attribute in s for s in pool_config_list)
+            file_attributes_to_num_of_appearances = {
+                'also_notifies:': 3, 'attributes:': 3, 'description:': 6,
+                'id:': 3, 'name:': 11, 'nameservers:': 3, 'ns_records:': 3,
+                'targets:': 3
+            }
+            err_msg = (f'{attribute} was supposed to appear '
+                       f'{file_attributes_to_num_of_appearances[attribute]} '
+                       'times on the designate-manage output, but '
+                       f'it appeared {num_of_occurrences} times.')
+            self.assertEqual(
+                file_attributes_to_num_of_appearances[attribute],
+                num_of_occurrences, err_msg)
+
+    @decorators.idempotent_id('ed42f367-e5ba-40d7-a08d-366ad787d223')
+    def test_pool_update_multiple_pools_dry_run(self):
+        self._update_pools_file(pools_file_path='multiple-pools.yaml')
+
+        pools_yaml_path = _get_pools_path(name='other-pools.yaml')
+        self._run_designate_manage_pool_command(
+            'update',
+            '--file',
+            pools_yaml_path,
+            '--dry-run'
+        )
+
+        pool_config = self._run_designate_manage_pool_command(
+            'show_config', '--all').split('\n\n')[0]
+
+        pool_config_list = pool_config.split('\n')
+
+        for attribute in self.file_attributes_to_num_of_appearances:
+            num_of_occurrences = sum(attribute in s for s in pool_config_list)
+            file_attributes_to_num_of_appearances = {
+                'also_notifies:': 2, 'attributes:': 2, 'description:': 4,
+                'id:': 2, 'name:': 7, 'nameservers:': 2, 'ns_records:': 2,
+                'targets:': 2
+            }
+            err_msg = (f'{attribute} was supposed to appear '
+                       f'{file_attributes_to_num_of_appearances[attribute]} '
+                       'times on the designate-manage output, but '
+                       f'it appeared {num_of_occurrences} times.')
+            self.assertEqual(
+                file_attributes_to_num_of_appearances[attribute],
+                num_of_occurrences, err_msg)
+
+    @decorators.idempotent_id('ed42f367-e5ba-40d7-a08d-366ad787d224')
+    def test_pool_generate_file(self):
+        temp_pools_yaml_conf_path = '/tmp/pools_tempest.yaml'
+        if os.path.exists(temp_pools_yaml_conf_path):
+            LOG.debug(f'Temporary pools.yaml file {temp_pools_yaml_conf_path} '
+                      'exists.\nRemoving this file so we could continue '
+                      'testing')
+            os.remove(temp_pools_yaml_conf_path)
+        self._run_designate_manage_pool_command('generate_file',
+                                                '--file',
+                                                temp_pools_yaml_conf_path)
+        self.assertTrue(os.path.exists(path=temp_pools_yaml_conf_path))
+        # (At least) the default pool config should be written to the file
+        pools_conf = self._load_config(filename=temp_pools_yaml_conf_path)
+        if len(pools_conf) > 1:
+            pool_idx = random.randint(0, len(pools_conf) - 1)
+        else:
+            pool_idx = 0
+        pool = yaml.safe_dump(pools_conf[pool_idx]).split('\n')
+
+        for attribute in self.file_attributes_to_num_of_appearances:
+            self.assertTrue(
+                any(attribute in s for s in pool),
+                f'{attribute} not in {pool}'
+            )
+        if os.path.exists(temp_pools_yaml_conf_path):
+            os.remove(temp_pools_yaml_conf_path)