diff --git a/watcher/decision_engine/model/element/instance.py b/watcher/decision_engine/model/element/instance.py index 451998356..ebdb16d7b 100644 --- a/watcher/decision_engine/model/element/instance.py +++ b/watcher/decision_engine/model/element/instance.py @@ -48,7 +48,7 @@ class Instance(compute_resource.ComputeResource): "disk": wfields.IntegerField(), "disk_capacity": wfields.NonNegativeIntegerField(), "vcpus": wfields.NonNegativeIntegerField(), - "metadata": wfields.DictField(), + "metadata": wfields.JsonField(), } def accept(self, visitor): diff --git a/watcher/decision_engine/model/model_root.py b/watcher/decision_engine/model/model_root.py index c1843bb90..4daeff5ee 100644 --- a/watcher/decision_engine/model/model_root.py +++ b/watcher/decision_engine/model/model_root.py @@ -235,3 +235,10 @@ class ModelRoot(nx.DiGraph, base.Model): model.add_instance(instance) return model + + @classmethod + def is_isomorphic(cls, G1, G2): + def node_match(node1, node2): + return node1.as_dict() == node2.as_dict() + return nx.algorithms.isomorphism.isomorph.is_isomorphic( + G1, G2, node_match=node_match) diff --git a/watcher/decision_engine/strategy/strategies/base.py b/watcher/decision_engine/strategy/strategies/base.py index 13c241032..effea1e5f 100644 --- a/watcher/decision_engine/strategy/strategies/base.py +++ b/watcher/decision_engine/strategy/strategies/base.py @@ -39,6 +39,8 @@ which are dynamically loaded by Watcher at launch time. import abc import six +from oslo_utils import strutils + from watcher.common import clients from watcher.common import context from watcher.common import exception @@ -264,6 +266,22 @@ class BaseStrategy(loadable.Loadable): def state_collector(self, s): self._cluster_state_collector = s + def filter_instances_by_audit_tag(self, instances): + if not self.config.check_optimize_metadata: + return instances + instances_to_migrate = [] + for instance in instances: + optimize = True + if instance.metadata: + try: + optimize = strutils.bool_from_string( + instance.metadata.get('optimize')) + except ValueError: + optimize = False + if optimize: + instances_to_migrate.append(instance) + return instances_to_migrate + @six.add_metaclass(abc.ABCMeta) class DummyBaseStrategy(BaseStrategy): diff --git a/watcher/decision_engine/strategy/strategies/basic_consolidation.py b/watcher/decision_engine/strategy/strategies/basic_consolidation.py index 2bb776510..6d9067ff6 100644 --- a/watcher/decision_engine/strategy/strategies/basic_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/basic_consolidation.py @@ -159,7 +159,12 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): "datasource", help="Data source to use in order to query the needed metrics", default="ceilometer", - choices=["ceilometer", "monasca", "gnocchi"]) + choices=["ceilometer", "monasca", "gnocchi"]), + cfg.BoolOpt( + "check_optimize_metadata", + help="Check optimize metadata field in instance before " + "migration", + default=False), ] @property @@ -437,9 +442,10 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): def node_and_instance_score(self, sorted_scores): """Get List of VMs from node""" node_to_release = sorted_scores[len(sorted_scores) - 1][0] - instances_to_migrate = self.compute_model.get_node_instances( + instances = self.compute_model.get_node_instances( self.compute_model.get_node_by_uuid(node_to_release)) + instances_to_migrate = self.filter_instances_by_audit_tag(instances) instance_score = [] for instance in instances_to_migrate: if instance.state == element.InstanceState.ACTIVE.value: diff --git a/watcher/objects/fields.py b/watcher/objects/fields.py index e05c7b870..d0df8548a 100644 --- a/watcher/objects/fields.py +++ b/watcher/objects/fields.py @@ -17,6 +17,7 @@ import ast import six +from oslo_serialization import jsonutils from oslo_versionedobjects import fields @@ -96,8 +97,26 @@ class FlexibleListOfDictField(fields.AutoTypedField): super(FlexibleListOfDictField, self)._null(obj, attr) +class Json(fields.FieldType): + def coerce(self, obj, attr, value): + if isinstance(value, six.string_types): + loaded = jsonutils.loads(value) + return loaded + return value + + def from_primitive(self, obj, attr, value): + return self.coerce(obj, attr, value) + + def to_primitive(self, obj, attr, value): + return jsonutils.dumps(value) + + +class JsonField(fields.AutoTypedField): + AUTO_TYPE = Json() + # ### Notification fields ### # + class BaseWatcherEnum(Enum): ALL = () diff --git a/watcher/tests/decision_engine/model/data/scenario_1.xml b/watcher/tests/decision_engine/model/data/scenario_1.xml index dcf08626d..7476af277 100644 --- a/watcher/tests/decision_engine/model/data/scenario_1.xml +++ b/watcher/tests/decision_engine/model/data/scenario_1.xml @@ -1,47 +1,47 @@ - - + + - + - - - + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/watcher/tests/decision_engine/model/data/scenario_1_with_metrics.xml b/watcher/tests/decision_engine/model/data/scenario_1_with_metrics.xml index 3edf6d17e..d8b80afca 100644 --- a/watcher/tests/decision_engine/model/data/scenario_1_with_metrics.xml +++ b/watcher/tests/decision_engine/model/data/scenario_1_with_metrics.xml @@ -1,8 +1,8 @@ - + - + diff --git a/watcher/tests/decision_engine/model/data/scenario_2_with_metrics.xml b/watcher/tests/decision_engine/model/data/scenario_2_with_metrics.xml index 6edba3e7d..668ef1985 100644 --- a/watcher/tests/decision_engine/model/data/scenario_2_with_metrics.xml +++ b/watcher/tests/decision_engine/model/data/scenario_2_with_metrics.xml @@ -1,11 +1,11 @@ - - - - - - + + + + + + diff --git a/watcher/tests/decision_engine/model/data/scenario_3_with_2_nodes.xml b/watcher/tests/decision_engine/model/data/scenario_3_with_2_nodes.xml index fee3a27eb..189d81ee9 100644 --- a/watcher/tests/decision_engine/model/data/scenario_3_with_2_nodes.xml +++ b/watcher/tests/decision_engine/model/data/scenario_3_with_2_nodes.xml @@ -1,8 +1,8 @@ - + - + diff --git a/watcher/tests/decision_engine/model/data/scenario_3_with_metrics.xml b/watcher/tests/decision_engine/model/data/scenario_3_with_metrics.xml index b8987ffd5..5c6079916 100644 --- a/watcher/tests/decision_engine/model/data/scenario_3_with_metrics.xml +++ b/watcher/tests/decision_engine/model/data/scenario_3_with_metrics.xml @@ -1,9 +1,9 @@ - - - - + + + + diff --git a/watcher/tests/decision_engine/model/data/scenario_5_with_instance_disk_0.xml b/watcher/tests/decision_engine/model/data/scenario_5_with_instance_disk_0.xml index 72caedbbb..a8bffab30 100644 --- a/watcher/tests/decision_engine/model/data/scenario_5_with_instance_disk_0.xml +++ b/watcher/tests/decision_engine/model/data/scenario_5_with_instance_disk_0.xml @@ -1,5 +1,5 @@ - + diff --git a/watcher/tests/decision_engine/model/data/scenario_6_with_2_nodes.xml b/watcher/tests/decision_engine/model/data/scenario_6_with_2_nodes.xml index 1ddcf7cab..c12eabaa5 100644 --- a/watcher/tests/decision_engine/model/data/scenario_6_with_2_nodes.xml +++ b/watcher/tests/decision_engine/model/data/scenario_6_with_2_nodes.xml @@ -1,10 +1,10 @@ - - + + - - + + diff --git a/watcher/tests/decision_engine/model/data/scenario_7_with_2_nodes.xml b/watcher/tests/decision_engine/model/data/scenario_7_with_2_nodes.xml index 1da834336..99ee529a8 100644 --- a/watcher/tests/decision_engine/model/data/scenario_7_with_2_nodes.xml +++ b/watcher/tests/decision_engine/model/data/scenario_7_with_2_nodes.xml @@ -1,10 +1,10 @@ - - + + - - + + diff --git a/watcher/tests/decision_engine/model/data/scenario_8_with_4_nodes.xml b/watcher/tests/decision_engine/model/data/scenario_8_with_4_nodes.xml index 6e619c4a7..a646c6e90 100644 --- a/watcher/tests/decision_engine/model/data/scenario_8_with_4_nodes.xml +++ b/watcher/tests/decision_engine/model/data/scenario_8_with_4_nodes.xml @@ -1,16 +1,16 @@ - - - + + + - + - + - + diff --git a/watcher/tests/decision_engine/model/data/scenario_9_with_3_active_plus_1_disabled_nodes.xml b/watcher/tests/decision_engine/model/data/scenario_9_with_3_active_plus_1_disabled_nodes.xml index 33396d824..d1d3f94eb 100644 --- a/watcher/tests/decision_engine/model/data/scenario_9_with_3_active_plus_1_disabled_nodes.xml +++ b/watcher/tests/decision_engine/model/data/scenario_9_with_3_active_plus_1_disabled_nodes.xml @@ -1,16 +1,16 @@ - - - + + + - + - + - + diff --git a/watcher/tests/decision_engine/model/faker_cluster_state.py b/watcher/tests/decision_engine/model/faker_cluster_state.py index 66dd5e1cd..2be8222ec 100644 --- a/watcher/tests/decision_engine/model/faker_cluster_state.py +++ b/watcher/tests/decision_engine/model/faker_cluster_state.py @@ -85,6 +85,8 @@ class FakerModelCollector(base.BaseClusterDataModelCollector): "disk": 20, "disk_capacity": 20, "vcpus": 10, + "metadata": + '{"optimize": true,"top": "floor","nested": {"x": "y"}}' } instance = element.Instance(**instance_attributes) diff --git a/watcher/tests/decision_engine/model/test_model.py b/watcher/tests/decision_engine/model/test_model.py index 65c364afe..d8438dc43 100644 --- a/watcher/tests/decision_engine/model/test_model.py +++ b/watcher/tests/decision_engine/model/test_model.py @@ -18,9 +18,7 @@ import os -from lxml import etree from oslo_utils import uuidutils -import six from watcher.common import exception from watcher.decision_engine.model import element @@ -45,26 +43,16 @@ class TestModel(base.TestCase): def test_model_structure(self): fake_cluster = faker_cluster_state.FakerModelCollector() - model = fake_cluster.build_scenario_1() + model1 = fake_cluster.build_scenario_1() - self.assertEqual(5, len(model.get_all_compute_nodes())) - self.assertEqual(35, len(model.get_all_instances())) - self.assertEqual(8, len(model.edges())) + 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') - parser = etree.XMLParser(remove_blank_text=True) - expected_struct = etree.fromstring(expected_struct_str, parser) - model_structure = etree.fromstring(model.to_string(), parser) + model2 = model_root.ModelRoot.from_xml(expected_struct_str) - normalized_expected_output = six.BytesIO() - normalized_model_output = six.BytesIO() - expected_struct.getroottree().write_c14n(normalized_expected_output) - model_structure.getroottree().write_c14n(normalized_model_output) - - normalized_expected_struct = normalized_expected_output.getvalue() - normalized_model_struct = normalized_model_output.getvalue() - - self.assertEqual(normalized_expected_struct, normalized_model_struct) + self.assertTrue(model_root.ModelRoot.is_isomorphic(model2, model1)) def test_build_model_from_xml(self): fake_cluster = faker_cluster_state.FakerModelCollector()