diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 09e25107..79beeb8d 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -333,16 +333,19 @@ network_port_type: network_uuid: description: | To provision the server with a NIC for a network, specify the UUID of - the network in the ``net_id`` key in a dict in ``networks`` list. + the network with the ``net_id`` key in a dict in ``networks`` list. in: body - required: true + required: false type: string networks: description: | A list of networks of the tenant. Optionally, you can create one or more NICs on the server. To provision the server with a NIC for a network, specify the UUID of the network - in the ``net_id`` key in a dict in ``networks`` list. To provision the server with a + with the ``net_id`` key in a dict in ``networks`` list. To provision the server with a specified type of NIC, specify the port-type key in a dict in a ``networks`` list. + To provision the server with a NIC for an already existing port, specify the port_id in + a ``networks`` list. Now net_id and port_id are exclusive, so you should use only one of + them at one time. in: body required: true type: array @@ -367,6 +370,13 @@ personality: in: body required: false type: string +port_uuid: + description: | + To provision the server with a NIC for an already existing port, + specify the port_id in a ``networks`` list. + in: body + required: false + type: string power_state: description: | The current power state of this Server. Usually, "power on" or diff --git a/api-ref/source/v1/servers.inc b/api-ref/source/v1/servers.inc index 6642e89b..dcf12424 100644 --- a/api-ref/source/v1/servers.inc +++ b/api-ref/source/v1/servers.inc @@ -38,6 +38,7 @@ Request - networks: networks - networks.net_id: network_uuid - networks.port_type: network_port_type + - networks.port_id: port_uuid - user_data: user_data - personality: personality - key_name: key_name diff --git a/mogan/api/controllers/v1/schemas/servers.py b/mogan/api/controllers/v1/schemas/servers.py index 1e9fb948..dc9ee678 100644 --- a/mogan/api/controllers/v1/schemas/servers.py +++ b/mogan/api/controllers/v1/schemas/servers.py @@ -32,8 +32,12 @@ create_server = { 'properties': { 'net_id': parameter_types.network_id, 'port_type': parameter_types.port_type, + 'port_id': parameter_types.network_port_id, }, - 'required': ['net_id'], + 'oneOf': [ + {'required': ['net_id']}, + {'required': ['port_id']} + ], 'additionalProperties': False, }, }, diff --git a/mogan/api/controllers/v1/servers.py b/mogan/api/controllers/v1/servers.py index 04cebd6b..1b69a9e0 100644 --- a/mogan/api/controllers/v1/servers.py +++ b/mogan/api/controllers/v1/servers.py @@ -681,9 +681,13 @@ class ServerController(ServerControllerBase): exception.ServerUserDataTooLarge, exception.Base64Exception, exception.NetworkRequiresSubnet, - exception.NetworkNotFound) as e: + exception.NetworkNotFound, + exception.PortRequiresFixedIP) as e: raise wsme.exc.ClientSideError( e.message, status_code=http_client.BAD_REQUEST) + except exception.PortInUse as e: + raise wsme.exc.ClientSideError( + e.message, status_code=http_client.CONFLICT) # Set the HTTP Location Header for the first server. pecan.response.location = link.build_url('server', servers[0].uuid) diff --git a/mogan/common/exception.py b/mogan/common/exception.py index 41bd9673..319ad5f7 100644 --- a/mogan/common/exception.py +++ b/mogan/common/exception.py @@ -297,6 +297,14 @@ class PortNotFound(NotFound): _msg_fmt = _("Port id %(port_id)s could not be found.") +class PortRequiresFixedIP(Invalid): + msg_fmt = _("Port %(port_id)s requires a FixedIP in order to be used.") + + +class PortInUse(Conflict): + msg_fmt = _("Port %(port_id)s is still in use.") + + class InterfaceAttachFailed(Invalid): msg_fmt = _("Failed to attach network adapter device to " "%(server_uuid)s") diff --git a/mogan/engine/flows/create_server.py b/mogan/engine/flows/create_server.py index 7210882f..0c5841fe 100644 --- a/mogan/engine/flows/create_server.py +++ b/mogan/engine/flows/create_server.py @@ -160,9 +160,13 @@ class BuildNetworkTask(flow_utils.MoganTask): # Match the specified port type with physical interface type if vif.get('port_type', 'None') == pif.port_type: try: - port = self.manager.network_api.create_port( - context, vif['net_id'], pif.address, server.uuid) - port_dict = port['port'] + if vif.get('net_id'): + port = self.manager.network_api.create_port( + context, vif['net_id'], pif.address, server.uuid) + port_dict = port['port'] + elif vif.get('port_id'): + port_dict = self.manager.network_api.show_port( + context, vif.get('port_id')) self.manager.driver.plug_vif(pif.port_uuid, port_dict['id']) @@ -179,8 +183,8 @@ class BuildNetworkTask(flow_utils.MoganTask): # Set nics here, so we can clean up the # created networks during reverting. server.nics = nics_obj - LOG.error("Server %(server)s: create network failed. " - "The reason is %(reason)s", + LOG.error("Server %(server)s: create or get network " + "failed. The reason is %(reason)s", {"server": server.uuid, "reason": e}) raise exception.NetworkError(_( "Build network for server failed.")) diff --git a/mogan/network/api.py b/mogan/network/api.py index e283c1bd..5ddfe58d 100644 --- a/mogan/network/api.py +++ b/mogan/network/api.py @@ -84,6 +84,10 @@ class API(object): raise exception.NetworkError(msg) return port + def show_port(self, context, port_uuid): + client = get_client(context.auth_token) + return self._show_port(client, port_uuid) + def _show_port(self, client, port_id): """Return the port for the client given the port id.""" @@ -201,13 +205,30 @@ class API(object): return nets + def _get_available_ports(self, port_ids, client): + """Return a port list available for the tenant.""" + + search_opts = {'id': port_ids} + ports = client.list_ports(**search_opts).get('ports', []) + + _ensure_requested_network_ordering( + lambda x: x['id'], + ports, + port_ids) + + return ports + def _ports_needed_per_server(self, client, requested_networks): ports_needed_per_server = 0 net_ids_requested = [] + port_ids_requested = [] for request in requested_networks: ports_needed_per_server += 1 - net_ids_requested.append(request['net_id']) + if request.get('net_id'): + net_ids_requested.append(request.get('net_id')) + if request.get('port_id'): + port_ids_requested.append(request.get('port_id')) # Now check to see if all requested networks exist if net_ids_requested: @@ -228,6 +249,27 @@ class API(object): id_str = id_str and id_str + ', ' + _id or _id raise exception.NetworkNotFound(network_id=id_str) + # Now check to see if all requested ports exist + if port_ids_requested: + ports = self._get_available_ports(port_ids_requested, client) + + if len(ports) != len(port_ids_requested): + requested_portid_set = set(port_ids_requested) + returned_portid_set = set([port['id'] for port in ports]) + lostid_set = requested_portid_set - returned_portid_set + if lostid_set: + id_str = '' + for _id in lostid_set: + id_str = id_str and id_str + ', ' + _id or _id + raise exception.PortNotFound(port_id=id_str) + # Check if those ports are used and have FixedIP now. + for port in ports: + if port.get('device_id', None): + raise exception.PortInUse(port_id=request.port_id) + deferred_ip = port.get('ip_allocation') == 'deferred' + if not deferred_ip and not port.get('fixed_ips'): + raise exception.PortRequiresFixedIP(port_id=port['id']) + return ports_needed_per_server def validate_networks(self, context, requested_networks, num_servers): diff --git a/mogan/tests/unit/api/v1/test_server.py b/mogan/tests/unit/api/v1/test_server.py index 39cfbb88..f5194c6b 100644 --- a/mogan/tests/unit/api/v1/test_server.py +++ b/mogan/tests/unit/api/v1/test_server.py @@ -70,6 +70,65 @@ class TestServerAuthorization(v1_test.APITestV1): headers = self.gen_headers(self.context) self.post_json('/servers', body, headers=headers, status=201) + @mock.patch('mogan.engine.api.API.create') + @mock.patch('mogan.objects.Flavor.get') + def test_server_post_with_port_ids(self, mock_get, mock_engine_create): + flavor = mock.MagicMock() + flavor.nics = [{"type": "Ethernet", "speed": "10GE"}, + {"type": "Ethernet", "speed": "10GE"}] + mock_get.return_value = flavor + mock_engine_create.side_effect = None + mock_engine_create.return_value = [self.server1] + fake_networks = [ + { + "port_id": "c1940655-8b8e-4370-b8f9-03ba1daeca31", + "port_type": "Ethernet" + }, + { + "port_id": "8e8ceb07-4641-4188-9b22-840755e92ee2", + "port_type": "Ethernet" + } + ] + body = gen_post_body(**{'networks': fake_networks}) + self.context.roles = "no-admin" + # we can not prevent the evil tenant, quota will limite him. + # Note(Shaohe): quota is in plan + self.context.tenant = self.evil_project + headers = self.gen_headers(self.context) + self.post_json('/servers', body, headers=headers, status=201) + + @mock.patch('mogan.engine.api.API.create') + @mock.patch('mogan.objects.Flavor.get') + def test_server_post_with_port_ids_and_networks(self, mock_get, + mock_engine_create): + flavor = mock.MagicMock() + flavor.nics = [{"type": "Ethernet", "speed": "10GE"}, + {"type": "Ethernet", "speed": "10GE"}] + mock_get.return_value = flavor + mock_engine_create.side_effect = None + mock_engine_create.return_value = [self.server1] + fake_networks = [ + { + "port_id": "c1940655-8b8e-4370-b8f9-03ba1daeca31", + "net_id": "c1940655-8b8e-4370-b8f9-03ba1daeca32", + "port_type": "Ethernet" + }, + { + "port_id": "8e8ceb07-4641-4188-9b22-840755e92ee2", + "net_id": "8e8ceb07-4641-4188-9b22-840755e92ee3", + "port_type": "Ethernet" + } + ] + body = gen_post_body(**{'networks': fake_networks}) + self.context.roles = "no-admin" + # we can not prevent the evil tenant, quota will limite him. + # Note(Shaohe): quota is in plan + self.context.tenant = self.evil_project + headers = self.gen_headers(self.context) + ret = self.post_json('/servers', body, headers=headers, + expect_errors=True) + self.assertTrue(ret.json['error_message']) + def test_server_get_one_by_owner(self): # not admin but the owner self.context.tenant = self.server1.project_id diff --git a/mogan/tests/unit/engine/test_engine_api.py b/mogan/tests/unit/engine/test_engine_api.py index 20f1499c..d61e764a 100644 --- a/mogan/tests/unit/engine/test_engine_api.py +++ b/mogan/tests/unit/engine/test_engine_api.py @@ -92,6 +92,27 @@ class ComputeAPIUnitTest(base.DbTestCase): None, 1) + @mock.patch('mogan.network.api.get_client') + def test__check_requested_networks(self, mock_get_client): + mock_get_client.return_value = mock.MagicMock() + mock_get_client.return_value.list_networks.return_value = \ + {'networks': [{'id': '1', 'subnets': {'id': '2'}}, + {'id': '3', 'subnets': {'id': '4'}}]} + mock_get_client.return_value.list_ports.return_value = \ + {'ports': [{'id': '5', + 'fixed_ips': [{'ip_address': '192.168.1.1'}]}, + {'id': '6', + 'fixed_ips': [{'ip_address': '192.168.1.2'}]}]} + mock_get_client.return_value.show_quota.return_value = \ + {'quota': {'port': 10}} + + requested_networks = [{'net_id': '1'}, {'net_id': '3'}, + {'port_id': '5'}, {'port_id': '6'}] + max_network_count = self.engine_api._check_requested_networks( + self.context, requested_networks=requested_networks, max_count=2) + + self.assertEqual(2, max_network_count) + @mock.patch.object(objects.Server, 'create') def test__provision_servers(self, mock_server_create): mock_server_create.return_value = mock.MagicMock()