Basic VIP management handlers added to nailgun API

New VIP handlers is introduced that allows list VIP for cluster and get or update
specific VIP.

/clusters/:cluster_id/network_configuration/ips/vips
/clusters/:cluster_id/network_configuration/ips/:ip_address_id/vips

Partial-Bug: #1482399
Implements Blueprint: allow-any-vip
Change-Id: I4a4058c112bbdb26ab410e5e97b4e1e3afe7a082
This commit is contained in:
Ilya Kutukov 2015-12-16 14:25:37 +03:00 committed by Alexey Shtokolov
parent 169fae21b0
commit 8f28676cf6
11 changed files with 1119 additions and 1 deletions

View File

@ -340,6 +340,14 @@ def content(*args, **kwargs):
def GET(self):
...
"""
# TODO(ikutukov): this decorator is not coherent and doing more
# than just a response mimetype setting via type-specific content_json
# method that perform validation.
# Before you start to implement handler business logic ensure that
# @content decorator not already doing what you are planning to write.
# I think that validation routine and common http headers formation not
# depending on each other and should be decoupled. At least they should
# not be under one decorator with abstract name.
exact_mimetypes = None
if len(args) >= 1 and isinstance(args[0], list):

View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import web
from nailgun.api.v1.handlers import base
from nailgun.api.v1.handlers.base import content
from nailgun.api.v1.validators import ip_addr
from nailgun import objects
class ClusterVIPHandler(base.SingleHandler):
validator = ip_addr.IPAddrValidator
single = objects.IPAddr
def _get_vip_from_cluster_or_http_error(self, cluster_id, ip_addr_id):
obj = self.get_object_or_404(self.single, ip_addr_id)
if cluster_id != obj.network_data.nodegroup.cluster_id:
raise self.http(
404,
"IP address with (ID={0}) does not belong to "
"cluster (ID={1})".format(ip_addr_id, cluster_id)
)
elif not obj.vip_name:
raise self.http(
400,
"IP address with (ID={0}) exists but has no "
"VIP metadata attached".format(ip_addr_id)
)
else:
return obj
def GET(self, cluster_id, ip_addr_id):
"""Get VIP record.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:parameter ip_addr_id: ip_addr record identifier.
:type ip_addr_id: basestring
:returns: JSON-serialised IpAddr object.
:http: * 200 (OK)
* 400 (data validation failed)
* 404 (ip_addr entry not found in db)
"""
obj = self._get_vip_from_cluster_or_http_error(
int(cluster_id), int(ip_addr_id))
return self.single.to_json(obj)
@content
def PUT(self, cluster_id, ip_addr_id):
"""Update VIP record.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:parameter ip_addr_id: ip_addr record identifier.
:type ip_addr_id: basestring
:returns: JSON-serialised IpAddr object.
:http: * 200 (OK)
* 400 (data validation failed)
* 404 (ip_addr entry not found in db)
"""
obj = self._get_vip_from_cluster_or_http_error(
int(cluster_id), int(ip_addr_id))
data = self.checked_data(
self.validator.validate_update,
existing_obj=obj
)
self.single.update(obj, data)
return self.single.to_json(obj)
def PATCH(self, cluster_id, ip_addr_id):
"""Update VIP record.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:parameter ip_addr_id: ip_addr record identifier.
:type ip_addr_id: basestring
:returns: JSON-serialised IpAddr object.
:http: * 200 (OK)
* 400 (data validation failed)
* 404 (ip_addr entry not found in db)
"""
return self.PUT(cluster_id, ip_addr_id)
def DELETE(self, cluster_id, ip_addr_id):
"""Delete method is disallowed.
:http: * 405 (method not supported)
"""
raise self.http(405, 'Delete is not supported for this entity')
class ClusterVIPCollectionHandler(base.CollectionHandler):
collection = objects.IPAddrCollection
validator = ip_addr.IPAddrValidator
@content
def GET(self, cluster_id):
"""Get VIPs collection optionally filtered by network or network role.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:returns: Collection of JSON-serialised IpAddr objects.
:http: * 200 (OK)
* 404 (cluster not found in db)
"""
network_id = web.input(network_id=None).network_id
network_role = web.input(network_role=None).network_role
self.get_object_or_404(objects.Cluster, int(cluster_id))
return self.collection.to_json(
self.collection.get_vips_by_cluster_id(
int(cluster_id),
network_id,
network_role
)
)
def POST(self, cluster_id):
"""Create method disallowed.
:http: * 405 (method not supported)
"""
raise self.http(405, 'Create is not supported for this entity')
@content
def PUT(self, cluster_id):
"""Update VIPs collection.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:returns: Collection of JSON-serialised updated IpAddr objects.
:http: * 200 (OK)
* 400 (data validation failed)
"""
update_data = self.checked_data(
self.validator.validate_collection_update,
cluster_id=int(cluster_id)
)
return self.collection.to_json(
self.collection.update_vips(update_data)
)
def PATCH(self, cluster_id):
"""Update VIPs collection.
:parameter cluster_id: cluster identifier.
:type cluster_id: basestring
:returns: Collection of JSON-serialised updated IpAddr objects.
:http: * 200 (OK)
* 400 (data validation failed)
"""
return self.PUT(cluster_id)

View File

@ -43,6 +43,9 @@ from nailgun.api.v1.handlers.cluster_plugin_link \
from nailgun.api.v1.handlers.cluster_plugin_link \
import ClusterPluginLinkHandler
from nailgun.api.v1.handlers.vip import ClusterVIPCollectionHandler
from nailgun.api.v1.handlers.vip import ClusterVIPHandler
from nailgun.api.v1.handlers.logs import LogEntryCollectionHandler
from nailgun.api.v1.handlers.logs import LogPackageDefaultConfig
from nailgun.api.v1.handlers.logs import LogPackageHandler
@ -231,6 +234,13 @@ urls = (
r'/clusters/(?P<cluster_id>\d+)/plugin_links/(?P<obj_id>\d+)/?$',
ClusterPluginLinkHandler,
r'/clusters/(?P<cluster_id>\d+)/network_configuration'
r'/ips/vips/?$',
ClusterVIPCollectionHandler,
r'/clusters/(?P<cluster_id>\d+)/network_configuration'
r'/ips/(?P<ip_addr_id>\d+)/vips/?$',
ClusterVIPHandler,
r'/nodegroups/?$',
NodeGroupCollectionHandler,
r'/nodegroups/(?P<obj_id>\d+)/?$',

View File

@ -15,6 +15,7 @@
import jsonschema
from jsonschema.exceptions import ValidationError
import six
from oslo_serialization import jsonutils
@ -29,6 +30,8 @@ class BasicValidator(object):
@classmethod
def validate_json(cls, data):
# todo(ikutukov): this method not only validation json but also
# returning parsed data
if data:
try:
res = jsonutils.loads(data)
@ -60,7 +63,12 @@ class BasicValidator(object):
except ValidationError as exc:
if len(exc.path) > 0:
raise errors.InvalidData(
": ".join([exc.path.pop(), exc.message])
# NOTE(ikutukov): here was a exc.path.pop(). It was buggy
# because JSONSchema error path could contain integers
# and joining integers as string is not a good idea in
# python. So some schema error messages were not working
# properly and give 500 error code except 400.
": ".join([six.text_type(exc.path), exc.message])
)
raise errors.InvalidData(exc.message)

View File

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
from oslo_serialization import jsonutils
from nailgun.api.v1.validators.base import BasicValidator
from nailgun.api.v1.validators.json_schema import ip_addr
from nailgun.db.sqlalchemy import models
from nailgun.errors import errors
from nailgun import objects
class IPAddrValidator(BasicValidator):
single_schema = ip_addr.IP_ADDR_UPDATE_SCHEMA
collection_schema = ip_addr.IP_ADDRS_UPDATE_SCHEMA
updatable_fields = (
"ip_addr",
"is_user_defined",
"vip_namespace",
)
@classmethod
def validate_update(cls, data, existing_obj):
"""Validate single IP address entry update information.
:param data: new data
:type data: dict
:param existing_obj: existing object
:type existing_obj: instance if fuel.objects.IPAddr
:return: validated data
:rtype: dict
"""
if isinstance(data, six.string_types):
data = cls.validate_json(data)
existing_data = dict(existing_obj)
bad_fields = []
for field, value in six.iteritems(data):
old_value = existing_data.get(field)
# field that not allowed to be changed is changed
if value != old_value and field not in cls.updatable_fields:
bad_fields.append(field)
if bad_fields:
bad_fields_verbose = ", ".join(repr(bf) for bf in bad_fields)
raise errors.InvalidData(
"\n".join([
"The following fields: {0} are not allowed to be "
"updated for record: {1}".format(
bad_fields_verbose,
jsonutils.dumps(data)
)
])
)
return data
@classmethod
def validate_collection_update(cls, data, cluster_id):
"""Validate IP address collection update information.
:param data: new data
:type data: list(dict)
:param cluster_id: if od objects.Cluster instance
:type cluster_id: int
:return: validated data
:rtype: list(dict)
"""
error_messages = []
data_to_update = cls.validate_json(data)
existing_instances = objects.IPAddrCollection.get_vips_by_cluster_id(
cluster_id)
for record in data_to_update:
instance = existing_instances.filter(
models.IPAddr.id == record.get('id')
).first()
if instance:
try:
cls.validate_update(record, instance)
except errors.InvalidData as e:
error_messages.append(e.message)
else:
error_messages.append(
"IPAddr with (ID={0}) does not exist or does not "
"belong to cluster (ID={1})".format(
record.get('id'),
cluster_id
)
)
if error_messages:
raise errors.InvalidData("\n".join(error_messages))
return data_to_update

View File

@ -0,0 +1,66 @@
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nailgun.api.v1.validators.json_schema import base_types
def _get_ip_addr_schema(*required_properties):
"""Generate JSON schema for ip addr record with given required properties.
:params: list(basestring) of required schema properties
:return: JSON schema
:rtype: dict
"""
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"additionalProperties": False,
"properties": {
"id": {"type": "integer"},
"ip_addr": base_types.NULLABLE_IP_ADDRESS,
"is_user_defined": {"type": "boolean"},
"network": base_types.NULLABLE_ID,
"node": base_types.NULLABLE_ID,
"vip_name": {"type": "string"},
"vip_namespace": {"type": "string"},
}
}
# actually only `ip_addr` and `is_user_defined`
# is allowed to be updated by validator business logic
if required_properties:
schema["required"] = list(required_properties)
return schema
IP_ADDR_UPDATE_SCHEMA = _get_ip_addr_schema()
IP_ADDR_UPDATE_WITH_ID_SCHEMA = _get_ip_addr_schema("id")
IP_ADDRS_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": IP_ADDR_UPDATE_WITH_ID_SCHEMA
}
# _IP_ADDRESS_SCHEMA currently not used and preserved
# to illustrate IP address fields
_IP_ADDR_SCHEMA = _get_ip_addr_schema("network", "ip_addr")
_IP_ADDRS_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": _IP_ADDR_SCHEMA
}

View File

@ -62,3 +62,6 @@ from nailgun.objects.cluster_plugin_link import ClusterPluginLink
from nailgun.objects.cluster_plugin_link import ClusterPluginLinkCollection
from nailgun.objects.openstack_config import OpenstackConfig
from nailgun.objects.openstack_config import OpenstackConfigCollection
from nailgun.objects.ip_addr import IPAddr
from nailgun.objects.ip_addr import IPAddrCollection

View File

@ -213,6 +213,7 @@ class NailgunCollection(object):
def order_by(cls, iterable, order_by):
"""Order given iterable by specified order_by.
:param iterable: model objects collection
:param order_by: tuple of model fields names or single field name for
ORDER BY criterion to SQLAlchemy query. If name starts with '-'
desc ordering applies, else asc.

