From bd54d99aa0b717007d79891eb7839b660616eb6f Mon Sep 17 00:00:00 2001 From: Andrey Shestakov Date: Wed, 25 Dec 2013 15:05:40 +0200 Subject: [PATCH] Add Neutron support Currently create instance doesnt work in OS installation with Neutron. To get it work, additional parameter 'nics' should be specified in Nova 'create' call. This change allows user to pass 'nics' parameter when create instance. Also possible to specify list of network IDs which should be attached to instance if networks are not specified by user. Closes-Bug: #1257838 Change-Id: Ifd98a880dfa22e9677e802bcd779acd9146ec671 --- trove/common/apischema.py | 9 +++- trove/common/cfg.py | 5 ++ trove/instance/models.py | 9 +++- trove/instance/service.py | 7 ++- trove/taskmanager/api.py | 4 +- trove/taskmanager/manager.py | 4 +- trove/taskmanager/models.py | 50 ++++++++++++++----- trove/templates/mysql/heat.template | 16 +++++- trove/tests/api/instances.py | 9 ++++ trove/tests/fakes/nova.py | 2 +- .../unittests/taskmanager/test_models.py | 14 +++--- 11 files changed, 101 insertions(+), 28 deletions(-) diff --git a/trove/common/apischema.py b/trove/common/apischema.py index dbc7a19aca..2947a44bf8 100644 --- a/trove/common/apischema.py +++ b/trove/common/apischema.py @@ -85,6 +85,12 @@ volume = { } } +nics = { + "type": "array", + "items": { + "type": "object", + } +} databases_ref_list = { "type": "array", @@ -199,7 +205,8 @@ instance = { "type": non_empty_string, "version": non_empty_string } - } + }, + "nics": nics } } } diff --git a/trove/common/cfg.py b/trove/common/cfg.py index acf51b2971..e0d2a60959 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -238,6 +238,11 @@ common_opts = [ default=['atom', 'json', 'xml'], help='Filetype endings not to be reattached to an id ' 'by the utils method correct_id_with_req.'), + cfg.ListOpt('default_neutron_networks', + default=[], + help='List of network IDs which should be attached' + ' to instance when networks are not specified' + ' in API call.'), ] CONF = cfg.CONF diff --git a/trove/instance/models.py b/trove/instance/models.py index 48cc9312bd..5277ac08db 100644 --- a/trove/instance/models.py +++ b/trove/instance/models.py @@ -451,7 +451,7 @@ class Instance(BuiltInstance): @classmethod def create(cls, context, name, flavor_id, image_id, databases, users, datastore, datastore_version, volume_size, backup_id, - availability_zone=None): + availability_zone=None, nics=None): client = create_nova_client(context) try: @@ -481,6 +481,11 @@ class Instance(BuiltInstance): raise exception.BackupFileNotFound( location=backup_info.location) + if not nics and CONF.default_neutron_networks: + nics = [] + for net_id in CONF.default_neutron_networks: + nics.append({"net-id": net_id}) + def _create_resources(): db_info = DBInstance.create(name=name, flavor_id=flavor_id, @@ -513,7 +518,7 @@ class Instance(BuiltInstance): datastore_version.packages, volume_size, backup_id, availability_zone, - root_password) + root_password, nics) return SimpleInstance(context, db_info, service_status, root_password) diff --git a/trove/instance/service.py b/trove/instance/service.py index d799460153..b69d8c774e 100644 --- a/trove/instance/service.py +++ b/trove/instance/service.py @@ -215,11 +215,16 @@ class InstanceController(wsgi.Controller): else: availability_zone = None + if 'nics' in body['instance']: + nics = body['instance']['nics'] + else: + nics = None + instance = models.Instance.create(context, name, flavor_id, image_id, databases, users, datastore, datastore_version, volume_size, backup_id, - availability_zone) + availability_zone, nics) view = views.InstanceDetailView(instance, req=req) return wsgi.Result(view.data(), 200) diff --git a/trove/taskmanager/api.py b/trove/taskmanager/api.py index b8cf8e0bfc..37d9234f0c 100644 --- a/trove/taskmanager/api.py +++ b/trove/taskmanager/api.py @@ -102,7 +102,7 @@ class API(proxy.RpcProxy): def create_instance(self, instance_id, name, flavor, image_id, databases, users, datastore_manager, packages, volume_size, backup_id=None, - availability_zone=None, root_password=None): + availability_zone=None, root_password=None, nics=None): LOG.debug("Making async call to create instance %s " % instance_id) self.cast(self.context, self.make_msg("create_instance", @@ -117,4 +117,4 @@ class API(proxy.RpcProxy): volume_size=volume_size, backup_id=backup_id, availability_zone=availability_zone, - root_password=root_password)) + root_password=root_password, nics=nics)) diff --git a/trove/taskmanager/manager.py b/trove/taskmanager/manager.py index 38f6078642..448356b130 100644 --- a/trove/taskmanager/manager.py +++ b/trove/taskmanager/manager.py @@ -83,12 +83,12 @@ class Manager(periodic_task.PeriodicTasks): def create_instance(self, context, instance_id, name, flavor, image_id, databases, users, datastore_manager, packages, volume_size, backup_id, availability_zone, - root_password): + root_password, nics): instance_tasks = FreshInstanceTasks.load(context, instance_id) instance_tasks.create_instance(flavor, image_id, databases, users, datastore_manager, packages, volume_size, backup_id, - availability_zone, root_password) + availability_zone, root_password, nics) if CONF.exists_notification_transformer: @periodic_task.periodic_task( diff --git a/trove/taskmanager/models.py b/trove/taskmanager/models.py index e36d75e5e9..102a9baef4 100644 --- a/trove/taskmanager/models.py +++ b/trove/taskmanager/models.py @@ -147,7 +147,7 @@ class ConfigurationMixin(object): class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def create_instance(self, flavor, image_id, databases, users, datastore_manager, packages, volume_size, - backup_id, availability_zone, root_password): + backup_id, availability_zone, root_password, nics): LOG.debug(_("begin create_instance for id: %s") % self.id) security_groups = None @@ -175,7 +175,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): image_id, datastore_manager, volume_size, - availability_zone) + availability_zone, + nics) elif use_nova_server_volume: volume_info = self._create_server_volume( flavor['id'], @@ -183,7 +184,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): security_groups, datastore_manager, volume_size, - availability_zone) + availability_zone, + nics) else: volume_info = self._create_server_volume_individually( flavor['id'], @@ -191,7 +193,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): security_groups, datastore_manager, volume_size, - availability_zone) + availability_zone, + nics) config = self._render_config(datastore_manager, flavor, self.id) @@ -302,7 +305,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_server_volume(self, flavor_id, image_id, security_groups, datastore_manager, volume_size, - availability_zone): + availability_zone, nics): LOG.debug(_("begin _create_server_volume for id: %s") % self.id) server = None try: @@ -321,7 +324,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): name, image_id, flavor_id, files=files, volume=volume_ref, security_groups=security_groups, - availability_zone=availability_zone) + availability_zone=availability_zone, nics=nics) LOG.debug(_("Created new compute instance %(server_id)s " "for id: %(id)s") % {'server_id': server.id, 'id': self.id}) @@ -349,14 +352,16 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_server_volume_heat(self, flavor, image_id, datastore_manager, - volume_size, availability_zone): + volume_size, availability_zone, nics): LOG.debug(_("begin _create_server_volume_heat for id: %s") % self.id) try: client = create_heat_client(self.context) + ifaces, ports = self._build_heat_nics(nics) template_obj = template.load_heat_template(datastore_manager) heat_template_unicode = template_obj.render( - volume_support=CONF.trove_volume_support) + volume_support=CONF.trove_volume_support, + ifaces=ifaces, ports=ports) try: heat_template = heat_template_unicode.encode('utf-8') except UnicodeEncodeError: @@ -422,7 +427,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_server_volume_individually(self, flavor_id, image_id, security_groups, datastore_manager, volume_size, - availability_zone): + availability_zone, nics): LOG.debug(_("begin _create_server_volume_individually for id: %s") % self.id) server = None @@ -432,7 +437,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): server = self._create_server(flavor_id, image_id, security_groups, datastore_manager, block_device_mapping, - availability_zone) + availability_zone, nics) server_id = server.id # Save server ID. self.update_db(compute_instance_id=server_id) @@ -522,7 +527,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_server(self, flavor_id, image_id, security_groups, datastore_manager, block_device_mapping, - availability_zone): + availability_zone, nics): files = {"/etc/guest_info": ("[DEFAULT]\nguest_id=%s\n" "datastore_manager=%s\n" "tenant_id=%s\n" % @@ -542,7 +547,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): server = self.nova_client.servers.create( name, image_id, flavor_id, files=files, userdata=userdata, security_groups=security_groups, block_device_mapping=bdmap, - availability_zone=availability_zone) + availability_zone=availability_zone, nics=nics) LOG.debug(_("Created new compute instance %(server_id)s " "for id: %(id)s") % {'server_id': server.id, 'id': self.id}) @@ -616,6 +621,27 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): ) return [security_group["name"]] + def _build_heat_nics(self, nics): + ifaces = [] + ports = [] + if nics: + for idx, nic in enumerate(nics): + iface_id = nic.get('port-id') + if iface_id: + ifaces.append(iface_id) + continue + net_id = nic.get('net-id') + if net_id: + port = {} + port['name'] = "Port%s" % idx + port['net_id'] = net_id + fixed_ip = nic.get('v4-fixed-ip') + if fixed_ip: + port['fixed_ip'] = fixed_ip + ports.append(port) + ifaces.append("{Ref: Port%s}" % idx) + return ifaces, ports + class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin): """ diff --git a/trove/templates/mysql/heat.template b/trove/templates/mysql/heat.template index bb87a992cb..b692d96ed5 100644 --- a/trove/templates/mysql/heat.template +++ b/trove/templates/mysql/heat.template @@ -18,6 +18,16 @@ Parameters: TenantId: Type: String Resources: +{% for port in ports %} + {{ port.name }}: + Type: OS::Neutron::Port + Properties: + network_id: "{{ port.net_id }}" + security_groups: [{Ref: MySqlDbaasSG}] + {% if port.fixed_ip %} + fixed_ips: [{"ip_address": "{{ port.fixed_ip }}"}] + {% endif %} +{% endfor %} BaseInstance: Type: AWS::EC2::Instance Metadata: @@ -38,7 +48,11 @@ Resources: ImageId: {Ref: ImageId} InstanceType: {Ref: Flavor} AvailabilityZone: {Ref: AvailabilityZone} - SecurityGroups : [{Ref: MySqlDbaasSG}] + {% if ifaces %} + NetworkInterfaces: [{{ ifaces|join(', ') }}] + {% else %} + SecurityGroups: [{Ref: MySqlDbaasSG}] + {% endif %} UserData: Fn::Base64: Fn::Join: diff --git a/trove/tests/api/instances.py b/trove/tests/api/instances.py index 8778e77809..27f5923d3b 100644 --- a/trove/tests/api/instances.py +++ b/trove/tests/api/instances.py @@ -387,6 +387,15 @@ class CreateInstance(object): None, databases, availability_zone="NOOP") assert_equal(400, dbaas.last_http_code) + @test(enabled=not FAKE) + def test_create_with_bad_nics(self): + instance_name = "instance-failure-with-bad-nics" + databases = [] + assert_raises(exceptions.BadRequest, dbaas.instances.create, + instance_name, instance_info.dbaas_flavor_href, + None, databases, nics="BAD") + assert_equal(400, dbaas.last_http_code) + @test(enabled=VOLUME_SUPPORT) def test_create_failure_with_empty_volume(self): instance_name = "instance-failure-with-no-volume-size" diff --git a/trove/tests/fakes/nova.py b/trove/tests/fakes/nova.py index e4dcee8be6..b9660bd028 100644 --- a/trove/tests/fakes/nova.py +++ b/trove/tests/fakes/nova.py @@ -260,7 +260,7 @@ class FakeServers(object): def create(self, name, image_id, flavor_ref, files=None, userdata=None, block_device_mapping=None, volume=None, security_groups=None, - availability_zone=None): + availability_zone=None, nics=None): id = "FAKE_%s" % uuid.uuid4() if volume: volume = self.volumes.create(volume['size'], volume['name'], diff --git a/trove/tests/unittests/taskmanager/test_models.py b/trove/tests/unittests/taskmanager/test_models.py index d0dcd341a8..5ffa5b655a 100644 --- a/trove/tests/unittests/taskmanager/test_models.py +++ b/trove/tests/unittests/taskmanager/test_models.py @@ -48,7 +48,8 @@ class fake_Server: class fake_ServerManager: def create(self, name, image_id, flavor_id, files, userdata, - security_groups, block_device_mapping, availability_zone=None): + security_groups, block_device_mapping, availability_zone=None, + nics=None): server = fake_Server() server.id = "server_id" server.name = name @@ -59,6 +60,7 @@ class fake_ServerManager: server.security_groups = security_groups server.block_device_mapping = block_device_mapping server.availability_zone = availability_zone + server.nics = nics return server @@ -180,33 +182,33 @@ class FreshInstanceTasksTest(testtools.TestCase): when(taskmanager_models.CONF).get("cloudinit_location").thenReturn( cloudinit_location) server = self.freshinstancetasks._create_server( - None, None, None, datastore_manager, None, None) + None, None, None, datastore_manager, None, None, None) self.assertEqual(server.userdata, self.userdata) def test_create_instance_guestconfig(self): when(taskmanager_models.CONF).get("guest_config").thenReturn( self.guestconfig) server = self.freshinstancetasks._create_server( - None, None, None, "test", None, None) + None, None, None, "test", None, None, None) self.assertTrue('/etc/trove-guestagent.conf' in server.files) self.assertEqual(server.files['/etc/trove-guestagent.conf'], self.guestconfig_content) def test_create_instance_with_az_kwarg(self): server = self.freshinstancetasks._create_server( - None, None, None, None, None, availability_zone='nova') + None, None, None, None, None, availability_zone='nova', nics=None) self.assertIsNotNone(server) def test_create_instance_with_az(self): server = self.freshinstancetasks._create_server( - None, None, None, None, None, 'nova') + None, None, None, None, None, 'nova', None) self.assertIsNotNone(server) def test_create_instance_with_az_none(self): server = self.freshinstancetasks._create_server( - None, None, None, None, None, None) + None, None, None, None, None, None, None) self.assertIsNotNone(server)