# Copyright (c) 2015 b<>com
#
# Authors: Jean-Emile DARTOIS <jean-emile.dartois@b-com.com>
#
# 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 threading

from unittest import mock

from oslo_utils import uuidutils

from watcher.common import exception
from watcher.decision_engine.model import element
from watcher.decision_engine.model import model_root
from watcher.tests.unit import base
from watcher.tests.unit.decision_engine.model import faker_cluster_state


class TestModel(base.TestCase):
    def load_data(self, filename):
        cwd = os.path.abspath(os.path.dirname(__file__))
        data_folder = os.path.join(cwd, "data")

        with open(os.path.join(data_folder, filename), 'rb') as xml_file:
            xml_data = xml_file.read()

        return xml_data

    def load_model(self, filename):
        return model_root.ModelRoot.from_xml(self.load_data(filename))

    def test_model_structure(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()
        model1 = fake_cluster.build_scenario_1()

        self.assertEqual(5, len(model1.get_all_compute_nodes()))
        self.assertEqual(35, len(model1.get_all_instances()))
        self.assertEqual(8, len(model1.edges()))

        expected_struct_str = self.load_data('scenario_1.xml')
        model2 = model_root.ModelRoot.from_xml(expected_struct_str)

        self.assertTrue(model_root.ModelRoot.is_isomorphic(model2, model1))

    def test_build_model_from_xml(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()

        expected_model = fake_cluster.generate_scenario_1()
        struct_str = self.load_data('scenario_1.xml')

        model = model_root.ModelRoot.from_xml(struct_str)
        self.assertEqual(expected_model.to_string(), model.to_string())

    @mock.patch.object(model_root.ModelRoot, 'get_all_compute_nodes')
    @mock.patch.object(model_root.ModelRoot, 'get_node_instances')
    @mock.patch('watcher.decision_engine.model.model_root.LOG')
    def test_get_model_to_list(self, mock_log, mock_instances, mock_nodes):
        fake_compute_node = element.ComputeNode(
            uuid='fake_node_uuid', hostname="hostname0"
        )
        fake_instance = element.Instance(uuid='fake_instance_uuid')

        mock_nodes.return_value = {'fake_node_uuid': fake_compute_node}
        mock_instances.return_value = [fake_instance]

        expected_keys = [
            'server_uuid',
            'node_uuid',
            'node_state',
            'node_hostname',
            'node_status',
            'server_flavor_extra_specs',
            'server_locked',
            'server_pinned_az',
            'server_state',
            'server_watcher_exclude',
        ]

        result = model_root.ModelRoot().to_list()
        self.assertEqual(1, len(result))

        result_keys = result[0].keys()
        self.assertEqual(sorted(expected_keys), sorted(result_keys))

        mock_log.debug.assert_any_call(
            'Attribute %s for Instance: %s is not provided',
            'name',
            'fake_instance_uuid',
        )
        mock_log.debug.assert_any_call(
            'Attribute %s for Compute: %s is not provided',
            'memory',
            'hostname0',
        )

        # test compute node has no instance
        mock_instances.return_value = []

        expected_keys = [
            'node_hostname',
            'node_status',
            'node_state',
            'node_uuid',
        ]

        result = model_root.ModelRoot().to_list()
        self.assertEqual(1, len(result))

        result_keys = result[0].keys()
        self.assertEqual(expected_keys, list(result_keys))

    def test_model_to_list_all_fields(self):
        struct_str = self.load_data('scenario_1.xml')
        model = model_root.ModelRoot.from_xml(struct_str)
        model_list = model.to_list()
        self.assertIsInstance(model_list, list)
        self.assertEqual(len(model_list), 8)
        self.assertEqual(
            model_list[0]['server_uuid'],
            'd000ef1f-dc19-4982-9383-087498bfde03',
        )

    def test_model_to_xml_all_fields(self):
        struct_str = self.load_data('scenario_2_with_metrics.xml')

        model = model_root.ModelRoot.from_xml(struct_str)
        model_xml = model.to_xml()
        self.assertIsInstance(model_xml, str)
        self.assertIn('Instance', model_xml)
        self.assertIn('Compute', model_xml)

    def test_get_node_by_instance_uuid(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1, hostname="hostname_0")
        node.uuid = uuid_
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_uuid(uuid_))
        uuid_ = f"{uuidutils.generate_uuid()}"
        instance = element.Instance(id=1)
        instance.uuid = uuid_
        instance.host = "hostname_0"
        model.add_instance(instance)
        self.assertEqual(instance, model.get_instance_by_uuid(uuid_))
        model.map_instance(instance, node)
        self.assertEqual(node, model.get_node_by_instance_uuid(instance.uuid))
        mapped_instance = model.get_instance_by_uuid(uuid_)
        self.assertEqual(mapped_instance.host, "hostname_0")

    def test_add_node(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_uuid(uuid_))

    def test_delete_node(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_uuid(uuid_))
        model.remove_node(node)
        self.assertRaises(
            exception.ComputeNodeNotFound, model.get_node_by_uuid, uuid_
        )

    def test_get_all_compute_nodes(self):
        model = model_root.ModelRoot()
        for id_ in range(10):
            uuid_ = f"{uuidutils.generate_uuid()}"
            node = element.ComputeNode(id_)
            node.uuid = uuid_
            model.add_node(node)
        all_nodes = model.get_all_compute_nodes()
        for uuid_ in all_nodes:
            node = model.get_node_by_uuid(uuid_)
            model.assert_node(node)

    def test_set_get_state_nodes(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)

        self.assertIn(node.state, [el.value for el in element.ServiceState])

        node = model.get_node_by_uuid(uuid_)
        node.state = element.ServiceState.OFFLINE.value
        self.assertIn(node.state, [el.value for el in element.ServiceState])

    def test_get_node_by_name(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        name = 'test_node'
        node = element.ComputeNode()
        node.uuid = uuid_
        node.hostname = name
        model.add_node(node)
        compute_node = model.get_node_by_name(name)
        model.assert_node(compute_node)
        self.assertEqual(name, compute_node['hostname'])
        self.assertEqual(uuid_, compute_node['uuid'])

    def test_node_from_name_raise(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        name = 'test_node'
        node = element.ComputeNode()
        node.uuid = uuid_
        node.hostname = name
        model.add_node(node)

        fake_name = 'fake_node'
        self.assertRaises(
            exception.ComputeNodeNotFound, model.get_node_by_name, fake_name
        )

    def test_node_from_uuid_raise(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)

        uuid2 = f"{uuidutils.generate_uuid()}"
        self.assertRaises(
            exception.ComputeNodeNotFound, model.get_node_by_uuid, uuid2
        )

    def test_remove_node_raise(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)

        uuid2 = f"{uuidutils.generate_uuid()}"
        node2 = element.ComputeNode(id=2)
        node2.uuid = uuid2

        self.assertRaises(
            exception.ComputeNodeNotFound, model.remove_node, node2
        )

    def test_assert_node_raise(self):
        model = model_root.ModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        node = element.ComputeNode(id=1)
        node.uuid = uuid_
        model.add_node(node)
        self.assertRaises(
            exception.IllegalArgumentException, model.assert_node, "objet_qcq"
        )

    def test_instance_from_uuid_raise(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()
        model = fake_cluster.generate_scenario_1()
        self.assertRaises(
            exception.InstanceNotFound,
            model.get_instance_by_uuid,
            "valeur_qcq",
        )

    def test_assert_instance_raise(self):
        model = model_root.ModelRoot()
        self.assertRaises(
            exception.IllegalArgumentException,
            model.assert_instance,
            "valeur_qcq",
        )

    def test_get_node_instances(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()
        model = fake_cluster.generate_scenario_1()
        node = element.ComputeNode(uuid="Node_0")
        instance0 = model.get_instance_by_uuid(
            "d000ef1f-dc19-4982-9383-087498bfde03"
        )
        instance1 = model.get_instance_by_uuid(
            "d010ef1f-dc19-4982-9383-087498bfde03"
        )
        instances = model.get_node_instances(node)

        self.assertEqual(2, len(instances))
        self.assertIn(instance0, instances)
        self.assertIn(instance1, instances)

    def test_get_node_used_resources(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()
        model = fake_cluster.generate_scenario_1()
        node = element.ComputeNode(uuid="Node_0")
        resources_used = model.get_node_used_resources(node)

        self.assertEqual(21, resources_used.get('vcpu'))
        self.assertEqual(5, resources_used.get('memory'))
        self.assertEqual(38, resources_used.get('disk'))

    def test_get_node_free_resources(self):
        fake_cluster = faker_cluster_state.FakerModelCollector()
        model = fake_cluster.generate_scenario_1()
        node = model.get_node_by_uuid("Node_0")
        resources_free = model.get_node_free_resources(node)

        self.assertEqual(19, resources_free.get('vcpu'))
        self.assertEqual(127, resources_free.get('memory'))
        self.assertEqual(212, resources_free.get('disk'))

    def _build_cache_test_model(self):
        model = model_root.ModelRoot()
        node_a = element.ComputeNode(
            uuid='node-a',
            hostname='host-a',
            vcpus=40,
            vcpu_reserved=0,
            vcpu_ratio=1,
            memory=4096,
            memory_mb_reserved=0,
            memory_ratio=1,
            disk=250,
            disk_gb_reserved=0,
            disk_ratio=1,
        )
        node_b = element.ComputeNode(
            uuid='node-b',
            hostname='host-b',
            vcpus=40,
            vcpu_reserved=0,
            vcpu_ratio=1,
            memory=4096,
            memory_mb_reserved=0,
            memory_ratio=1,
            disk=250,
            disk_gb_reserved=0,
            disk_ratio=1,
        )
        inst1 = element.Instance(uuid='inst-1', vcpus=4, memory=512, disk=10)
        inst2 = element.Instance(uuid='inst-2', vcpus=2, memory=256, disk=20)
        model.add_node(node_a)
        model.add_node(node_b)
        model.add_instance(inst1)
        model.add_instance(inst2)
        model.map_instance(inst1, node_a)
        model.map_instance(inst2, node_a)
        return model, node_a, node_b, inst1, inst2

    def test_resource_cache_map_instance(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        # test cache is empty
        self.assertEqual({}, model._node_resource_cache)
        # Populate cache for both nodes.
        used_a = model.get_node_used_resources(node_a)
        self.assertEqual(6, used_a['vcpu'])
        self.assertEqual(768, used_a['memory'])
        self.assertEqual(30, used_a['disk'])
        used_b = model.get_node_used_resources(node_b)
        self.assertEqual({"vcpu": 0, "memory": 0, "disk": 0}, used_b)
        # test cache is populated
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )

        # Map a new instance to node_b: cache must update.
        inst3 = element.Instance(uuid='inst-3', vcpus=8, memory=1024, disk=40)
        model.add_instance(inst3)
        model.map_instance(inst3, node_b)

        used_b = model.get_node_used_resources(node_b)
        self.assertEqual(8, used_b['vcpu'])
        self.assertEqual(1024, used_b['memory'])
        self.assertEqual(40, used_b['disk'])
        # test cache is updated
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 8, 'memory': 1024, 'disk': 40},
            },
            model._node_resource_cache,
        )

        # Validate that get_node_used_resources does not read the instances
        # when the cache is populated
        mock_get_node_instances = mock.patch.object(
            model, 'get_node_instances'
        )
        mock_get_node_instances.return_value = []
        with mock_get_node_instances:
            used_b = model.get_node_used_resources(node_b)
            self.assertEqual(8, used_b['vcpu'])
            self.assertEqual(1024, used_b['memory'])
            self.assertEqual(40, used_b['disk'])
            # It should report 0 resources usage with mocked get_node_instances
            # when the cache is invalidated
            model.invalidate_node_resource_cache(node_b)
            used_b = model.get_node_used_resources(node_b)
            self.assertEqual(0, used_b['vcpu'])
            self.assertEqual(0, used_b['memory'])
            self.assertEqual(0, used_b['disk'])

    def test_resource_cache_unmap_instance(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        model.get_node_used_resources(node_a)

        model.unmap_instance(inst1, node_a)

        used_a = model.get_node_used_resources(node_a)
        used_b = model.get_node_used_resources(node_b)
        self.assertEqual(2, used_a['vcpu'])
        self.assertEqual(256, used_a['memory'])
        self.assertEqual(20, used_a['disk'])
        self.assertEqual(0, used_b['vcpu'])
        self.assertEqual(0, used_b['memory'])
        self.assertEqual(0, used_b['disk'])
        # test cache is updated
        self.assertEqual(
            {
                'node-a': {'vcpu': 2, 'memory': 256, 'disk': 20},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )

    def test_resource_cache_migrate_instance(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        model.get_node_used_resources(node_a)
        model.get_node_used_resources(node_b)

        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )

        model.migrate_instance(inst1, node_a, node_b)

        used_a = model.get_node_used_resources(node_a)
        self.assertEqual(2, used_a['vcpu'])
        self.assertEqual(256, used_a['memory'])
        self.assertEqual(20, used_a['disk'])
        used_b = model.get_node_used_resources(node_b)
        self.assertEqual(4, used_b['vcpu'])
        self.assertEqual(512, used_b['memory'])
        self.assertEqual(10, used_b['disk'])
        # test cache is updated
        self.assertEqual(
            {
                'node-a': {'vcpu': 2, 'memory': 256, 'disk': 20},
                'node-b': {'vcpu': 4, 'memory': 512, 'disk': 10},
            },
            model._node_resource_cache,
        )

    def test_resource_cache_remove_instance(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        model.get_node_used_resources(node_a)
        model.get_node_used_resources(node_b)
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )

        model.remove_instance(inst1)

        used_a = model.get_node_used_resources(node_a)
        self.assertEqual(2, used_a['vcpu'])
        self.assertEqual(256, used_a['memory'])
        self.assertEqual(20, used_a['disk'])
        # test cache is updated
        self.assertEqual(
            {
                'node-a': {'vcpu': 2, 'memory': 256, 'disk': 20},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )

    def test_resource_cache_remove_node(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        model.get_node_used_resources(node_a)
        model.get_node_used_resources(node_b)
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )
        model.remove_node(node_b)

        # test cache is updated
        self.assertEqual(
            {'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30}},
            model._node_resource_cache,
        )
        used_a = model.get_node_used_resources(node_a)
        self.assertEqual(6, used_a['vcpu'])
        self.assertEqual(768, used_a['memory'])
        self.assertEqual(30, used_a['disk'])
        model.remove_node(node_a)
        self.assertEqual({}, model._node_resource_cache)

    def test_resource_cache_map_instance_idempotent(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        model.get_node_used_resources(node_a)
        model.get_node_used_resources(node_b)
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )
        # Calling map_instance again for an already-mapped instance
        # must not double-count its resources.
        model.map_instance(inst1, node_a)
        self.assertEqual(
            {
                'node-a': {'vcpu': 6, 'memory': 768, 'disk': 30},
                'node-b': {'vcpu': 0, 'memory': 0, 'disk': 0},
            },
            model._node_resource_cache,
        )
        used_a = model.get_node_used_resources(node_a)
        self.assertEqual(6, used_a['vcpu'])
        self.assertEqual(768, used_a['memory'])
        self.assertEqual(30, used_a['disk'])

    def test_resource_cache_invalidate_all(self):
        model, node_a, node_b, inst1, inst2 = self._build_cache_test_model()
        # Populate cache for both nodes.
        model.get_node_used_resources(node_a)
        model.migrate_instance(inst1, node_a, node_b)
        model.get_node_used_resources(node_b)

        self.assertEqual(
            {
                'node-a': {'vcpu': 2, 'memory': 256, 'disk': 20},
                'node-b': {'vcpu': 4, 'memory': 512, 'disk': 10},
            },
            model._node_resource_cache,
        )
        model.invalidate_resource_cache()
        self.assertEqual({}, model._node_resource_cache)

    def test_map_instance_with_string_uuids_no_deadlock(self):
        """map_instance with string UUIDs must complete within timeout.

        Fails with @lockutils.synchronized (non-reentrant Semaphore): the
        thread never sets `done` and done.wait() returns False, triggering
        the assertion. Passes with a reentrant threading.RLock.

        Related bug: https://bugs.launchpad.net/watcher/+bug/2152254
        """
        model = model_root.ModelRoot()
        node = element.ComputeNode(uuid='node-uuid-1', hostname='host1')
        instance = element.Instance(uuid='instance-uuid-1')
        model.add_node(node)
        model.add_instance(instance)
        done = threading.Event()

        def call():
            model.map_instance('instance-uuid-1', 'node-uuid-1')
            done.set()

        t = threading.Thread(target=call, daemon=True)
        t.start()
        self.assertTrue(
            done.wait(timeout=2),
            "map_instance deadlocked — lock acquired by map_instance is not "
            "reentrant, blocking the internal get_instance_by_uuid and "
            "get_node_by_uuid calls on the same thread",
        )


class TestStorageModel(base.TestCase):
    def load_data(self, filename):
        cwd = os.path.abspath(os.path.dirname(__file__))
        data_folder = os.path.join(cwd, "data")

        with open(os.path.join(data_folder, filename), 'rb') as xml_file:
            xml_data = xml_file.read()

        return xml_data

    def load_model(self, filename):
        return model_root.StorageModelRoot.from_xml(self.load_data(filename))

    def test_model_structure(self):
        fake_cluster = faker_cluster_state.FakerStorageModelCollector()
        model1 = fake_cluster.build_scenario_1()

        self.assertEqual(2, len(model1.get_all_storage_nodes()))
        self.assertEqual(9, len(model1.get_all_volumes()))
        self.assertEqual(12, len(model1.edges()))

        expected_struct_str = self.load_data('storage_scenario_1.xml')
        model2 = model_root.StorageModelRoot.from_xml(expected_struct_str)
        self.assertTrue(
            model_root.StorageModelRoot.is_isomorphic(model2, model1)
        )

    def test_build_model_from_xml(self):
        fake_cluster = faker_cluster_state.FakerStorageModelCollector()

        expected_model = fake_cluster.generate_scenario_1()
        struct_str = self.load_data('storage_scenario_1.xml')

        model = model_root.StorageModelRoot.from_xml(struct_str)
        self.assertEqual(expected_model.to_string(), model.to_string())

    def test_assert_node_raise(self):
        model = model_root.StorageModelRoot()
        node = element.StorageNode(host="host@backend")
        model.add_node(node)
        self.assertRaises(
            exception.IllegalArgumentException, model.assert_node, "obj"
        )

    def test_assert_pool_raise(self):
        model = model_root.StorageModelRoot()
        pool = element.Pool(name="host@backend#pool")
        model.add_pool(pool)
        self.assertRaises(
            exception.IllegalArgumentException, model.assert_pool, "obj"
        )

    def test_assert_volume_raise(self):
        model = model_root.StorageModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertRaises(
            exception.IllegalArgumentException, model.assert_volume, "obj"
        )

    def test_add_node(self):
        model = model_root.StorageModelRoot()
        hostname = "host@backend"
        node = element.StorageNode(host=hostname)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_name(hostname))

    def test_add_pool(self):
        model = model_root.StorageModelRoot()
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))

    def test_remove_node(self):
        model = model_root.StorageModelRoot()
        hostname = "host@backend"
        node = element.StorageNode(host=hostname)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_name(hostname))
        model.remove_node(node)
        self.assertRaises(
            exception.StorageNodeNotFound, model.get_node_by_name, hostname
        )

    def test_remove_pool(self):
        model = model_root.StorageModelRoot()
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        model.remove_pool(pool)
        self.assertRaises(
            exception.PoolNotFound, model.get_pool_by_pool_name, pool_name
        )

    def test_map_unmap_pool(self):
        model = model_root.StorageModelRoot()
        hostname = "host@backend"
        node = element.StorageNode(host=hostname)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_name(hostname))
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        model.map_pool(pool, node)
        self.assertTrue(pool.name in model.predecessors(node.host))
        model.unmap_pool(pool, node)
        self.assertFalse(pool.name in model.predecessors(node.host))

    def test_add_volume(self):
        model = model_root.StorageModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertEqual(volume, model.get_volume_by_uuid(uuid_))

    def test_remove_volume(self):
        model = model_root.StorageModelRoot()
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertEqual(volume, model.get_volume_by_uuid(uuid_))
        model.remove_volume(volume)
        self.assertRaises(
            exception.VolumeNotFound, model.get_volume_by_uuid, uuid_
        )

    def test_map_unmap_volume(self):
        model = model_root.StorageModelRoot()
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertEqual(volume, model.get_volume_by_uuid(uuid_))
        model.map_volume(volume, pool)
        self.assertTrue(volume.uuid in model.predecessors(pool.name))
        model.unmap_volume(volume, pool)
        self.assertFalse(volume.uuid in model.predecessors(pool.name))

    def test_get_all_storage_nodes(self):
        model = model_root.StorageModelRoot()
        for i in range(10):
            hostname = f"host_{i}"
            node = element.StorageNode(host=hostname)
            model.add_node(node)
        all_nodes = model.get_all_storage_nodes()
        for hostname in all_nodes:
            node = model.get_node_by_name(hostname)
            model.assert_node(node)

    def test_get_all_volumes(self):
        model = model_root.StorageModelRoot()
        for id_ in range(10):
            uuid_ = f"{uuidutils.generate_uuid()}"
            volume = element.Volume(uuid=uuid_)
            model.add_volume(volume)
        all_volumes = model.get_all_volumes()
        for vol in all_volumes:
            volume = model.get_volume_by_uuid(vol)
            model.assert_volume(volume)

    def test_get_node_pools(self):
        model = model_root.StorageModelRoot()
        hostname = "host@backend"
        node = element.StorageNode(host=hostname)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_name(hostname))
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        model.map_pool(pool, node)
        self.assertEqual([pool], model.get_node_pools(node))

    def test_get_pool_by_volume(self):
        model = model_root.StorageModelRoot()
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertEqual(volume, model.get_volume_by_uuid(uuid_))
        model.map_volume(volume, pool)
        self.assertEqual(pool, model.get_pool_by_volume(volume))

    def test_get_pool_volumes(self):
        model = model_root.StorageModelRoot()
        pool_name = "host@backend#pool"
        pool = element.Pool(name=pool_name)
        model.add_pool(pool)
        self.assertEqual(pool, model.get_pool_by_pool_name(pool_name))
        uuid_ = f"{uuidutils.generate_uuid()}"
        volume = element.Volume(uuid=uuid_)
        model.add_volume(volume)
        self.assertEqual(volume, model.get_volume_by_uuid(uuid_))
        model.map_volume(volume, pool)
        self.assertEqual([volume], model.get_pool_volumes(pool))


class TestBaremetalModel(base.TestCase):
    def load_data(self, filename):
        cwd = os.path.abspath(os.path.dirname(__file__))
        data_folder = os.path.join(cwd, "data")

        with open(os.path.join(data_folder, filename), 'rb') as xml_file:
            xml_data = xml_file.read()

        return xml_data

    def load_model(self, filename):
        return model_root.StorageModelRoot.from_xml(self.load_data(filename))

    def test_model_structure(self):
        fake_cluster = faker_cluster_state.FakerBaremetalModelCollector()
        model1 = fake_cluster.build_scenario_1()
        self.assertEqual(2, len(model1.get_all_ironic_nodes()))

        expected_struct_str = self.load_data('ironic_scenario_1.xml')
        model2 = model_root.BaremetalModelRoot.from_xml(expected_struct_str)
        self.assertTrue(
            model_root.BaremetalModelRoot.is_isomorphic(model2, model1)
        )

    def test_build_model_from_xml(self):
        fake_cluster = faker_cluster_state.FakerBaremetalModelCollector()

        expected_model = fake_cluster.generate_scenario_1()
        struct_str = self.load_data('ironic_scenario_1.xml')

        model = model_root.BaremetalModelRoot.from_xml(struct_str)
        self.assertEqual(expected_model.to_string(), model.to_string())

    def test_assert_node_raise(self):
        model = model_root.BaremetalModelRoot()
        node_uuid = uuidutils.generate_uuid()
        node = element.IronicNode(uuid=node_uuid)
        model.add_node(node)
        self.assertRaises(
            exception.IllegalArgumentException, model.assert_node, "obj"
        )

    def test_add_node(self):
        model = model_root.BaremetalModelRoot()
        node_uuid = uuidutils.generate_uuid()
        node = element.IronicNode(uuid=node_uuid)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_uuid(node_uuid))

    def test_remove_node(self):
        model = model_root.BaremetalModelRoot()
        node_uuid = uuidutils.generate_uuid()
        node = element.IronicNode(uuid=node_uuid)
        model.add_node(node)
        self.assertEqual(node, model.get_node_by_uuid(node_uuid))
        model.remove_node(node)
        self.assertRaises(
            exception.IronicNodeNotFound, model.get_node_by_uuid, node_uuid
        )

    def test_get_all_ironic_nodes(self):
        model = model_root.BaremetalModelRoot()
        for i in range(10):
            node_uuid = uuidutils.generate_uuid()
            node = element.IronicNode(uuid=node_uuid)
            model.add_node(node)
        all_nodes = model.get_all_ironic_nodes()
        for node_uuid in all_nodes:
            node = model.get_node_by_uuid(node_uuid)
            model.assert_node(node)