View File

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nailgun.db.sqlalchemy import models
from nailgun.objects import Cluster
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
from nailgun.objects.serializers.ip_addr import IPAddrSerializer
class IPAddr(NailgunObject):
model = models.IPAddr
serializer = IPAddrSerializer
class IPAddrCollection(NailgunCollection):
single = IPAddr
@classmethod
def get_by_cluster_id(cls, cluster_id):
"""Get records filtered by cluster identifier.
Or returns all records if no cluster_id is provided.
:param cluster_id: cluster identifier or None to get all records
:type cluster_id: int|None
:return: vips query
:rtype: SQLAlchemy Query
"""
query = cls.all()
if cluster_id is not None:
query = query.join(models.NetworkGroup)\
.join(models.NodeGroup)\
.filter(models.NodeGroup.cluster_id == cluster_id)
return query
@classmethod
def get_vips_by_cluster_id(cls, cluster_id,
network_id=None, network_role=None):
"""Get VIP filtered by cluster ID.
VIP is determined by not NULL vip_name field of IPAddr model.
:param cluster_id: cluster identifier or None to get all records
:type cluster_id: int|None
:param network_id: network identifier
:type network_id: int
:param network_role: network role
:type network_role: str
:return: vips query
:rtype: SQLAlchemy Query
"""
query = cls.get_by_cluster_id(cluster_id)\
.filter(models.IPAddr.vip_name.isnot(None))
if network_id:
query = query.filter(models.IPAddr.network == network_id)
if network_role:
# Get all network_roles for cluster and gain vip names from it,
# then bound query to this names.
# See network_roles.yaml in plugin examples for the details of
# input structure.
cluster_obj = Cluster.get_by_uid(cluster_id)
vips = []
for cluster_network_role in Cluster.get_network_roles(cluster_obj):
if cluster_network_role.get('id') == network_role:
vips.extend(
cluster_network_role
.get('properties', {})
.get('vip', [])
)
vip_names = (vip['name'] for vip in vips)
unique_vip_names = list(set(vip_names))
query = query.filter(models.IPAddr.vip_name.in_(unique_vip_names))
return query
@classmethod
def update_vips(cls, new_data_list):
"""Perform batch update of VIP data.
:param new_data_list:
:type new_data_list: list(dict)
:return: vips query
:rtype: SQLAlchemy Query
"""
# create dictionary where key is id
data_by_ids = {item['id']: item for item in new_data_list}
# get db instances
query = cls.filter_by_list(None, 'id', list(data_by_ids))
cls.lock_for_update(query).all()
for existing_instance in query:
cls.single.update(
existing_instance,
data_by_ids[existing_instance.id]
)
return query

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nailgun.objects.serializers.base import BasicSerializer
class IPAddrSerializer(BasicSerializer):
fields = (
"id",
"network",
"node",
"ip_addr",
"vip_name",
"vip_namespace",
"is_user_defined",
)

