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
This commit is contained in:
Dan Wendlandt 2012-06-13 10:41:32 -07:00
parent 0230e96196
commit 44f66c79d2
9 changed files with 404 additions and 164 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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'])

View File

@ -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