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)