API for managing network groups

Individual network groups can be created, deleted and
modified. IP range management for each network is still
handled by the standard network_configuration API handlers.

Change-Id: I861485b5aebd8ae784fc71f405aab87e73f6d2e0
Implements: blueprint templates-for-networking
This commit is contained in:
Ryan Moe 2015-07-13 07:43:34 -07:00 committed by Aleksey Kasatkin
parent 52a413e672
commit 7084177cae
11 changed files with 413 additions and 5 deletions

View File

@ -0,0 +1,38 @@
# -*- 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.api.v1.handlers.base import CollectionHandler
from nailgun.api.v1.handlers.base import SingleHandler
from nailgun.api.v1.validators.network import NetworkGroupValidator
from nailgun import objects
class NetworkGroupHandler(SingleHandler):
"""Network group handler
"""
validator = NetworkGroupValidator
single = objects.NetworkGroup
class NetworkGroupCollectionHandler(CollectionHandler):
"""Network group collection handler
"""
collection = objects.NetworkGroupCollection
validator = NetworkGroupValidator

View File

@ -43,6 +43,8 @@ from nailgun.api.v1.handlers.logs import LogPackageHandler
from nailgun.api.v1.handlers.logs import LogSourceByNodeCollectionHandler
from nailgun.api.v1.handlers.logs import LogSourceCollectionHandler
from nailgun.api.v1.handlers.logs import SnapshotDownloadHandler
from nailgun.api.v1.handlers.network_group import NetworkGroupCollectionHandler
from nailgun.api.v1.handlers.network_group import NetworkGroupHandler
from nailgun.api.v1.handlers.node_group import NodeGroupCollectionHandler
from nailgun.api.v1.handlers.node_group import NodeGroupHandler
@ -192,6 +194,11 @@ urls = (
r'/clusters/(?P<obj_id>\d+)/deployment_tasks/?$',
ClusterDeploymentTasksHandler,
r'/networks/?$',
NetworkGroupCollectionHandler,
r'/networks/(?P<obj_id>\d+)/?$',
NetworkGroupHandler,
r'/clusters/(?P<cluster_id>\d+)/assignment/?$',
NodeAssignmentHandler,
r'/clusters/(?P<cluster_id>\d+)/unassignment/?$',

View File

@ -0,0 +1,33 @@
# 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
single_scheme = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "NetworkGroup",
"description": "Serialized NetworkGroup object",
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"release": base_types.NULLABLE_ID,
"vlan_start": {"type": "number"},
"cidr": base_types.NET_ADDRESS,
"gateway": base_types.NULLABLE_IP_ADDRESS,
"group_id": base_types.NULLABLE_ID,
"meta": {"type": "object"}
}
}

View File

@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from nailgun.api.v1.validators.base import BasicValidator
from nailgun.api.v1.validators.json_schema import network_group as ng_scheme
from nailgun import consts
from nailgun import objects
@ -22,7 +25,6 @@ from nailgun.db import db
from nailgun.db.sqlalchemy.models import Cluster
from nailgun.db.sqlalchemy.models import Node
from nailgun.errors import errors
import six
class NetworkConfigurationValidator(BasicValidator):
@ -377,3 +379,40 @@ class NetAssignmentValidator(BasicValidator):
"bond".format(node['id'], iface['id']),
log_message=True
)
class NetworkGroupValidator(BasicValidator):
single_schema = ng_scheme.single_scheme
@classmethod
def validate(cls, data):
d = cls.validate_json(data)
node_group = objects.NodeGroup.get_by_uid(d.get('group_id'))
if not node_group:
raise errors.InvalidData(
"Node group with ID {0} does not exist".format(
d.get('group_id'))
)
if objects.NetworkGroup.get_from_node_group_by_name(
node_group.id, d.get('name')):
raise errors.AlreadyExists(
"Network with name {0} already exists "
"in node group {1}".format(d['name'], node_group.name)
)
return d
@classmethod
def validate_update(cls, data, **kwargs):
return cls.validate(data)
@classmethod
def validate_delete(cls, data, instance, force=False):
if not instance.group_id:
# Only default Admin-pxe network doesn't have group_id.
# It cannot be deleted.
raise errors.InvalidData(
"Default Admin-pxe network cannot be deleted")

