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:
parent
169fae21b0
commit
8f28676cf6
@ -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):
|
||||
|
180
nailgun/nailgun/api/v1/handlers/vip.py
Normal file
180
nailgun/nailgun/api/v1/handlers/vip.py
Normal 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)
|
@ -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+)/?$',
|
||||
|
@ -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)
|
||||
|
||||
|
111
nailgun/nailgun/api/v1/validators/ip_addr.py
Normal file
111
nailgun/nailgun/api/v1/validators/ip_addr.py
Normal 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
|
66
nailgun/nailgun/api/v1/validators/json_schema/ip_addr.py
Normal file
66
nailgun/nailgun/api/v1/validators/json_schema/ip_addr.py
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
|
117
nailgun/nailgun/objects/ip_addr.py
Normal file
117
nailgun/nailgun/objects/ip_addr.py
Normal 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
|
30
nailgun/nailgun/objects/serializers/ip_addr.py
Normal file
30
nailgun/nailgun/objects/serializers/ip_addr.py
Normal 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",
|
||||
)
|
584
nailgun/nailgun/test/integration/test_ip_addrs_management.py
Normal file
584
nailgun/nailgun/test/integration/test_ip_addrs_management.py
Normal 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']
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue
Block a user