From 44f66c79d225b651335b2c4c416d1d7ce5751527 Mon Sep 17 00:00:00 2001 From: Dan Wendlandt Date: Wed, 13 Jun 2012 10:41:32 -0700 Subject: [PATCH] API v2: mprove validation of post/put, rename few attributes bug #1012438 Additional work for bp v2-api-melange-integration - rename few attributes: op_state -> status additional_routes -> additional_host_routes prefix -> cidr - expand request body validation to indicate whether fields can be specified during create and or update. - add test cases to validate defaults, and input validation. - update db_base_plugin_v2 to generate gateway_ip for subnet and mac for port when unspecified. - validate that tenant-id is only specified in req by admin users - automatically set tenant-id based on request.context.tenant_id if needed - enable port tests in test_db_plugin.py Change-Id: If7f5101e4974a6ef93ff8a1d945f8642dd21b16e --- quantum/api/v2/base.py | 76 +++++++++-- quantum/api/v2/router.py | 63 +++++++-- quantum/api/v2/views.py | 6 +- quantum/db/db_base_plugin_v2.py | 39 ++++-- quantum/db/models_v2.py | 8 +- quantum/quantum_plugin_base_v2.py | 13 +- quantum/tests/unit/test_api_v2.py | 165 ++++++++++++++++++++-- quantum/tests/unit/test_db_plugin.py | 197 ++++++++++++++------------- tools/pip-requires | 1 + 9 files changed, 404 insertions(+), 164 deletions(-) diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index eff436b5f..cde4f62f8 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -93,11 +93,11 @@ def verbose(request): class Controller(object): - def __init__(self, plugin, collection, resource, params): + def __init__(self, plugin, collection, resource, attr_info): self._plugin = plugin self._collection = collection self._resource = resource - self._params = params + self._attr_info = attr_info self._view = getattr(views, self._resource) def _items(self, request): @@ -129,7 +129,9 @@ class Controller(object): def create(self, request, body=None): """Creates a new instance of the requested entity""" - body = self._prepare_request_body(body, allow_bulk=True) + + body = self._prepare_request_body(request.context, body, True, + allow_bulk=True) obj_creator = getattr(self._plugin, "create_%s" % self._resource) kwargs = {self._resource: body} @@ -144,15 +146,39 @@ class Controller(object): def update(self, request, id, body=None): """Updates the specified entity's attributes""" + body = self._prepare_request_body(request.context, body, False) obj_updater = getattr(self._plugin, "update_%s" % self._resource) kwargs = {self._resource: body} obj = obj_updater(request.context, id, **kwargs) return {self._resource: self._view(obj)} - def _prepare_request_body(self, body, allow_bulk=False): - """ verifies required parameters are in request body. - Parameters with default values are considered to be + def _populate_tenant_id(self, context, res_dict, is_create): + + if self._resource not in ['network', 'port']: + return + + if ('tenant_id' in res_dict and + res_dict['tenant_id'] != context.tenant_id and + not context.is_admin): + msg = _("Specifying 'tenant_id' other than authenticated" + "tenant in request requires admin privileges") + raise webob.exc.HTTPBadRequest(msg) + + if is_create and 'tenant_id' not in res_dict: + if context.tenant_id: + res_dict['tenant_id'] = context.tenant_id + else: + msg = _("Running without keystyone AuthN requires " + " that tenant_id is specified") + raise webob.exc.HTTPBadRequest(msg) + + def _prepare_request_body(self, context, body, is_create, + allow_bulk=False): + """ verifies required attributes are in request body, and that + an attribute is only specified if it is allowed for the given + operation (create/update). + Attribute with default values are considered to be optional. body argument must be the deserialized body @@ -163,9 +189,11 @@ class Controller(object): body = body or {self._resource: {}} if self._collection in body and allow_bulk: - bulk_body = [self._prepare_request_body({self._resource: b}) + bulk_body = [self._prepare_request_body(context, + {self._resource: b}, + is_create) if self._resource not in b - else self._prepare_request_body(b) + else self._prepare_request_body(context, b, is_create) for b in body[self._collection]] if not bulk_body: @@ -181,13 +209,31 @@ class Controller(object): msg = _("Unable to find '%s' in request body") % self._resource raise webob.exc.HTTPBadRequest(msg) - for param in self._params: - param_value = res_dict.get(param['attr'], param.get('default')) - if param_value is None: - msg = _("Failed to parse request. Parameter %s not " - "specified") % param - raise webob.exc.HTTPUnprocessableEntity(msg) - res_dict[param['attr']] = param_value + self._populate_tenant_id(context, res_dict, is_create) + + if is_create: # POST + for attr, attr_vals in self._attr_info.iteritems(): + is_required = ('default' not in attr_vals and + attr_vals['allow_post']) + if is_required and attr not in res_dict: + msg = _("Failed to parse request. Required " + " attribute '%s' not specified") % attr + raise webob.exc.HTTPUnprocessableEntity(msg) + + if not attr_vals['allow_post'] and attr in res_dict: + msg = _("Attribute '%s' not allowed in POST" % attr) + raise webob.exc.HTTPUnprocessableEntity(msg) + + if attr_vals['allow_post']: + res_dict[attr] = res_dict.get(attr, + attr_vals.get('default')) + + else: # PUT + for attr, attr_vals in self._attr_info.iteritems(): + if attr in res_dict and not attr_vals['allow_put']: + msg = _("Cannot update read-only attribute %s") % attr + raise webob.exc.HTTPUnprocessableEntity(msg) + return body diff --git a/quantum/api/v2/router.py b/quantum/api/v2/router.py index 6fdc9daa5..938ea200e 100644 --- a/quantum/api/v2/router.py +++ b/quantum/api/v2/router.py @@ -36,17 +36,56 @@ MEMBER_ACTIONS = ['show', 'update', 'delete'] REQUIREMENTS = {'id': UUID_PATTERN, 'format': 'xml|json'} -RESOURCE_PARAM_MAP = { - 'networks': [ - {'attr': 'name'}, - ], - 'ports': [ - {'attr': 'state', 'default': 'DOWN'}, - ], - 'subnets': [ - {'attr': 'prefix'}, - {'attr': 'network_id'}, - ] +ATTR_NOT_SPECIFIED = object() + +# Note: a default of ATTR_NOT_SPECIFIED indicates that an +# attribute is not required, but will be generated by the plugin +# if it is not specified. Particularly, a value of ATTR_NOT_SPECIFIED +# is different from an attribute that has been specified with a value of +# None. For example, if 'gateway_ip' is ommitted in a request to +# create a subnet, the plugin will receive ATTR_NOT_SPECIFIED +# and the default gateway_ip will be generated. +# However, if gateway_ip is specified as None, this means that +# the subnet does not have a gateway IP. + +RESOURCE_ATTRIBUTE_MAP = { + 'networks': { + 'id': {'allow_post': False, 'allow_put': False}, + 'name': {'allow_post': True, 'allow_put': True}, + 'subnets': {'allow_post': True, 'allow_put': True, 'default': []}, + 'admin_state_up': {'allow_post': True, 'allow_put': True, + 'default': True}, + 'status': {'allow_post': False, 'allow_put': False}, + 'tenant_id': {'allow_post': True, 'allow_put': True}, + }, + 'ports': { + 'id': {'allow_post': False, 'allow_put': False}, + 'network_id': {'allow_post': True, 'allow_put': False}, + 'admin_state_up': {'allow_post': True, 'allow_put': True, + 'default': True}, + 'mac_address': {'allow_post': True, 'allow_put': False, + 'default': ATTR_NOT_SPECIFIED}, + 'fixed_ips_v4': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + 'fixed_ips_v6': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + 'host_routes': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + 'device_id': {'allow_post': True, 'allow_put': True, 'default': ''}, + 'tenant_id': {'allow_post': True, 'allow_put': True}, + }, + 'subnets': { + 'id': {'allow_post': False, 'allow_put': False}, + 'ip_version': {'allow_post': True, 'allow_put': False}, + 'network_id': {'allow_post': True, 'allow_put': False}, + 'cidr': {'allow_post': True, 'allow_put': False}, + 'gateway_ip': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + 'dns_namesevers': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + 'additional_host_routes': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED}, + } } @@ -113,7 +152,7 @@ class APIRouter(wsgi.Router): mapper.connect('index', '/', controller=Index(resources)) for resource in resources: _map_resource(resources[resource], resource, - RESOURCE_PARAM_MAP.get(resources[resource], + RESOURCE_ATTRIBUTE_MAP.get(resources[resource], dict())) super(APIRouter, self).__init__(mapper) diff --git a/quantum/api/v2/views.py b/quantum/api/v2/views.py index 932607e61..843a976c4 100644 --- a/quantum/api/v2/views.py +++ b/quantum/api/v2/views.py @@ -22,13 +22,13 @@ def resource(data, keys): def port(port_data): """Represents a view for a port object""" keys = ('id', 'network_id', 'mac_address', 'fixed_ips', - 'device_id', 'admin_state_up', 'tenant_id', 'op_status') + 'device_id', 'admin_state_up', 'tenant_id', 'status') return resource(port_data, keys) def network(network_data): """Represents a view for a network object""" - keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status', + keys = ('id', 'name', 'subnets', 'admin_state_up', 'status', 'tenant_id', 'mac_ranges') return resource(network_data, keys) @@ -36,5 +36,5 @@ def network(network_data): def subnet(subnet_data): """Represents a view for a subnet object""" keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', 'ip_version', - 'prefix') + 'cidr') return resource(subnet_data, keys) diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index a1dbf59a5..4825724ab 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -14,11 +14,14 @@ # limitations under the License. import logging +import random +import netaddr from sqlalchemy import orm from sqlalchemy.orm import exc from quantum import quantum_plugin_base_v2 +from quantum.api.v2 import router as api_router from quantum.common import exceptions as q_exc from quantum.db import api as db from quantum.db import models_v2 @@ -132,7 +135,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): 'name': network['name'], 'tenant_id': network['tenant_id'], 'admin_state_up': network['admin_state_up'], - 'op_status': network['op_status'], + 'status': network['status'], 'subnets': [subnet['id'] for subnet in network['subnets']]} @@ -141,9 +144,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): def _make_subnet_dict(self, subnet, fields=None): res = {'id': subnet['id'], 'network_id': subnet['network_id'], - 'tenant_id': subnet['tenant_id'], 'ip_version': subnet['ip_version'], - 'prefix': subnet['prefix'], + 'cidr': subnet['cidr'], 'gateway_ip': subnet['gateway_ip']} return self._fields(res, fields) @@ -153,7 +155,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): 'tenant_id': port['tenant_id'], "mac_address": port["mac_address"], "admin_state_up": port["admin_state_up"], - "op_status": port["op_status"], + "status": port["status"], "fixed_ips": [ip["address"] for ip in port["fixed_ips"]], "device_id": port["device_id"]} return self._fields(res, fields) @@ -168,7 +170,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): network = models_v2.Network(tenant_id=tenant_id, name=n['name'], admin_state_up=n['admin_state_up'], - op_status="ACTIVE") + status="ACTIVE") context.session.add(network) return self._make_network_dict(network) @@ -204,14 +206,15 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): def create_subnet(self, context, subnet): s = subnet['subnet'] - # NOTE(jkoelker) Get the tenant_id outside of the session to avoid - # unneeded db action if the operation raises - tenant_id = self._get_tenant_id_for_create(context, s) + + if s['gateway_ip'] == api_router.ATTR_NOT_SPECIFIED: + net = netaddr.IPNetwork(s['cidr']) + s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1)) + with context.session.begin(): - subnet = models_v2.Subnet(tenant_id=tenant_id, - network_id=s['network_id'], + subnet = models_v2.Subnet(network_id=s['network_id'], ip_version=s['ip_version'], - prefix=s['prefix'], + cidr=s['cidr'], gateway_ip=s['gateway_ip']) context.session.add(subnet) @@ -249,16 +252,22 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): # unneeded db action if the operation raises tenant_id = self._get_tenant_id_for_create(context, p) - #FIXME(danwent): allocate MAC - mac_address = p.get('mac_address', 'ca:fe:de:ad:be:ef') + if p['mac_address'] == api_router.ATTR_NOT_SPECIFIED: + #FIXME(danwent): this is exact Nova mac generation logic + # we will want to provide more flexibility and to check + # for uniqueness. + mac = [0xfa, 0x16, 0x3e, random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), random.randint(0x00, 0xff)] + p['mac_address'] = ':'.join(map(lambda x: "%02x" % x, mac)) + with context.session.begin(): network = self._get_network(context, p["network_id"]) port = models_v2.Port(tenant_id=tenant_id, network_id=p['network_id'], - mac_address=mac_address, + mac_address=p['mac_address'], admin_state_up=p['admin_state_up'], - op_status="ACTIVE", + status="ACTIVE", device_id=p['device_id']) context.session.add(port) diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index 82b5ec30e..f03a78456 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -43,18 +43,18 @@ class Port(model_base.BASEV2, HasTenant): fixed_ips = orm.relationship(IPAllocation, backref='ports') mac_address = sa.Column(sa.String(32), nullable=False) admin_state_up = sa.Column(sa.Boolean(), nullable=False) - op_status = sa.Column(sa.String(16), nullable=False) + status = sa.Column(sa.String(16), nullable=False) device_id = sa.Column(sa.String(255), nullable=False) -class Subnet(model_base.BASEV2, HasTenant): +class Subnet(model_base.BASEV2): """Represents a quantum subnet""" network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id')) allocations = orm.relationship(IPAllocation, backref=orm.backref('subnet', uselist=False)) ip_version = sa.Column(sa.Integer, nullable=False) - prefix = sa.Column(sa.String(255), nullable=False) + cidr = sa.Column(sa.String(64), nullable=False) gateway_ip = sa.Column(sa.String(255)) #TODO(danwent): @@ -68,5 +68,5 @@ class Network(model_base.BASEV2, HasTenant): name = sa.Column(sa.String(255)) ports = orm.relationship(Port, backref='networks') subnets = orm.relationship(Subnet, backref='networks') - op_status = sa.Column(sa.String(16)) + status = sa.Column(sa.String(16)) admin_state_up = sa.Column(sa.Boolean) diff --git a/quantum/quantum_plugin_base_v2.py b/quantum/quantum_plugin_base_v2.py index 47d068e44..262f0d4ad 100644 --- a/quantum/quantum_plugin_base_v2.py +++ b/quantum/quantum_plugin_base_v2.py @@ -39,19 +39,20 @@ class QuantumPluginBaseV2(object): is bound. "ip_version": integer indicating IP protocol version. example: 4 - "prefix": string indicating IP prefix indicating addresses - that can be allocated for devices on this subnet. - example: "10.0.0.0/24" + "cidr": string indicating IP prefix indicating addresses + that can be allocated for devices on this subnet. + example: "10.0.0.0/24" "gateway_ip": string indicating the default gateway for devices on this subnet. example: "10.0.0.1" "dns_nameservers": list of strings stricting indication the DNS name servers for devices on this subnet. example: [ "8.8.8.8", "8.8.4.4" ] - "excluded_ranges" : list of dicts indicating pairs of IPs that - should not be allocated from the prefix. + "reserved_ranges" : list of dicts indicating pairs of IPs that + should not be automatically allocated from + the prefix. example: [ { "start" : "10.0.0.2", "end" : "10.0.0.5" } ] - "additional_routes": list of dicts indicating routes beyond + "additional_host_routes": list of dicts indicating routes beyond the default gateway and local prefix route that should be injected into the device. example: [{"destination": "192.168.0.0/16", diff --git a/quantum/tests/unit/test_api_v2.py b/quantum/tests/unit/test_api_v2.py index b7f8c5f87..06e8c4148 100644 --- a/quantum/tests/unit/test_api_v2.py +++ b/quantum/tests/unit/test_api_v2.py @@ -21,6 +21,7 @@ import webtest from webob import exc +from quantum import context from quantum.common import exceptions as q_exc from quantum.api.v2 import resource as wsgi_resource from quantum.api.v2 import router @@ -30,6 +31,10 @@ from quantum.api.v2 import views LOG = logging.getLogger(__name__) +def _uuid(): + return str(uuid.uuid4()) + + def _get_path(resource, id=None, fmt=None): path = '/%s' % resource @@ -354,27 +359,107 @@ class APIv2TestCase(unittest.TestCase): verbose=True) +# Note: since all resources use the same controller and validation +# logic, we actually get really good coverage from testing just networks. class JSONV2TestCase(APIv2TestCase): def test_list(self): - return_value = [{'network': {'name': 'net1', - 'admin_state_up': True, - 'subnets': []}}] + input_dict = {'id': str(uuid.uuid4()), + 'name': 'net1', + 'admin_state_up': True, + 'status': "ACTIVE", + 'tenant_id': str(uuid.uuid4()), + 'subnets': []} + return_value = [input_dict] instance = self.plugin.return_value instance.get_networks.return_value = return_value res = self.api.get(_get_path('networks')) self.assertTrue('networks' in res.json) + self.assertEqual(len(res.json['networks']), 1) + output_dict = res.json['networks'][0] + self.assertEqual(len(input_dict), len(output_dict)) + for k, v in input_dict.iteritems(): + self.assertEqual(v, output_dict[k]) def test_create(self): - data = {'network': {'name': 'net1', 'admin_state_up': True}} - return_value = {'subnets': []} + net_id = _uuid() + data = {'network': {'name': 'net1', 'admin_state_up': True, + 'tenant_id': _uuid()}} + return_value = {'subnets': [], 'status': "ACTIVE", + 'id': net_id} return_value.update(data['network'].copy()) instance = self.plugin.return_value instance.create_network.return_value = return_value res = self.api.post_json(_get_path('networks'), data) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + self.assertTrue('network' in res.json) + net = res.json['network'] + self.assertEqual(net['id'], net_id) + self.assertEqual(net['status'], "ACTIVE") + + def test_create_use_defaults(self): + net_id = _uuid() + initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid()}} + full_input = {'network': {'admin_state_up': True, 'subnets': []}} + full_input['network'].update(initial_input['network']) + + return_value = {'id': net_id, 'status': "ACTIVE"} + return_value.update(full_input['network']) + + instance = self.plugin.return_value + instance.create_network.return_value = return_value + + res = self.api.post_json(_get_path('networks'), initial_input) + + instance.create_network.assert_called_with(mock.ANY, + network=full_input) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + self.assertTrue('network' in res.json) + net = res.json['network'] + self.assertEqual(net['id'], net_id) + self.assertEqual(net['admin_state_up'], True) + self.assertEqual(net['status'], "ACTIVE") + + def test_create_no_keystone_env(self): + data = {'name': 'net1'} + res = self.api.post_json(_get_path('networks'), data, + expect_errors=True) + self.assertEqual(res.status_int, exc.HTTPBadRequest.code) + + def test_create_with_keystone_env(self): + tenant_id = _uuid() + net_id = _uuid() + env = {'quantum.context': context.Context('', tenant_id)} + # tenant_id should be fetched from env + initial_input = {'network': {'name': 'net1'}} + full_input = {'network': {'admin_state_up': True, 'subnets': [], + 'tenant_id': tenant_id}} + full_input['network'].update(initial_input['network']) + + return_value = {'id': net_id, 'status': "ACTIVE"} + return_value.update(full_input['network']) + + instance = self.plugin.return_value + instance.create_network.return_value = return_value + + res = self.api.post_json(_get_path('networks'), initial_input, + extra_environ=env) + + instance.create_network.assert_called_with(mock.ANY, + network=full_input) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + + def test_create_bad_keystone_tenant(self): + tenant_id = _uuid() + data = {'network': {'name': 'net1', 'tenant_id': tenant_id}} + env = {'quantum.context': context.Context('', tenant_id + "bad")} + res = self.api.post_json(_get_path('networks'), data, + expect_errors=True, + extra_environ=env) + self.assertEqual(res.status_int, exc.HTTPBadRequest.code) def test_create_no_body(self): data = {'whoa': None} @@ -388,14 +473,23 @@ class JSONV2TestCase(APIv2TestCase): self.assertEqual(res.status_int, exc.HTTPBadRequest.code) def test_create_missing_attr(self): - data = {'network': {'what': 'who'}} + data = {'network': {'what': 'who', 'tenant_id': _uuid()}} + res = self.api.post_json(_get_path('networks'), data, + expect_errors=True) + self.assertEqual(res.status_int, 422) + + def test_create_readonly_attr(self): + data = {'network': {'name': 'net1', 'tenant_id': _uuid(), + 'status': "ACTIVE"}} res = self.api.post_json(_get_path('networks'), data, expect_errors=True) self.assertEqual(res.status_int, 422) def test_create_bulk(self): - data = {'networks': [{'name': 'net1', 'admin_state_up': True}, - {'name': 'net2', 'admin_state_up': True}]} + data = {'networks': [{'name': 'net1', 'admin_state_up': True, + 'tenant_id': _uuid()}, + {'name': 'net2', 'admin_state_up': True, + 'tenant_id': _uuid()}]} def side_effect(context, network): nets = network.copy() @@ -416,18 +510,52 @@ class JSONV2TestCase(APIv2TestCase): self.assertEqual(res.status_int, exc.HTTPBadRequest.code) def test_create_bulk_missing_attr(self): - data = {'networks': [{'what': 'who'}]} + data = {'networks': [{'what': 'who', 'tenant_id': _uuid()}]} res = self.api.post_json(_get_path('networks'), data, expect_errors=True) self.assertEqual(res.status_int, 422) def test_create_bulk_partial_body(self): - data = {'networks': [{'name': 'net1', 'admin_state_up': True}, - {}]} + data = {'networks': [{'name': 'net1', 'admin_state_up': True, + 'tenant_id': _uuid()}, + {'tenant_id': _uuid()}]} res = self.api.post_json(_get_path('networks'), data, expect_errors=True) self.assertEqual(res.status_int, 422) + def test_create_attr_not_specified(self): + net_id = _uuid() + tenant_id = _uuid() + device_id = _uuid() + initial_input = {'port': {'network_id': net_id, 'tenant_id': tenant_id, + 'device_id': device_id, + 'admin_state_up': True}} + full_input = {'port': {'admin_state_up': True, + 'mac_address': router.ATTR_NOT_SPECIFIED, + 'fixed_ips_v4': router.ATTR_NOT_SPECIFIED, + 'fixed_ips_v6': router.ATTR_NOT_SPECIFIED, + 'host_routes': router.ATTR_NOT_SPECIFIED}} + full_input['port'].update(initial_input['port']) + return_value = {'id': _uuid(), 'status': 'ACTIVE', + 'admin_state_up': True, + 'mac_address': 'ca:fe:de:ad:be:ef', + 'fixed_ips_v4': ['10.0.0.0/24'], + 'fixed_ips_v6': [], + 'host_routes': [], + 'device_id': device_id} + return_value.update(initial_input['port']) + + instance = self.plugin.return_value + instance.create_port.return_value = return_value + res = self.api.post_json(_get_path('ports'), initial_input) + + instance.create_port.assert_called_with(mock.ANY, port=full_input) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + self.assertTrue('port' in res.json) + port = res.json['port'] + self.assertEqual(port['network_id'], net_id) + self.assertEqual(port['mac_address'], 'ca:fe:de:ad:be:ef') + def test_fields(self): return_value = {'name': 'net1', 'admin_state_up': True, 'subnets': []} @@ -445,7 +573,8 @@ class JSONV2TestCase(APIv2TestCase): self.assertEqual(res.status_int, exc.HTTPNoContent.code) def test_update(self): - data = {'network': {'name': 'net1', 'admin_state_up': True}} + # leave out 'name' field intentionally + data = {'network': {'admin_state_up': True}} return_value = {'subnets': []} return_value.update(data['network'].copy()) @@ -455,6 +584,12 @@ class JSONV2TestCase(APIv2TestCase): self.api.put_json(_get_path('networks', id=str(uuid.uuid4())), data) + def test_update_readonly_field(self): + data = {'network': {'status': "NANANA"}} + res = self.api.put_json(_get_path('networks', id=_uuid()), data, + expect_errors=True) + self.assertEqual(res.status_int, 422) + class V2Views(unittest.TestCase): def _view(self, keys, func): @@ -471,16 +606,16 @@ class V2Views(unittest.TestCase): self.assertTrue('two' not in res) def test_network(self): - keys = ('id', 'name', 'subnets', 'admin_state_up', 'op_status', + keys = ('id', 'name', 'subnets', 'admin_state_up', 'status', 'tenant_id', 'mac_ranges') self._view(keys, views.network) def test_port(self): keys = ('id', 'network_id', 'mac_address', 'fixed_ips', - 'device_id', 'admin_state_up', 'tenant_id', 'op_status') + 'device_id', 'admin_state_up', 'tenant_id', 'status') self._view(keys, views.port) def test_subnet(self): keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', - 'ip_version', 'prefix') + 'ip_version', 'cidr') self._view(keys, views.subnet) diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index f4a6670ef..2c0dcafa3 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -86,22 +86,41 @@ class QuantumDbPluginV2TestCase(unittest.TestCase): def _create_network(self, fmt, name, admin_status_up): data = {'network': {'name': name, - 'admin_state_up': admin_status_up}} + 'admin_state_up': admin_status_up, + 'tenant_id': self._tenant_id}} network_req = self.new_create_request('networks', data, fmt) return network_req.get_response(self.api) - def _create_subnet(self, fmt, net_id, gateway_ip, prefix): + def _create_subnet(self, fmt, net_id, gateway_ip, cidr): data = {'subnet': {'network_id': net_id, - 'allocations': [], - 'prefix': prefix, - 'ip_version': 4, - 'gateway_ip': gateway_ip}} + 'cidr': cidr, + 'ip_version': 4}} + if gateway_ip: + data['subnet']['gateway_ip'] = gateway_ip + subnet_req = self.new_create_request('subnets', data, fmt) return subnet_req.get_response(self.api) - def _make_subnet(self, fmt, network, gateway, prefix): + def _create_port(self, fmt, net_id, custom_req_body=None, + expected_res_status=None, **kwargs): + content_type = 'application/' + fmt + data = {'port': {'network_id': net_id, + 'tenant_id': self._tenant_id}} + for arg in ('admin_state_up', 'device_id', 'mac_address', + 'fixed_ips_v4', 'fixed_ips_v6'): + if arg in kwargs: + data['port'][arg] = kwargs[arg] + + port_req = self.new_create_request('ports', data, fmt) + return port_req.get_response(self.api) + + def _make_subnet(self, fmt, network, gateway, cidr): res = self._create_subnet(fmt, network['network']['id'], - gateway, prefix) + gateway, cidr) + return self.deserialize(fmt, res) + + def _make_port(self, fmt, net_id, **kwargs): + res = self._create_port(fmt, net_id, **kwargs) return self.deserialize(fmt, res) def _delete(self, collection, id): @@ -116,19 +135,28 @@ class QuantumDbPluginV2TestCase(unittest.TestCase): self._delete('networks', network['network']['id']) @contextlib.contextmanager - def subnet(self, network=None, gateway='10.0.0.1', - prefix='10.0.0.0/24', fmt='json'): + def subnet(self, network=None, gateway=None, + cidr='10.0.0.0/24', fmt='json'): # TODO(anyone) DRY this if not network: with self.network() as network: - subnet = self._make_subnet(fmt, network, gateway, prefix) + subnet = self._make_subnet(fmt, network, gateway, cidr) yield subnet self._delete('subnets', subnet['subnet']['id']) else: - subnet = self._make_subnet(fmt, network, gateway, prefix) + subnet = self._make_subnet(fmt, network, gateway, cidr) yield subnet self._delete('subnets', subnet['subnet']['id']) + @contextlib.contextmanager + def port(self, subnet=None, fmt='json'): + if not subnet: + with self.subnet() as subnet: + net_id = subnet['subnet']['network_id'] + port = self._make_port(fmt, net_id) + yield port + self._delete('ports', port['port']['id']) + class TestV2HTTPResponse(QuantumDbPluginV2TestCase): def test_create_returns_201(self): @@ -167,74 +195,45 @@ class TestV2HTTPResponse(QuantumDbPluginV2TestCase): self.assertEquals(res.status_int, 404) -#class TestPortsV2(APIv2TestCase): -# def setUp(self): -# super(TestPortsV2, self).setUp() -# res = self._create_network('json', 'net1', True) -# data = self._deserializers['application/json'].\ -# deserialize(res.body)['body'] -# self.net_id = data['network']['id'] -# -# def _create_port(self, fmt, net_id, admin_state_up, device_id, -# custom_req_body=None, -# expected_res_status=None): -# content_type = 'application/' + fmt -# data = {'port': {'network_id': net_id, -# 'admin_state_up': admin_state_up, -# 'device_id': device_id}} -# port_req = self.new_create_request('ports', data, fmt) -# port_res = port_req.get_response(self.api) -# return json.loads(port_res.body) -# -# def test_create_port_json(self): -# port = self._create_port('json', self.net_id, True, 'dev_id_1') -# self.assertEqual(port['id'], 'dev_id_1') -# self.assertEqual(port['admin_state_up'], 'DOWN') -# self.assertEqual(port['device_id'], 'dev_id_1') -# self.assertTrue('mac_address' in port) -# self.assertTrue('op_status' in port) -# -# def test_list_ports(self): -# port1 = self._create_port('json', self.net_id, True, 'dev_id_1') -# port2 = self._create_port('json', self.net_id, True, 'dev_id_2') -# -# res = self.new_list_request('ports', 'json') -# port_list = json.loads(res.body)['body'] -# self.assertTrue(port1 in port_list['ports']) -# self.assertTrue(port2 in port_list['ports']) -# -# def test_show_port(self): -# port = self._create_port('json', self.net_id, True, 'dev_id_1') -# res = self.new_show_request('port', 'json', port['id']) -# port = json.loads(res.body)['body'] -# self.assertEquals(port['port']['name'], 'dev_id_1') -# -# def test_delete_port(self): -# port = self._create_port('json', self.net_id, True, 'dev_id_1') -# self.new_delete_request('port', 'json', port['id']) -# -# port = self.new_show_request('port', 'json', port['id']) -# -# self.assertEquals(res.status_int, 404) -# -# def test_update_port(self): -# port = self._create_port('json', self.net_id, True, 'dev_id_1') -# port_body = {'port': {'device_id': 'Bob'}} -# res = self.new_update_request('port', port_body, port['id']) -# port = json.loads(res.body)['body'] -# self.assertEquals(port['device_id'], 'Bob') -# -# def test_delete_non_existent_port_404(self): -# res = self.new_delete_request('port', 'json', 1) -# self.assertEquals(res.status_int, 404) -# -# def test_show_non_existent_port_404(self): -# res = self.new_show_request('port', 'json', 1) -# self.assertEquals(res.status_int, 404) -# -# def test_update_non_existent_port_404(self): -# res = self.new_update_request('port', 'json', 1) -# self.assertEquals(res.status_int, 404) +class TestPortsV2(QuantumDbPluginV2TestCase): + + def test_create_port_json(self): + keys = [('admin_state_up', True), ('status', 'ACTIVE')] + with self.port() as port: + for k, v in keys: + self.assertEquals(port['port'][k], v) + self.assertTrue('mac_address' in port['port']) + + def test_list_ports(self): + with contextlib.nested(self.port(), self.port()) as (port1, port2): + req = self.new_list_request('ports', 'json') + port_list = self.deserialize('json', req.get_response(self.api)) + self.assertEqual(len(port_list['ports']), 2) + ids = [p['id'] for p in port_list['ports']] + self.assertTrue(port1['port']['id'] in ids) + self.assertTrue(port2['port']['id'] in ids) + + def test_show_port(self): + with self.port() as port: + req = self.new_show_request('ports', port['port']['id'], 'json') + sport = self.deserialize('json', req.get_response(self.api)) + self.assertEquals(port['port']['id'], sport['port']['id']) + + def test_delete_port(self): + port_id = None + with self.port() as port: + port_id = port['port']['id'] + req = self.new_show_request('port', 'json', port['port']['id']) + res = req.get_response(self.api) + self.assertEquals(res.status_int, 404) + + def test_update_port(self): + with self.port() as port: + data = {'port': {'admin_state_up': False}} + req = self.new_update_request('ports', data, port['port']['id']) + res = self.deserialize('json', req.get_response(self.api)) + self.assertEqual(res['port']['admin_state_up'], + data['port']['admin_state_up']) class TestNetworksV2(QuantumDbPluginV2TestCase): @@ -243,7 +242,7 @@ class TestNetworksV2(QuantumDbPluginV2TestCase): def test_create_network(self): name = 'net1' keys = [('subnets', []), ('name', name), ('admin_state_up', True), - ('op_status', 'ACTIVE')] + ('status', 'ACTIVE')] with self.network(name=name) as net: for k, v in keys: self.assertEquals(net['network'][k], v) @@ -268,24 +267,34 @@ class TestNetworksV2(QuantumDbPluginV2TestCase): class TestSubnetsV2(QuantumDbPluginV2TestCase): + def test_create_subnet(self): gateway = '10.0.0.1' - prefix = '10.0.0.0/24' + cidr = '10.0.0.0/24' keys = [('ip_version', 4), ('gateway_ip', gateway), - ('prefix', prefix)] - with self.subnet(gateway=gateway, prefix=prefix) as subnet: + ('cidr', cidr)] + with self.subnet(gateway=gateway, cidr=cidr) as subnet: + for k, v in keys: + self.assertEquals(subnet['subnet'][k], v) + + def test_create_subnet_defaults(self): + generated_gateway = '10.0.0.1' + cidr = '10.0.0.0/24' + keys = [('ip_version', 4), ('gateway_ip', generated_gateway), + ('cidr', cidr)] + # intentionally not passing gateway in + with self.subnet(cidr=cidr) as subnet: for k, v in keys: self.assertEquals(subnet['subnet'][k], v) def test_update_subnet(self): with self.subnet() as subnet: - data = {'subnet': {'network_id': 'blarg', - 'prefix': '192.168.0.0/24'}} + data = {'subnet': {'gateway_ip': '11.0.0.1'}} req = self.new_update_request('subnets', data, subnet['subnet']['id']) res = self.deserialize('json', req.get_response(self.api)) - self.assertEqual(res['subnet']['prefix'], - data['subnet']['prefix']) + self.assertEqual(res['subnet']['gateway_ip'], + data['subnet']['gateway_ip']) def test_show_subnet(self): with self.network() as network: @@ -303,15 +312,15 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase): # or just drop 2.6 support ;) with self.network() as network: with self.subnet(network=network, gateway='10.0.0.1', - prefix='10.0.1.0/24') as subnet: + cidr='10.0.1.0/24') as subnet: with self.subnet(network=network, gateway='10.0.1.1', - prefix='10.0.1.0/24') as subnet2: + cidr='10.0.1.0/24') as subnet2: req = self.new_list_request('subnets') res = self.deserialize('json', req.get_response(self.api)) res1 = res['subnets'][0] res2 = res['subnets'][1] - self.assertEquals(res1['prefix'], - subnet['subnet']['prefix']) - self.assertEquals(res2['prefix'], - subnet2['subnet']['prefix']) + self.assertEquals(res1['cidr'], + subnet['subnet']['cidr']) + self.assertEquals(res2['cidr'], + subnet2['subnet']['cidr']) diff --git a/tools/pip-requires b/tools/pip-requires index ea254aad5..18fdd91e6 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -3,6 +3,7 @@ PasteDeploy==1.5.0 Routes>=1.12.3 eventlet>=0.9.12 lxml +netaddr python-gflags==1.3 sqlalchemy>0.6.4 webob==1.2.0 -- 2.45.2