View File

@ -99,9 +99,11 @@ def upgrade():
set_deployable_false_for_old_releases()
upgrade_node_labels()
extend_segmentation_type()
network_groups_name_upgrade()
def downgrade():
network_groups_name_downgrade()
downgrade_node_labels()
extensions_field_downgrade()
downgrade_cluster_ui_settings()
@ -131,6 +133,29 @@ def downgrade():
type_='foreignkey')
def network_groups_name_upgrade():
op.alter_column('network_groups',
'name',
type_=sa.String(length=50),
existing_type=sa.Enum(
'fuelweb_admin', 'storage',
'management', 'public',
'fixed', 'private',
name='network_group_name'))
drop_enum('network_group_name')
def network_groups_name_downgrade():
network_group_name = sa.Enum('fuelweb_admin', 'storage',
'management', 'public',
'fixed', 'private',
name='network_group_name')
network_group_name.create(op.get_bind(), checkfirst=False)
op.execute('ALTER TABLE network_groups ALTER COLUMN name '
'TYPE network_group_name '
'USING name::text::network_group_name')
def extend_node_model_upgrade():
op.add_column(
'node_nic_interfaces',

View File

@ -15,13 +15,11 @@
# under the License.
from sqlalchemy import Column
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import relationship
from sqlalchemy import String
from nailgun import consts
from nailgun.db.sqlalchemy.models.base import Base
from nailgun.db.sqlalchemy.models.fields import JSON
@ -51,8 +49,7 @@ class NetworkGroup(Base):
__tablename__ = 'network_groups'
id = Column(Integer, primary_key=True)
name = Column(Enum(*consts.NETWORKS, name='network_group_name'),
nullable=False)
name = Column(String(50), nullable=False)
# can be nullable only for fuelweb admin net
release = Column(Integer, ForeignKey('releases.id'))
# can be nullable only for fuelweb admin net

View File

@ -50,3 +50,6 @@ from nailgun.objects.node_group import NodeGroupCollection
from nailgun.objects.plugin import Plugin
from nailgun.objects.plugin import PluginCollection
from nailgun.objects.network_group import NetworkGroup
from nailgun.objects.network_group import NetworkGroupCollection

View File

@ -0,0 +1,40 @@
# -*- 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.network_group import NetworkGroupSerializer
from nailgun.db import db
from nailgun.db.sqlalchemy.models import NetworkGroup as DBNetworkGroup
from nailgun.objects import NailgunCollection
from nailgun.objects import NailgunObject
class NetworkGroup(NailgunObject):
model = DBNetworkGroup
serializer = NetworkGroupSerializer
@classmethod
def get_from_node_group_by_name(cls, node_group_id, network_name):
ng = db().query(DBNetworkGroup).filter_by(group_id=node_group_id,
name=network_name)
return ng.first() if ng else None
class NetworkGroupCollection(NailgunCollection):
single = NetworkGroup

View File

@ -0,0 +1,31 @@
# -*- 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 NetworkGroupSerializer(BasicSerializer):
fields = (
'id',
'name',
'release',
'vlan_start',
'cidr',
'gateway',
'meta',
'group_id'
)

View File

@ -797,3 +797,22 @@ class TestTunSegmentType(base.BaseAlembicMigrationTest):
[self.meta.tables['neutron_config'].c.segmentation_type])).\
fetchall()
self.assertIn(('tun',), types)
class TestStringNetworkGroupName(base.BaseAlembicMigrationTest):
def test_tun_segment_type_added(self):
db.execute(
self.meta.tables['network_groups'].insert(),
[{
'id': 3,
'name': 'custom_name',
'vlan_start': None,
'cidr': '10.20.0.0/24',
'gateway': '10.20.0.200',
}])
names = db.execute(
sa.select(
[self.meta.tables['network_groups'].c.name])). \
fetchall()
self.assertIn(('custom_name',), names)

