From 6e38dcb70ebdf7eae955402908c62f0ae048ccf6 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 5 Dec 2011 17:28:33 +0000 Subject: [PATCH] blueprint api-operational-status Adds a new attribute expressing current operational status for port and network resources Also includes: - db models: changes to accomodate operational status concept - unit tests: changes to include different validation functions for API v1.0 and v.1.1 This changeset does not include changes to the client library NOTE: Addressing issue concerning unit tests for OVS plugin. API unit tests launched with PLUGIN_DIR set to Cisco's plugin work fine as well. Change-Id: I0c4f0f8a8c392bae75c668c28070364ca230f965 --- quantum/api/api_common.py | 16 +++++++ quantum/api/networks.py | 12 ++++-- quantum/api/ports.py | 8 ++-- quantum/api/views/networks.py | 28 ++++++++++-- quantum/api/views/ports.py | 26 +++++++++-- quantum/common/test_lib.py | 2 + quantum/db/api.py | 9 ++-- quantum/db/models.py | 22 +++++++--- quantum/plugins/openvswitch/run_tests.py | 5 +++ quantum/plugins/sample/SamplePlugin.py | 16 +++++-- quantum/tests/unit/_test_api.py | 40 ++++++++--------- quantum/tests/unit/test_api.py | 55 ++++++++++++++++++++++++ 12 files changed, 190 insertions(+), 49 deletions(-) diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py index d3a6c974559..96fe70a2080 100644 --- a/quantum/api/api_common.py +++ b/quantum/api/api_common.py @@ -27,6 +27,22 @@ XML_NS_V11 = 'http://openstack.org/quantum/api/v1.1' LOG = logging.getLogger('quantum.api.api_common') +class OperationalStatus: + """ Enumeration for operational status + + UP : the resource is available (operationall up) + DOWN : the resource is not operational; this might indicate + a failure in the underlying switching fabric. + PROVISIONING: the plugin is creating or updating the resource + in the underlying switching fabric + UNKNOWN: the plugin does not support the operational status concept. + """ + UP = "UP" + DOWN = "DOWN" + PROVISIONING = "PROVISIONING" + UNKNOWN = "UNKNOWN" + + def create_resource(version, controller_dict): """ Generic function for creating a wsgi resource diff --git a/quantum/api/networks.py b/quantum/api/networks.py index a770c24d1fe..50b1e0aa71d 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -146,12 +146,18 @@ class ControllerV10(Controller): class ControllerV11(Controller): - """Network resources controller for Quantum v1.1 API""" + """Network resources controller for Quantum v1.1 API + + Note: at this state this class only adds serialization + metadata for the operational status concept. + API filters, pagination, and atom links will be handled by + this class as well. + """ _serialization_metadata = { "attributes": { - "network": ["id", "name"], - "port": ["id", "state"], + "network": ["id", "name", "op-status"], + "port": ["id", "state", "op-status"], "attachment": ["id"]}, "plurals": {"networks": "network", "ports": "port"} diff --git a/quantum/api/ports.py b/quantum/api/ports.py index d0d69f52d3a..fb5a5786465 100644 --- a/quantum/api/ports.py +++ b/quantum/api/ports.py @@ -50,7 +50,7 @@ class Controller(common.QuantumController): port_details=False): """ Returns a list of ports. """ port_list = self._plugin.get_all_ports(tenant_id, network_id) - builder = ports_view.get_view_builder(request) + builder = ports_view.get_view_builder(request, self.version) # Load extra data for ports if required. if port_details: @@ -69,7 +69,7 @@ class Controller(common.QuantumController): """ Returns a specific port. """ port = self._plugin.get_port_details( tenant_id, network_id, port_id) - builder = ports_view.get_view_builder(request) + builder = ports_view.get_view_builder(request, self.version) result = builder.build(port, port_details=True, att_details=att_details)['port'] return dict(port=result) @@ -111,7 +111,7 @@ class Controller(common.QuantumController): port = self._plugin.create_port(tenant_id, network_id, body['port']['state'], **body) - builder = ports_view.get_view_builder(request) + builder = ports_view.get_view_builder(request, self.version) result = builder.build(port)['port'] return dict(port=result) @@ -151,7 +151,7 @@ class ControllerV11(Controller): _serialization_metadata = { "attributes": { - "port": ["id", "state"], + "port": ["id", "state", "op-status"], "attachment": ["id"]}, "plurals": {"ports": "port"} } diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py index 1ed858e50a3..40b0b35bdeb 100644 --- a/quantum/api/views/networks.py +++ b/quantum/api/views/networks.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from quantum.api.api_common import OperationalStatus + def get_view_builder(req, version): base_url = req.application_url @@ -64,6 +66,26 @@ class ViewBuilder10(object): class ViewBuilder11(ViewBuilder10): - #TODO(salvatore-orlando): will extend for Operational status - # in appropriate branch - pass + + def _build_simple(self, network_data): + """Return a simple model of a network.""" + return dict(network=dict(id=network_data['net-id'])) + + def _build_detail(self, network_data): + """Return a detailed model of a network. """ + op_status = network_data.get('net-op-status', + OperationalStatus.UNKNOWN) + return dict(network={'id': network_data['net-id'], + 'name': network_data['net-name'], + 'op-status': op_status}) + + def _build_port(self, port_data): + """Return details about a specific logical port.""" + op_status = port_data.get('port-op-status', + OperationalStatus.UNKNOWN) + port_dict = {'id': port_data['port-id'], + 'state': port_data['port-state'], + 'op-status': op_status} + if port_data['attachment']: + port_dict['attachment'] = dict(id=port_data['attachment']) + return port_dict diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py index dd3f18ea236..919e9f5c493 100644 --- a/quantum/api/views/ports.py +++ b/quantum/api/views/ports.py @@ -15,13 +15,19 @@ # License for the specific language governing permissions and limitations # under the License. +from quantum.api.api_common import OperationalStatus -def get_view_builder(req): + +def get_view_builder(req, version): base_url = req.application_url - return ViewBuilder(base_url) + view_builder = { + '1.0': ViewBuilder10, + '1.1': ViewBuilder11, + }[version](base_url) + return view_builder -class ViewBuilder(object): +class ViewBuilder10(object): def __init__(self, base_url=None): """ @@ -37,3 +43,17 @@ class ViewBuilder(object): if att_details and port_data['attachment']: port['port']['attachment'] = dict(id=port_data['attachment']) return port + + +class ViewBuilder11(ViewBuilder10): + + def build(self, port_data, port_details=False, att_details=False): + """Generates a port entity with operation status info""" + port = dict(port=dict(id=port_data['port-id'])) + if port_details: + port['port']['state'] = port_data['port-state'] + port['port']['op-status'] = port_data.get('port-op-status', + OperationalStatus.UNKNOWN) + if att_details and port_data['attachment']: + port['port']['attachment'] = dict(id=port_data['attachment']) + return port diff --git a/quantum/common/test_lib.py b/quantum/common/test_lib.py index 9df0574c7b6..03578817d6a 100644 --- a/quantum/common/test_lib.py +++ b/quantum/common/test_lib.py @@ -286,4 +286,6 @@ def run_tests(c=None): # quantum/plugins/openvswitch/ ) test_config = { "plugin_name": "quantum.plugins.sample.SamplePlugin.FakePlugin", + "default_net_op_status": "UP", + "default_port_op_status": "UP", } diff --git a/quantum/db/api.py b/quantum/db/api.py index d262f8acf18..84d5b9f53f2 100644 --- a/quantum/db/api.py +++ b/quantum/db/api.py @@ -22,6 +22,7 @@ import logging from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, exc +from quantum.api.api_common import OperationalStatus from quantum.common import exceptions as q_exc from quantum.db import models @@ -80,11 +81,11 @@ def unregister_models(): BASE.metadata.drop_all(_ENGINE) -def network_create(tenant_id, name): +def network_create(tenant_id, name, op_status=OperationalStatus.UNKNOWN): session = get_session() with session.begin(): - net = models.Network(tenant_id, name) + net = models.Network(tenant_id, name, op_status) session.add(net) session.flush() return net @@ -137,13 +138,13 @@ def network_destroy(net_id): raise q_exc.NetworkNotFound(net_id=net_id) -def port_create(net_id, state=None): +def port_create(net_id, state=None, op_status=OperationalStatus.UNKNOWN): # confirm network exists network_get(net_id) session = get_session() with session.begin(): - port = models.Port(net_id) + port = models.Port(net_id, op_status) port['state'] = state or 'DOWN' session.add(port) session.flush() diff --git a/quantum/db/models.py b/quantum/db/models.py index 409e91dea19..1039cbe2199 100644 --- a/quantum/db/models.py +++ b/quantum/db/models.py @@ -16,14 +16,15 @@ # @author: Somik Behera, Nicira Networks, Inc. # @author: Brad Hall, Nicira Networks, Inc. # @author: Dan Wendlandt, Nicira Networks, Inc. +# @author: Salvatore Orlando, Citrix Systems import uuid - from sqlalchemy import Column, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relation, object_mapper +from quantum.api import api_common as common BASE = declarative_base() @@ -73,16 +74,20 @@ class Port(BASE, QuantumBase): interface_id = Column(String(255), nullable=True) # Port state - Hardcoding string value at the moment state = Column(String(8)) + op_status = Column(String(16)) - def __init__(self, network_id): + def __init__(self, network_id, + op_status=common.OperationalStatus.UNKNOWN): self.uuid = str(uuid.uuid4()) self.network_id = network_id self.interface_id = None self.state = "DOWN" + self.op_status = op_status def __repr__(self): - return "" % (self.uuid, self.network_id, - self.state, self.interface_id) + return "" % (self.uuid, self.network_id, + self.state, self.op_status, + self.interface_id) class Network(BASE, QuantumBase): @@ -93,12 +98,15 @@ class Network(BASE, QuantumBase): tenant_id = Column(String(255), nullable=False) name = Column(String(255)) ports = relation(Port, order_by=Port.uuid, backref="network") + op_status = Column(String(16)) - def __init__(self, tenant_id, name): + def __init__(self, tenant_id, name, + op_status=common.OperationalStatus.UNKNOWN): self.uuid = str(uuid.uuid4()) self.tenant_id = tenant_id self.name = name + self.op_status = op_status def __repr__(self): - return "" % \ - (self.uuid, self.name, self.tenant_id) + return "" % \ + (self.uuid, self.name, self.op_status, self.tenant_id) diff --git a/quantum/plugins/openvswitch/run_tests.py b/quantum/plugins/openvswitch/run_tests.py index 0377dd8943d..9649efa0d87 100644 --- a/quantum/plugins/openvswitch/run_tests.py +++ b/quantum/plugins/openvswitch/run_tests.py @@ -49,7 +49,12 @@ if __name__ == '__main__': # we should only invoked the tests once invoke_once = len(sys.argv) > 1 + # NOTE (salvatore-orlando): + # please update default values for operational status according to + # the plugin behavior. test_config['plugin_name'] = "ovs_quantum_plugin.OVSQuantumPlugin" + test_config['default_net_op_status'] = "UNKNOWN" + test_config['default_port_op_status'] = "UNKNOWN" cwd = os.getcwd() c = config.Config(stream=sys.stdout, diff --git a/quantum/plugins/sample/SamplePlugin.py b/quantum/plugins/sample/SamplePlugin.py index 2f5626609d3..5fb6cbc812c 100644 --- a/quantum/plugins/sample/SamplePlugin.py +++ b/quantum/plugins/sample/SamplePlugin.py @@ -18,6 +18,7 @@ import logging +from quantum.api.api_common import OperationalStatus from quantum.common import exceptions as exc from quantum.db import api as db @@ -174,7 +175,8 @@ class FakePlugin(object): nets = [] for net in db.network_list(tenant_id): net_item = {'net-id': str(net.uuid), - 'net-name': net.name} + 'net-name': net.name, + 'net-op-status': net.op_status} nets.append(net_item) return nets @@ -189,6 +191,7 @@ class FakePlugin(object): ports = self.get_all_ports(tenant_id, net_id) return {'net-id': str(net.uuid), 'net-name': net.name, + 'net-op-status': net.op_status, 'net-ports': ports} def create_network(self, tenant_id, net_name, **kwargs): @@ -198,8 +201,11 @@ class FakePlugin(object): """ LOG.debug("FakePlugin.create_network() called") new_net = db.network_create(tenant_id, net_name) + # Put operational status UP + db.network_update(new_net.uuid, net_name, + op_status=OperationalStatus.UP) # Return uuid for newly created network as net-id. - return {'net-id': new_net['uuid']} + return {'net-id': new_net.uuid} def delete_network(self, tenant_id, net_id): """ @@ -248,7 +254,8 @@ class FakePlugin(object): port = self._get_port(tenant_id, net_id, port_id) return {'port-id': str(port.uuid), 'attachment': port.interface_id, - 'port-state': port.state} + 'port-state': port.state, + 'port-op-status': port.op_status} def create_port(self, tenant_id, net_id, port_state=None, **kwargs): """ @@ -258,6 +265,9 @@ class FakePlugin(object): # verify net_id self._get_network(tenant_id, net_id) port = db.port_create(net_id, port_state) + # Put operational status UP + db.port_update(port.uuid, net_id, + op_status=OperationalStatus.UP) port_item = {'port-id': str(port.uuid)} return port_item diff --git a/quantum/tests/unit/_test_api.py b/quantum/tests/unit/_test_api.py index d021c82f852..629c246b44a 100644 --- a/quantum/tests/unit/_test_api.py +++ b/quantum/tests/unit/_test_api.py @@ -157,9 +157,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_network_res.status_int, 200) network_data = self._deserialize_net_response(content_type, show_network_res) - self.assertEqual({'id': network_id, - 'name': self.network_name}, - network_data['network']) + self.assert_network(id=network_id, name=self.network_name, + network_data=network_data['network']) LOG.debug("_test_show_network - fmt:%s - END", fmt) def _test_show_network_detail(self, fmt): @@ -174,11 +173,9 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_network_res.status_int, 200) network_data = self._deserialize_net_response(content_type, show_network_res) - self.assertEqual({'id': network_id, - 'name': self.network_name, - 'ports': [{'id': port_id, - 'state': 'ACTIVE'}]}, - network_data['network']) + self.assert_network_details(id=network_id, name=self.network_name, + port_id=port_id, port_state='ACTIVE', + network_data=network_data['network']) LOG.debug("_test_show_network_detail - fmt:%s - END", fmt) def _test_show_network_not_found(self, fmt): @@ -208,9 +205,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_network_res.status_int, 200) network_data = self._deserialize_net_response(content_type, show_network_res) - self.assertEqual({'id': network_id, - 'name': new_name}, - network_data['network']) + self.assert_network(id=network_id, name=new_name, + network_data=network_data['network']) LOG.debug("_test_rename_network - fmt:%s - END", fmt) def _test_rename_network_badrequest(self, fmt): @@ -365,8 +361,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_port_res.status_int, 200) port_data = self._deserialize_port_response(content_type, show_port_res) - self.assertEqual({'id': port_id, 'state': port_state}, - port_data['port']) + self.assert_port(id=port_id, state=port_state, + port_data=port_data['port']) LOG.debug("_test_show_port - fmt:%s - END", fmt) def _test_show_port_detail(self, fmt): @@ -383,8 +379,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_port_res.status_int, 200) port_data = self._deserialize_port_response(content_type, show_port_res) - self.assertEqual({'id': port_id, 'state': port_state}, - port_data['port']) + self.assert_port(id=port_id, state=port_state, + port_data=port_data['port']) # Part 2 - plug attachment into port interface_id = "test_interface" @@ -401,9 +397,9 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_port_res.status_int, 200) port_data = self._deserialize_port_response(content_type, show_port_res) - self.assertEqual({'id': port_id, 'state': port_state, - 'attachment': {'id': interface_id}}, - port_data['port']) + self.assert_port_attachment(id=port_id, state=port_state, + interface_id=interface_id, + port_data=port_data['port']) LOG.debug("_test_show_port_detail - fmt:%s - END", fmt) @@ -575,8 +571,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_port_res.status_int, 200) port_data = self._deserialize_port_response(content_type, show_port_res) - self.assertEqual({'id': port_id, 'state': new_port_state}, - port_data['port']) + self.assert_port(id=port_id, state=new_port_state, + port_data=port_data['port']) # now set it back to the original value update_port_req = testlib.update_port_request(self.tenant_id, network_id, port_id, @@ -591,8 +587,8 @@ class AbstractAPITest(unittest.TestCase): self.assertEqual(show_port_res.status_int, 200) port_data = self._deserialize_port_response(content_type, show_port_res) - self.assertEqual({'id': port_id, 'state': port_state}, - port_data['port']) + self.assert_port(id=port_id, state=port_state, + port_data=port_data['port']) LOG.debug("_test_set_port_state - fmt:%s - END", fmt) def _test_set_port_state_networknotfound(self, fmt): diff --git a/quantum/tests/unit/test_api.py b/quantum/tests/unit/test_api.py index 5d637c9e540..57c5b4cb446 100644 --- a/quantum/tests/unit/test_api.py +++ b/quantum/tests/unit/test_api.py @@ -21,9 +21,33 @@ import quantum.api.networks as nets import quantum.api.ports as ports import quantum.tests.unit._test_api as test_api +from quantum.common.test_lib import test_config + class APITestV10(test_api.AbstractAPITest): + def assert_network(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'name': kwargs['name']}, + kwargs['network_data']) + + def assert_network_details(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'name': kwargs['name'], + 'ports': [{'id': kwargs['port_id'], + 'state': 'ACTIVE'}]}, + kwargs['network_data']) + + def assert_port(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'state': kwargs['state']}, + kwargs['port_data']) + + def assert_port_attachment(self, **kwargs): + self.assertEqual({'id': kwargs['id'], 'state': kwargs['state'], + 'attachment': {'id': kwargs['interface_id']}}, + kwargs['port_data']) + def setUp(self): super(APITestV10, self).setUp('quantum.api.APIRouterV10', {test_api.NETS: nets.ControllerV10._serialization_metadata, @@ -33,7 +57,38 @@ class APITestV10(test_api.AbstractAPITest): class APITestV11(test_api.AbstractAPITest): + def assert_network(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'name': kwargs['name'], + 'op-status': self.net_op_status}, + kwargs['network_data']) + + def assert_network_details(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'name': kwargs['name'], + 'op-status': self.net_op_status, + 'ports': [{'id': kwargs['port_id'], + 'state': 'ACTIVE', + 'op-status': self.port_op_status}]}, + kwargs['network_data']) + + def assert_port(self, **kwargs): + self.assertEqual({'id': kwargs['id'], + 'state': kwargs['state'], + 'op-status': self.port_op_status}, + kwargs['port_data']) + + def assert_port_attachment(self, **kwargs): + self.assertEqual({'id': kwargs['id'], 'state': kwargs['state'], + 'op-status': self.port_op_status, + 'attachment': {'id': kwargs['interface_id']}}, + kwargs['port_data']) + def setUp(self): + self.net_op_status = test_config.get('default_net_op_status', + 'UNKNOWN') + self.port_op_status = test_config.get('default_port_op_status', + 'UNKNOWN') super(APITestV11, self).setUp('quantum.api.APIRouterV11', {test_api.NETS: nets.ControllerV11._serialization_metadata, test_api.PORTS: ports.ControllerV11._serialization_metadata,