From 2441a02c1c8538ab942b75c97818acf51aaf22e3 Mon Sep 17 00:00:00 2001 From: chestack Date: Wed, 20 Sep 2017 15:35:56 +0800 Subject: [PATCH] Enable to specify network for Trove Cluster we should enable heat to specify network for trove cluster after [1] merged [1] https://review.openstack.org/#/c/179443/ Change-Id: I161b7cc1c4824f6aa4a4667bf2d909a2ead81cb4 --- .../resources/openstack/trove/cluster.py | 125 ++++++++++++++++-- heat/tests/openstack/trove/test_cluster.py | 19 ++- heat/tests/test_translation_rule.py | 57 ++++++++ ...ks-for-trove-cluster-b997a049eedbad17.yaml | 3 + 4 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/set-networks-for-trove-cluster-b997a049eedbad17.yaml diff --git a/heat/engine/resources/openstack/trove/cluster.py b/heat/engine/resources/openstack/trove/cluster.py index 261b4a2cf4..ced52fe309 100644 --- a/heat/engine/resources/openstack/trove/cluster.py +++ b/heat/engine/resources/openstack/trove/cluster.py @@ -20,6 +20,7 @@ from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine import support +from heat.engine import translation LOG = logging.getLogger(__name__) @@ -62,9 +63,15 @@ class TroveCluster(resource.Resource): ) _INSTANCE_KEYS = ( - FLAVOR, VOLUME_SIZE, + FLAVOR, VOLUME_SIZE, NETWORKS, ) = ( - 'flavor', 'volume_size', + 'flavor', 'volume_size', 'networks', + ) + + _NICS_KEYS = ( + NET, PORT, V4_FIXED_IP + ) = ( + 'network', 'port', 'fixed_ip' ) ATTRIBUTES = ( @@ -121,10 +128,50 @@ class TroveCluster(resource.Resource): constraints=[ constraints.Range(1, 150), ] - ) + ), + NETWORKS: properties.Schema( + properties.Schema.LIST, + _("List of network interfaces to create on instance."), + support_status=support.SupportStatus(version='10.0.0'), + default=[], + schema=properties.Schema( + properties.Schema.MAP, + schema={ + NET: properties.Schema( + properties.Schema.STRING, + _('Name or UUID of the network to attach ' + 'this NIC to. Either %(port)s or ' + '%(net)s must be specified.') % { + 'port': PORT, 'net': NET}, + constraints=[ + constraints.CustomConstraint( + 'neutron.network') + ] + ), + PORT: properties.Schema( + properties.Schema.STRING, + _('Name or UUID of Neutron port to ' + 'attach this NIC to. Either %(port)s ' + 'or %(net)s must be specified.') + % {'port': PORT, 'net': NET}, + constraints=[ + constraints.CustomConstraint( + 'neutron.port') + ], + ), + V4_FIXED_IP: properties.Schema( + properties.Schema.STRING, + _('Fixed IPv4 address for this NIC.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ] + ), + }, + ), + ), } ) - ) + ), } attributes_schema = { @@ -142,6 +189,30 @@ class TroveCluster(resource.Resource): entity = 'clusters' + def translation_rules(self, properties): + return [ + translation.TranslationRule( + properties, + translation.TranslationRule.RESOLVE, + translation_path=[self.INSTANCES, self.NETWORKS, self.NET], + client_plugin=self.client_plugin('neutron'), + finder='find_resourceid_by_name_or_id', + entity='network'), + translation.TranslationRule( + properties, + translation.TranslationRule.RESOLVE, + translation_path=[self.INSTANCES, self.NETWORKS, self.PORT], + client_plugin=self.client_plugin('neutron'), + finder='find_resourceid_by_name_or_id', + entity='port'), + translation.TranslationRule( + properties, + translation.TranslationRule.RESOLVE, + translation_path=[self.INSTANCES, self.FLAVOR], + client_plugin=self.client_plugin(), + finder='find_flavor_by_name_or_id'), + ] + def _cluster_name(self): return self.properties[self.NAME] or self.physical_resource_name() @@ -152,11 +223,14 @@ class TroveCluster(resource.Resource): # convert instances to format required by troveclient instances = [] for instance in self.properties[self.INSTANCES]: - instances.append({ - 'flavorRef': self.client_plugin().find_flavor_by_name_or_id( - instance[self.FLAVOR]), - 'volume': {'size': instance[self.VOLUME_SIZE]} - }) + instance_dict = { + 'flavorRef': instance[self.FLAVOR], + 'volume': {'size': instance[self.VOLUME_SIZE]}, + } + instance_nics = self.get_instance_nics(instance) + if instance_nics: + instance_dict["nics"] = instance_nics + instances.append(instance_dict) args = { 'name': self._cluster_name(), @@ -168,6 +242,21 @@ class TroveCluster(resource.Resource): self.resource_id_set(cluster.id) return cluster.id + def get_instance_nics(self, instance): + nics = [] + for nic in instance[self.NETWORKS]: + nic_dict = {} + if nic.get(self.NET): + nic_dict['net-id'] = nic.get(self.NET) + if nic.get(self.PORT): + nic_dict['port-id'] = nic.get(self.PORT) + ip = nic.get(self.V4_FIXED_IP) + if ip: + nic_dict['v4-fixed-ip'] = ip + nics.append(nic_dict) + + return nics + def _refresh_cluster(self, cluster_id): try: cluster = self.client().clusters.get(cluster_id) @@ -256,6 +345,24 @@ class TroveCluster(resource.Resource): datastore_type, datastore_version, self.DATASTORE_TYPE, self.DATASTORE_VERSION) + # check validity of instances' NETWORKS + is_neutron = self.is_using_neutron() + for instance in self.properties[self.INSTANCES]: + for nic in instance[self.NETWORKS]: + # 'nic.get(self.PORT) is not None' including two cases: + # 1. has set port value in template + # 2. using 'get_resource' to reference a new resource + if not is_neutron and nic.get(self.PORT) is not None: + msg = (_("Can not use %s property on Nova-network.") + % self.PORT) + raise exception.StackValidationFailed(message=msg) + + if (bool(nic.get(self.NET) is not None) == + bool(nic.get(self.PORT) is not None)): + msg = (_("Either %(net)s or %(port)s must be provided.") + % {'net': self.NET, 'port': self.PORT}) + raise exception.StackValidationFailed(message=msg) + def _resolve_attribute(self, name): if self.resource_id is None: return diff --git a/heat/tests/openstack/trove/test_cluster.py b/heat/tests/openstack/trove/test_cluster.py index 8e459bebfb..1480bab286 100644 --- a/heat/tests/openstack/trove/test_cluster.py +++ b/heat/tests/openstack/trove/test_cluster.py @@ -18,6 +18,7 @@ from troveclient import exceptions as troveexc from heat.common import exception from heat.common import template_format +from heat.engine.clients.os import neutron from heat.engine.clients.os import trove from heat.engine.resources.openstack.trove import cluster from heat.engine import scheduler @@ -38,10 +39,16 @@ resources: instances: - flavor: m1.heat volume_size: 1 + networks: + - port: port1 - flavor: m1.heat volume_size: 1 + networks: + - port: port2 - flavor: m1.heat volume_size: 1 + networks: + - port: port3 ''' @@ -86,6 +93,9 @@ class TroveClusterTest(common.HeatTestCase): self.client = mock_client.return_value self.troveclient = mock.Mock() self.troveclient.flavors.get.return_value = FakeFlavor(1, 'm1.heat') + self.patchobject(neutron.NeutronClientPlugin, + 'find_resourceid_by_name_or_id', + return_value='someportid') self.troveclient.datastore_versions.list.return_value = [FakeVersion()] self.patchobject(trove.TroveClientPlugin, 'client', return_value=self.troveclient) @@ -106,9 +116,12 @@ class TroveClusterTest(common.HeatTestCase): expected_state = (tc.CREATE, tc.COMPLETE) self.assertEqual(expected_state, tc.state) args = self.client.clusters.create.call_args[1] - self.assertEqual([{'flavorRef': 1, 'volume': {'size': 1}}, - {'flavorRef': 1, 'volume': {'size': 1}}, - {'flavorRef': 1, 'volume': {'size': 1}}], + self.assertEqual([{'flavorRef': '1', 'volume': {'size': 1}, + 'nics': [{'port-id': 'someportid'}]}, + {'flavorRef': '1', 'volume': {'size': 1}, + 'nics': [{'port-id': 'someportid'}]}, + {'flavorRef': '1', 'volume': {'size': 1}, + 'nics': [{'port-id': 'someportid'}]}], args['instances']) self.assertEqual('mongodb', args['datastore']) self.assertEqual('2.6.1', args['datastore_version']) diff --git a/heat/tests/test_translation_rule.py b/heat/tests/test_translation_rule.py index be336e2186..bddc24791d 100644 --- a/heat/tests/test_translation_rule.py +++ b/heat/tests/test_translation_rule.py @@ -597,6 +597,63 @@ class TestTranslationRule(common.HeatTestCase): self.assertEqual('yellow', result) self.assertEqual('yellow', tran.resolved_translations['far.0.red']) + def test_resolve_rule_nested_list_populated(self): + client_plugin, schema = self._test_resolve_rule_nested_list() + data = { + 'instances': [{'networks': [{'port': 'port1', 'net': 'net1'}]}] + } + props = properties.Properties(schema, data) + rule = translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + ['instances', 'networks', 'port'], + client_plugin=client_plugin, + finder='find_name_id', + entity='port' + ) + tran = translation.Translation(props) + tran.set_rules([rule]) + self.assertTrue(tran.has_translation('instances.networks.port')) + result = tran.translate('instances.0.networks.0.port', + data['instances'][0]['networks'][0]['port']) + self.assertEqual('port1_id', result) + self.assertEqual('port1_id', tran.resolved_translations[ + 'instances.0.networks.0.port']) + + def _test_resolve_rule_nested_list(self): + class FakeClientPlugin(object): + def find_name_id(self, entity=None, value=None): + if entity == 'net': + return 'net1_id' + if entity == 'port': + return 'port1_id' + + schema = { + 'instances': properties.Schema( + properties.Schema.LIST, + schema=properties.Schema( + properties.Schema.MAP, + schema={ + 'networks': properties.Schema( + properties.Schema.LIST, + schema=properties.Schema( + properties.Schema.MAP, + schema={ + 'port': properties.Schema( + properties.Schema.STRING, + ), + 'net': properties.Schema( + properties.Schema.STRING, + ), + } + ) + ) + } + ) + )} + + return FakeClientPlugin(), schema + def test_resolve_rule_list_with_function(self): client_plugin, schema = self._test_resolve_rule(is_list=True) join_func = cfn_funcs.Join(None, diff --git a/releasenotes/notes/set-networks-for-trove-cluster-b997a049eedbad17.yaml b/releasenotes/notes/set-networks-for-trove-cluster-b997a049eedbad17.yaml new file mode 100644 index 0000000000..3fafbc9d9e --- /dev/null +++ b/releasenotes/notes/set-networks-for-trove-cluster-b997a049eedbad17.yaml @@ -0,0 +1,3 @@ +--- +features: + - Allow to set networks of instances for OS::Trove::Cluster resource.