View File

@ -0,0 +1,176 @@
# -*- 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 import objects
from nailgun.test.base import BaseIntegrationTest
from nailgun.utils import reverse
class TestHandlers(BaseIntegrationTest):
def setUp(self):
super(TestHandlers, self).setUp()
self.cluster = self.env.create_cluster(api=False)
def _create_network_group(self, expect_errors=False, **kwargs):
ng = {
"release": self.cluster.release.id,
"name": "external",
"vlan_start": 50,
"cidr": "10.3.0.0/24",
"gateway": "10.3.0.3",
"group_id": objects.Cluster.get_default_group(self.cluster).id,
"meta": {}
}
ng.update(kwargs)
resp = self.app.post(
reverse('NetworkGroupCollectionHandler'),
jsonutils.dumps(ng),
headers=self.default_headers,
expect_errors=expect_errors,
)
return resp
def test_create_network_group(self):
resp = self._create_network_group(name='test')
self.assertEqual(201, resp.status_code)
def test_get_network_group(self):
resp = self._create_network_group(name='test')
self.assertEqual(201, resp.status_code)
new_ng = jsonutils.loads(resp.body)
net_group = jsonutils.loads(resp.body)
resp = self.app.get(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': net_group['id']}
),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
self.assertEqual(jsonutils.loads(resp.body), new_ng)
def test_delete_network_group(self):
resp = self._create_network_group(name='test')
self.assertEqual(201, resp.status_code)
net_group = jsonutils.loads(resp.body)
resp = self.app.delete(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': net_group['id']}
),
headers=self.default_headers
)
self.assertEqual(204, resp.status_code)
def test_cannot_delete_admin_network_group(self):
admin = objects.Cluster.get_network_manager().get_admin_network_group()
resp = self.app.delete(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': admin.id}
),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
self.assertRegexpMatches(resp.json_body["message"],
'Default Admin-pxe network cannot be deleted')
def test_create_network_group_non_default_name(self):
resp = self._create_network_group(name='test')
new_ng = jsonutils.loads(resp.body)
self.assertEqual(201, resp.status_code)
self.assertEqual('test', new_ng['name'])
def test_modify_network_group(self):
resp = self._create_network_group(name='test')
new_ng = jsonutils.loads(resp.body)
new_ng['name'] = 'test2'
resp = self.app.put(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': new_ng['id']}
),
jsonutils.dumps(new_ng),
headers=self.default_headers
)
updated_ng = jsonutils.loads(resp.body)
self.assertEquals('test2', updated_ng['name'])
def test_duplicate_network_name_on_creation(self):
resp = self._create_network_group()
self.assertEqual(201, resp.status_code)
resp = self._create_network_group(expect_errors=True)
self.assertEqual(409, resp.status_code)
self.assertRegexpMatches(resp.json_body["message"],
'Network with name .* already exists')
def test_duplicate_network_name_on_change(self):
resp = self._create_network_group(name='test')
new_ng = jsonutils.loads(resp.body)
new_ng['name'] = 'public'
resp = self.app.put(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': new_ng['id']}
),
jsonutils.dumps(new_ng),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(409, resp.status_code)
self.assertRegexpMatches(resp.json_body["message"],
'Network with name .* already exists')
def test_invalid_group_id_on_creation(self):
resp = self._create_network_group(expect_errors=True, group_id=-1)
self.assertEqual(400, resp.status_code)
self.assertRegexpMatches(resp.json_body["message"],
'Node group with ID -1 does not exist')
def test_invalid_group_id_on_change(self):
resp = self._create_network_group(name='test')
new_ng = jsonutils.loads(resp.body)
new_ng['group_id'] = -1
resp = self.app.put(
reverse(
'NetworkGroupHandler',
kwargs={'obj_id': new_ng['id']}
),
jsonutils.dumps(new_ng),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(400, resp.status_code)
self.assertRegexpMatches(resp.json_body["message"],
'Node group with ID -1 does not exist')