View File

@ -0,0 +1,584 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from nailgun.db.sqlalchemy.models import IPAddr
from nailgun.test.integration.test_network_manager import \
BaseNetworkManagerTest
from nailgun.utils import reverse
class BaseIPAddrTest(BaseNetworkManagerTest):
def setUp(self):
self.maxDiff = None
super(BaseIPAddrTest, self).setUp()
self.vips_to_create = {
'management': {
'haproxy': '192.168.0.1',
'vrouter': '192.168.0.2',
}
}
self.cluster = self.env.create_cluster(api=False)
self.vips = self._create_ip_addrs_by_rules(
self.cluster,
self.vips_to_create)
self.vip_ids = [v.get('id') for v in self.vips]
self.expected_vips = [
{
'vip_name': 'haproxy',
'node': None,
'ip_addr': '192.168.0.1',
'is_user_defined': False,
'vip_namespace': None
},
{
'vip_name': 'vrouter',
'node': None,
'ip_addr': '192.168.0.2',
'is_user_defined': False,
'vip_namespace': None
}
]
self.non_existing_id = 11341134
def _remove_from_response(self, response, fields):
list_given = isinstance(response, list)
if not list_given:
response = [response]
clean_response = []
for resp_item in response:
resp_item_clone = resp_item.copy()
for f in fields:
resp_item_clone.pop(f, None)
clean_response.append(resp_item_clone)
return clean_response if list_given else clean_response[0]
class TestIPAddrList(BaseIPAddrTest):
def test_vips_list_for_cluster(self):
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={'cluster_id': self.cluster['id']}
),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertItemsEqual(
self.expected_vips,
self._remove_from_response(
resp.json_body,
['id', 'network']
),
)
def test_vips_list_with_two_clusters(self):
self.second_cluster = self.env.create_cluster(api=False)
self._create_ip_addrs_by_rules(
self.second_cluster,
self.vips_to_create
)
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={'cluster_id': self.cluster['id']}
),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertItemsEqual(
self.expected_vips,
self._remove_from_response(
resp.json_body,
['id', 'network']
),
)
def test_wrong_cluster(self):
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={'cluster_id': 99999}
),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(404, resp.status_code)
def test_create_fail(self):
resp = self.app.post(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': self.vip_ids[0]
}
),
{"some": "params"},
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(405, resp.status_code)
def test_update(self):
update_data = [
{
'id': self.vip_ids[0],
'is_user_defined': True,
'ip_addr': '192.168.0.44'
},
{
'id': self.vip_ids[1],
'ip_addr': '192.168.0.43'
}
]
expected_data = [
{
'id': self.vip_ids[0],
'is_user_defined': True,
'vip_name': self.vips[0]["vip_name"],
'ip_addr': '192.168.0.44',
'vip_namespace': None
},
{
'id': self.vip_ids[1],
'is_user_defined': False,
'vip_name': self.vips[1]["vip_name"],
'ip_addr': '192.168.0.43',
'vip_namespace': None
}
]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(update_data),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertEqual(expected_data, self._remove_from_response(
resp.json_body,
['node', 'network']
))
def test_update_fail_with_no_id(self):
new_data = [{
'ip_addr': '192.168.0.44'
}]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
self.assertIn("'id' is a required property", resp.json_body["message"])
def test_update_fail_with_non_updatable_field(self):
new_data = [
{
'id': self.vip_ids[0],
'ip_addr': '192.168.0.44'
},
{
'id': self.vip_ids[1],
'ip_addr': '192.168.0.44',
'network': self.non_existing_id
}
]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
self.assertIn("'network'", resp.json_body["message"])
def test_update_pass_with_non_updatable_not_changed_field(self):
new_data = [
{
'id': self.vips[0].id,
'ip_addr': '192.168.0.44',
'network': self.vips[0].network
}
]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(200, resp.status_code)
def test_update_fail_with_not_found_id(self):
new_data = [{
'id': self.non_existing_id,
'ip_addr': '192.168.0.44'
}]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertIn(str(self.non_existing_id), resp.json_body["message"])
self.assertEqual(400, resp.status_code)
def test_update_fail_on_dict_request(self):
new_data = {
'id': self.vip_ids[0],
'ip_addr': '192.168.0.44'
}
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
def test_update_failing_on_empty_request(self):
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params="",
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
def test_update_not_failing_on_empty_list_request(self):
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps([]),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(200, resp.status_code)
def test_update_fail_on_wrong_fields(self):
new_data_suites = [
[{
'id': self.vip_ids[0],
"network": self.non_existing_id,
"wrong_field": "value"
}]
]
for new_data in new_data_suites:
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
def test_update_fail_on_ip_from_wrong_cluster(self):
another_cluster = self.env.create_cluster(api=False)
new_data_suites = [
[{
'id': self.vip_ids[0],
"network": self.non_existing_id
}]
]
for new_data in new_data_suites:
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': another_cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
self.assertIn(
"does not belong to cluster",
resp.json_body.get("message")
)
def test_all_data_not_changed_on_single_error(self):
old_all_addr = [dict(a) for a in self.db.query(IPAddr).all()]
self.db.commit() # we will re-query all from ip_addr table later
new_data = [
{
'id': self.vip_ids[0],
'is_user_defined': True,
'ip_addr': '192.168.0.44'
},
{
# should fail on no id
'is_user_defined': False,
'ip_addr': '192.168.0.43'
}
]
resp = self.app.patch(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params=jsonutils.dumps(new_data),
headers=self.default_headers,
expect_errors=True
)
new_all_addr = [dict(a) for a in self.db.query(IPAddr).all()]
self.assertEqual(old_all_addr, new_all_addr)
self.assertEqual(400, resp.status_code)
def test_ipaddr_filter_by_network_id(self):
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params={"network_id": self.vips[0]['network']},
headers=self.default_headers
)
self.assertEqual([dict(v) for v in self.vips], resp.json_body)
self.assertEqual(200, resp.status_code)
def test_ipaddr_filter_by_missing_network_id(self):
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params={"network_id": self.non_existing_id},
headers=self.default_headers
)
self.assertEqual([], resp.json_body)
self.assertEqual(200, resp.status_code)
def test_ipaddr_filter_by_network_role(self):
# create more vips in another network
ips_in_different_network_name = {
'public': {
'vrouter_pub': '172.16.0.4',
'public': '172.16.0.5',
}
}
expected_vips = self._create_ip_addrs_by_rules(
self.cluster,
ips_in_different_network_name)
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params={'network_role': 'public/vip'},
headers=self.default_headers
)
expected_vips = [dict(vip) for vip in expected_vips]
self.assertItemsEqual(expected_vips, resp.json_body)
self.assertEqual(200, resp.status_code)
def test_ipaddr_filter_by_missing_network_role(self):
resp = self.app.get(
reverse(
'ClusterVIPCollectionHandler',
kwargs={
'cluster_id': self.cluster['id']
}
),
params={'network_role': 'NOT_EXISTING_NETWORK_ROLE'},
headers=self.default_headers
)
self.assertEqual([], resp.json_body)
self.assertEqual(200, resp.status_code)
class TestIPAddrHandler(BaseIPAddrTest):
def test_get_ip_addr(self):
resp = self.app.get(
reverse(
'ClusterVIPHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': self.vip_ids[0]
}
),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertIn(
self._remove_from_response(
resp.json_body,
['id', 'network']
),
self.expected_vips
)
def test_delete_fail(self):
resp = self.app.delete(
reverse(
'ClusterVIPHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': self.vip_ids[0]
}
),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(405, resp.status_code)
def test_fail_on_no_vip_metadata(self):
not_vip_ip_addr = IPAddr(
network=self.cluster.network_groups[0].id,
ip_addr="127.0.0.1",
vip_name=None
)
self.db.add(not_vip_ip_addr)
self.db.flush()
not_vip_id = not_vip_ip_addr.get('id')
resp = self.app.get(
reverse(
'ClusterVIPHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': not_vip_id
}
),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
def test_update_ip_addr(self):
update_data = {
'is_user_defined': True,
'ip_addr': '192.168.0.100',
'vip_namespace': 'new-namespace'
}
expected_data = {
'is_user_defined': True,
'vip_name': self.vips[0]['vip_name'],
'ip_addr': '192.168.0.100',
'vip_namespace': 'new-namespace'
}
resp = self.app.patch(
reverse(
'ClusterVIPHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': self.vips[0]['id']
}
),
params=jsonutils.dumps(update_data),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertEqual(
expected_data,
self._remove_from_response(
resp.json_body,
['id', 'network', 'node']
)
)
def test_update_ip_addr_with_put(self):
update_data = {
'is_user_defined': True,
'ip_addr': '192.168.0.100',
'vip_namespace': 'new-namespace'
}
expected_data = {
'is_user_defined': True,
'vip_name': self.vips[0]['vip_name'],
'ip_addr': '192.168.0.100',
'vip_namespace': 'new-namespace'
}
resp = self.app.put(
reverse(
'ClusterVIPHandler',
kwargs={
'cluster_id': self.cluster['id'],
'ip_addr_id': self.vips[0]['id']
}
),
params=jsonutils.dumps(update_data),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertEqual(
expected_data,
self._remove_from_response(
resp.json_body,
['id', 'network', 'node']
)
)