Add API handler for handling network template
YAML network template can be uploaded/got/deleted via a call to api/clusters/<id>/network_configuration/template Change-Id: I5c9ae51558dab2301e8ad8dffcf68a63ed0abdb4 Implements: blueprint templates-for-networking
This commit is contained in:
parent
bb362555db
commit
eded14892c
@ -248,7 +248,7 @@ class BaseHandler(object):
|
||||
def content_json(func, cls, *args, **kwargs):
|
||||
json_resp = lambda data: (
|
||||
jsonutils.dumps(data)
|
||||
if isinstance(data, (dict, list)) else data
|
||||
if isinstance(data, (dict, list)) or not data else data
|
||||
)
|
||||
|
||||
request_validate_needed = True
|
||||
|
@ -301,7 +301,7 @@ class VmwareAttributesHandler(BaseHandler):
|
||||
:http: * 200 (OK)
|
||||
* 400 (wrong attributes data specified |
|
||||
cluster doesn't accept vmware configuration)
|
||||
* 403 (attriutes can't be changed)
|
||||
* 403 (attributes can't be changed)
|
||||
* 404 (cluster not found in db |
|
||||
cluster has no vmware attributes)
|
||||
"""
|
||||
|
@ -195,6 +195,46 @@ class NeutronNetworkConfigurationHandler(ProviderHandler):
|
||||
self.raise_task(task)
|
||||
|
||||
|
||||
class TemplateNetworkConfigurationHandler(ProviderHandler):
|
||||
"""Neutron Network configuration handler
|
||||
"""
|
||||
|
||||
@content
|
||||
def GET(self, cluster_id):
|
||||
""":returns: network template for cluster (json format)
|
||||
:http: * 200 (OK)
|
||||
* 404 (cluster not found in db)
|
||||
"""
|
||||
cluster = self.get_object_or_404(objects.Cluster, cluster_id)
|
||||
return cluster.network_config.configuration_template
|
||||
|
||||
@content
|
||||
def PUT(self, cluster_id):
|
||||
""":returns: {}
|
||||
:http: * 200 (OK)
|
||||
* 400 (invalid object data specified)
|
||||
* 403 (change of configuration is forbidden)
|
||||
* 404 (cluster not found in db)
|
||||
"""
|
||||
template = jsonutils.loads(web.data())
|
||||
|
||||
cluster = self.get_object_or_404(objects.Cluster, cluster_id)
|
||||
self.check_if_network_configuration_locked(cluster)
|
||||
objects.Cluster.set_network_template(cluster, template)
|
||||
raise self.http(200)
|
||||
|
||||
def DELETE(self, cluster_id):
|
||||
""":returns: {}
|
||||
:http: * 204 (object successfully deleted)
|
||||
* 403 (change of configuration is forbidden)
|
||||
* 404 (cluster not found in db)
|
||||
"""
|
||||
cluster = self.get_object_or_404(objects.Cluster, cluster_id)
|
||||
self.check_if_network_configuration_locked(cluster)
|
||||
objects.Cluster.set_network_template(cluster, None)
|
||||
raise self.http(204)
|
||||
|
||||
|
||||
class NetworkConfigurationVerifyHandler(ProviderHandler):
|
||||
"""Network configuration verify handler base
|
||||
"""
|
||||
|
@ -54,6 +54,8 @@ from nailgun.api.v1.handlers.network_configuration \
|
||||
import NovaNetworkConfigurationHandler
|
||||
from nailgun.api.v1.handlers.network_configuration \
|
||||
import NovaNetworkConfigurationVerifyHandler
|
||||
from nailgun.api.v1.handlers.network_configuration \
|
||||
import TemplateNetworkConfigurationHandler
|
||||
|
||||
from nailgun.api.v1.handlers.node import NodeAgentHandler
|
||||
from nailgun.api.v1.handlers.node import NodeCollectionHandler
|
||||
@ -153,6 +155,8 @@ urls = (
|
||||
r'/clusters/(?P<cluster_id>\d+)/network_configuration/'
|
||||
'neutron/verify/?$',
|
||||
NeutronNetworkConfigurationVerifyHandler,
|
||||
r'/clusters/(?P<cluster_id>\d+)/network_configuration/template/?$',
|
||||
TemplateNetworkConfigurationHandler,
|
||||
|
||||
r'/clusters/(?P<cluster_id>\d+)/orchestrator/deployment/?$',
|
||||
DeploymentInfo,
|
||||
|
@ -86,6 +86,7 @@ def upgrade():
|
||||
upgrade_node_roles_metadata()
|
||||
node_roles_as_plugin_upgrade()
|
||||
migrate_volumes_into_extension_upgrade()
|
||||
networking_templates_upgrade()
|
||||
extend_releases_model_upgrade()
|
||||
upgrade_task_names()
|
||||
vms_conf_upgrade()
|
||||
@ -98,6 +99,7 @@ def downgrade():
|
||||
downgrade_cluster_ui_settings()
|
||||
extend_nic_model_downgrade()
|
||||
extend_releases_model_downgrade()
|
||||
networking_templates_downgrade()
|
||||
migrate_volumes_into_extension_downgrade()
|
||||
node_roles_as_plugin_downgrade()
|
||||
extend_plugin_model_downgrade()
|
||||
@ -240,6 +242,26 @@ def extend_plugin_model_upgrade():
|
||||
)
|
||||
|
||||
|
||||
def networking_templates_upgrade():
|
||||
op.add_column(
|
||||
'networking_configs',
|
||||
sa.Column(
|
||||
'configuration_template', fields.JSON(),
|
||||
nullable=True, server_default=None)
|
||||
)
|
||||
op.add_column(
|
||||
'nodes',
|
||||
sa.Column(
|
||||
'network_template', fields.JSON(), nullable=True,
|
||||
server_default=None)
|
||||
)
|
||||
|
||||
|
||||
def networking_templates_downgrade():
|
||||
op.drop_column('nodes', 'network_template')
|
||||
op.drop_column('networking_configs', 'configuration_template')
|
||||
|
||||
|
||||
def extend_ip_addrs_model_downgrade():
|
||||
vrouter_enum = sa.Enum('haproxy', 'vrouter',
|
||||
name='network_vip_types')
|
||||
|
@ -40,6 +40,8 @@ class NetworkingConfig(Base):
|
||||
"8.8.8.8"
|
||||
])
|
||||
floating_ranges = Column(JSON, default=[])
|
||||
configuration_template = Column(JSON, default=None, server_default=None,
|
||||
nullable=True)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_on': discriminator
|
||||
|
@ -103,6 +103,8 @@ class Node(Base):
|
||||
ip_addrs = relationship("IPAddr", viewonly=True)
|
||||
replaced_deployment_info = Column(JSON, default=[])
|
||||
replaced_provisioning_info = Column(JSON, default={})
|
||||
network_template = Column(JSON, default=None, server_default=None,
|
||||
nullable=True)
|
||||
|
||||
@property
|
||||
def interfaces(self):
|
||||
|
@ -498,10 +498,14 @@ class Cluster(NailgunObject):
|
||||
)
|
||||
cls.replace_provisioning_info_on_nodes(instance, [], nodes_to_remove)
|
||||
cls.replace_deployment_info_on_nodes(instance, [], nodes_to_remove)
|
||||
from nailgun.objects import NodeCollection
|
||||
NodeCollection.reset_network_template(nodes_to_remove)
|
||||
|
||||
map(
|
||||
net_manager.assign_networks_by_default,
|
||||
nodes_to_add
|
||||
)
|
||||
cls.update_nodes_network_template(instance, nodes_to_add)
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
@ -916,6 +920,19 @@ class Cluster(NailgunObject):
|
||||
"""
|
||||
return instance.release.network_roles_metadata
|
||||
|
||||
@classmethod
|
||||
def set_network_template(cls, instance, template):
|
||||
instance.network_config.configuration_template = template
|
||||
cls.update_nodes_network_template(instance, instance.nodes)
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
def update_nodes_network_template(cls, instance, nodes):
|
||||
from nailgun.objects import Node
|
||||
template = instance.network_config.configuration_template
|
||||
for node in nodes:
|
||||
Node.apply_network_template(node, template)
|
||||
|
||||
|
||||
class ClusterCollection(NailgunCollection):
|
||||
"""Cluster collection
|
||||
|
@ -18,7 +18,9 @@
|
||||
Node-related objects and collections
|
||||
"""
|
||||
import itertools
|
||||
import jinja2
|
||||
import operator
|
||||
from oslo.serialization import jsonutils
|
||||
import traceback
|
||||
|
||||
from datetime import datetime
|
||||
@ -693,6 +695,13 @@ class Node(NailgunObject):
|
||||
network_manager = Cluster.get_network_manager(instance.cluster)
|
||||
network_manager.assign_networks_by_default(instance)
|
||||
cls.add_pending_change(instance, consts.CLUSTER_CHANGES.interfaces)
|
||||
cls.set_network_template(instance)
|
||||
|
||||
@classmethod
|
||||
def set_network_template(cls, instance):
|
||||
template = instance.cluster.network_config.configuration_template
|
||||
cls.apply_network_template(instance, template)
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
def add_pending_change(cls, instance, change):
|
||||
@ -786,6 +795,7 @@ class Node(NailgunObject):
|
||||
def remove_replaced_params(cls, instance):
|
||||
instance.replaced_deployment_info = []
|
||||
instance.replaced_provisioning_info = {}
|
||||
instance.network_template = None
|
||||
|
||||
@classmethod
|
||||
def all_roles(cls, instance):
|
||||
@ -797,6 +807,46 @@ class Node(NailgunObject):
|
||||
|
||||
return sorted(roles | primary_roles)
|
||||
|
||||
@classmethod
|
||||
def apply_network_template(cls, instance, template):
|
||||
if template is None:
|
||||
instance.network_template = None
|
||||
return
|
||||
|
||||
template_body = template['adv_net_template']
|
||||
# Get the correct nic_mapping for this node so we can
|
||||
# dynamically replace any interface references in any
|
||||
# template for this node.
|
||||
node_group = instance.group_id
|
||||
if node_group not in template_body:
|
||||
node_group = 'default'
|
||||
|
||||
node_name = cls.make_slave_name(instance)
|
||||
nic_mapping = template_body[node_group]['nic_mapping']
|
||||
if node_name not in nic_mapping:
|
||||
node_name = 'default'
|
||||
|
||||
nic_mapping = nic_mapping[node_name]
|
||||
env = jinja2.Environment(variable_start_string='<%',
|
||||
variable_end_string='%>')
|
||||
|
||||
# Replace interface references and re-parse JSON
|
||||
template_object = env.from_string(jsonutils.dumps(template_body))
|
||||
node_template = template_object.render(nic_mapping)
|
||||
parsed_template = jsonutils.loads(node_template)
|
||||
|
||||
output = parsed_template[node_group]
|
||||
output['templates'] = output.pop('network_scheme')
|
||||
output['roles'] = {}
|
||||
output['endpoints'] = {}
|
||||
for v in output['templates'].values():
|
||||
for endpoint in v['endpoints']:
|
||||
output['endpoints'][endpoint] = {}
|
||||
for role, ep in v['roles'].items():
|
||||
output['roles'][role] = ep
|
||||
|
||||
instance.network_template = output
|
||||
|
||||
|
||||
class NodeCollection(NailgunCollection):
|
||||
"""Node collection
|
||||
@ -885,3 +935,8 @@ class NodeCollection(NailgunCollection):
|
||||
@classmethod
|
||||
def get_by_ids(cls, ids):
|
||||
return db.query(models.Node).filter(models.Node.id.in_(ids)).all()
|
||||
|
||||
@classmethod
|
||||
def reset_network_template(cls, instances):
|
||||
for instance in instances:
|
||||
instance.network_template = None
|
||||
|
@ -77,7 +77,8 @@ class NeutronNetworkConfigurationSerializer(NetworkConfigurationSerializer):
|
||||
network_cfg_fields = (
|
||||
'dns_nameservers', 'segmentation_type', 'net_l23_provider',
|
||||
'floating_ranges', 'vlan_range', 'gre_id_range',
|
||||
'base_mac', 'internal_cidr', 'internal_gateway')
|
||||
'base_mac', 'internal_cidr', 'internal_gateway',
|
||||
'configuration_template')
|
||||
|
||||
@classmethod
|
||||
def serialize_for_cluster(cls, cluster):
|
||||
|
@ -110,7 +110,8 @@ class TestNetworkModels(BaseIntegrationTest):
|
||||
"internal_gateway": "192.168.111.1",
|
||||
"floating_ranges": [["172.16.0.130", "172.16.0.254"]],
|
||||
"dns_nameservers": ["8.8.4.4", "8.8.8.8"],
|
||||
"cluster_id": cluster.id
|
||||
"cluster_id": cluster.id,
|
||||
"configuration_template": {}
|
||||
}
|
||||
nc = NeutronConfig(**kw)
|
||||
self.db.add(nc)
|
||||
|
@ -161,6 +161,27 @@ def prepare():
|
||||
'fuel_version': '6.1',
|
||||
}])
|
||||
|
||||
result = db.execute(
|
||||
meta.tables['networking_configs'].insert(),
|
||||
[{
|
||||
'cluster_id': None,
|
||||
'dns_nameservers': ['8.8.8.8'],
|
||||
'floating_ranges': [],
|
||||
'configuration_template': None,
|
||||
}])
|
||||
db.execute(
|
||||
meta.tables['neutron_config'].insert(),
|
||||
[{
|
||||
'id': result.inserted_primary_key[0],
|
||||
'vlan_range': [],
|
||||
'gre_id_range': [],
|
||||
'base_mac': '00:00:00:00:00:00',
|
||||
'internal_cidr': '10.10.10.00/24',
|
||||
'internal_gateway': '10.10.10.01',
|
||||
'segmentation_type': 'vlan',
|
||||
'net_l23_provider': 'ovs'
|
||||
}])
|
||||
|
||||
result = db.execute(
|
||||
meta.tables['nodes'].insert(),
|
||||
[
|
||||
@ -483,6 +504,19 @@ class TestInterfacesOffloadingModesMigration(base.BaseAlembicMigrationTest):
|
||||
jsonutils.loads(result.fetchone()[0]), [])
|
||||
|
||||
|
||||
class TestNetworkingTemplatesMigration(base.BaseAlembicMigrationTest):
|
||||
def test_new_fields_exists_and_empty(self):
|
||||
result = db.execute(
|
||||
sa.select([self.meta.tables['networking_configs']
|
||||
.c.configuration_template]))
|
||||
self.assertIsNone(result.fetchone()[0])
|
||||
|
||||
result = db.execute(
|
||||
sa.select([self.meta.tables['nodes']
|
||||
.c.network_template]))
|
||||
self.assertIsNone(result.fetchone()[0])
|
||||
|
||||
|
||||
class TestInterfacesPxePropertyMigration(base.BaseAlembicMigrationTest):
|
||||
|
||||
def test_old_fields_exists(self):
|
||||
|
81
nailgun/nailgun/test/unit/test_network_template_handler.py
Normal file
81
nailgun/nailgun/test/unit/test_network_template_handler.py
Normal file
@ -0,0 +1,81 @@
|
||||
# -*- 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.test.base import BaseIntegrationTest
|
||||
from nailgun.utils import reverse
|
||||
|
||||
|
||||
class TestHandlers(BaseIntegrationTest):
|
||||
|
||||
def get_template(self, cluster_id, expect_errors=False):
|
||||
resp = self.app.get(
|
||||
reverse(
|
||||
'TemplateNetworkConfigurationHandler',
|
||||
kwargs={'cluster_id': cluster_id}
|
||||
),
|
||||
headers=self.default_headers,
|
||||
expect_errors=expect_errors
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
def test_network_template_upload(self):
|
||||
cluster = self.env.create_cluster(api=False)
|
||||
template = {'template': 'test'}
|
||||
resp = self.app.put(
|
||||
reverse(
|
||||
'TemplateNetworkConfigurationHandler',
|
||||
kwargs={'cluster_id': cluster.id},
|
||||
),
|
||||
jsonutils.dumps(template),
|
||||
headers=self.default_headers
|
||||
)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
resp = self.get_template(cluster.id)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('test', resp.json_body.get('template'))
|
||||
|
||||
def test_template_not_set(self):
|
||||
resp = self.get_template(1, expect_errors=True)
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_delete_template(self):
|
||||
cluster = self.env.create_cluster(api=False)
|
||||
template = {'template': 'test'}
|
||||
resp = self.app.put(
|
||||
reverse(
|
||||
'TemplateNetworkConfigurationHandler',
|
||||
kwargs={'cluster_id': cluster.id},
|
||||
),
|
||||
jsonutils.dumps(template),
|
||||
headers=self.default_headers
|
||||
)
|
||||
self.assertEquals(200, resp.status_code)
|
||||
|
||||
resp = self.app.delete(
|
||||
reverse(
|
||||
'TemplateNetworkConfigurationHandler',
|
||||
kwargs={'cluster_id': cluster.id},
|
||||
),
|
||||
headers=self.default_headers
|
||||
)
|
||||
self.assertEquals(204, resp.status_code)
|
||||
|
||||
resp = self.get_template(cluster.id)
|
||||
self.assertEquals(None, resp.json_body)
|
@ -163,6 +163,95 @@ class TestAssignmentHandlers(BaseIntegrationTest):
|
||||
|
||||
self.assertEquals(404, resp.status_code)
|
||||
|
||||
def test_add_node_with_cluster_network_template(self):
|
||||
net_template = """
|
||||
{
|
||||
"adv_net_template": {
|
||||
"default": {
|
||||
"network_assignments": {
|
||||
"management": {
|
||||
"ep": "br-mgmt"
|
||||
},
|
||||
"storage": {
|
||||
"ep": "br-storage"
|
||||
},
|
||||
"public": {
|
||||
"ep": "br-ex"
|
||||
},
|
||||
"private": {
|
||||
"ep": "br-prv"
|
||||
},
|
||||
"fuelweb_admin": {
|
||||
"ep": "br-fw-admin"
|
||||
}
|
||||
},
|
||||
"templates_for_node_role": {
|
||||
"controller": [
|
||||
"common"
|
||||
]
|
||||
},
|
||||
"nic_mapping": {
|
||||
"default": {
|
||||
"if4": "eth3",
|
||||
"if1": "eth0",
|
||||
"if2": "eth1",
|
||||
"if3": "eth2"
|
||||
}
|
||||
},
|
||||
"network_scheme": {
|
||||
"common": {
|
||||
"endpoints": [
|
||||
"br-mgmt"
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"action": "add-br",
|
||||
"name": "br-mgmt"
|
||||
},
|
||||
{
|
||||
"action": "add-port",
|
||||
"bridge": "br-mgmt",
|
||||
"name": "<% if2 %>"
|
||||
}
|
||||
],
|
||||
"roles": {
|
||||
"management": "br-mgmt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
net_template = jsonutils.loads(net_template)
|
||||
cluster = self.env.create_cluster(api=False)
|
||||
cluster.network_config.configuration_template = net_template
|
||||
|
||||
node = self.env.create_node()
|
||||
assignment_data = [
|
||||
{
|
||||
"id": node.id,
|
||||
"roles": ['controller']
|
||||
}
|
||||
]
|
||||
self.app.post(
|
||||
reverse(
|
||||
'NodeAssignmentHandler',
|
||||
kwargs={'cluster_id': cluster.id}
|
||||
),
|
||||
jsonutils.dumps(assignment_data),
|
||||
headers=self.default_headers
|
||||
)
|
||||
net_scheme = node.network_template['templates']['common']
|
||||
self.assertNotEqual({}, node.network_template)
|
||||
self.assertEquals(['br-mgmt'], net_scheme['endpoints'])
|
||||
self.assertEquals({'management': 'br-mgmt'}, net_scheme['roles'])
|
||||
|
||||
# The order of transformations matters
|
||||
self.assertIn('add-br', net_scheme['transformations'][0].values())
|
||||
self.assertIn('add-port', net_scheme['transformations'][1].values())
|
||||
self.assertEquals('eth1', net_scheme['transformations'][1]['name'])
|
||||
|
||||
|
||||
class TestClusterStateUnassigment(BaseIntegrationTest):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user