fuel-web/nailgun/nailgun/orchestrator/base_serializers.py

434 lines
16 KiB
Python

# -*- 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.
"""Base classes of deployment serializers for orchestrator"""
from copy import deepcopy
from netaddr import IPNetwork
from nailgun.db import db
from nailgun.db.sqlalchemy.models import NetworkGroup
from nailgun.errors import errors
from nailgun import objects
from nailgun.settings import settings
class MellanoxMixin(object):
@classmethod
def inject_mellanox_settings_for_deployment(
cls, node_attrs, cluster, networks):
"""Mellanox settings for deployment
Serialize mellanox node attrs then it will be merged with common
attributes, if mellanox plugin or iSER storage enabled.
:param node_attrs: attributes for specific node
:type node_attrs: dict
:param cluster: A cluster instance
:type cluster: Cluster model
:param networks: networks related for specific node
:type networks: list
:returns: None
"""
cluster_attrs = objects.Cluster.get_editable_attributes(cluster)
neutron_mellanox_data = cluster_attrs.get('neutron_mellanox', {})
# We need to support mellanox for releases < 8.0. So if mellanox
# attributes exist (primary in old releases) then merge them with
# common node attributes.
if neutron_mellanox_data:
storage_data = cluster_attrs.get('storage', {})
nm = objects.Cluster.get_network_manager(cluster)
node_attrs['neutron_mellanox'] = {}
# Find Physical port for VFs generation
if 'plugin' in neutron_mellanox_data and \
neutron_mellanox_data['plugin']['value'] == 'ethernet':
# Set config for ml2 mellanox mechanism driver
node_attrs['neutron_mellanox'].update({
'physical_port': nm.get_network_by_netname(
'private', networks)['dev'],
'ml2_eswitch': {
'vnic_type': 'hostdev',
'apply_profile_patch': True}
})
# Fix network scheme to have physical port for RDMA if iSER enabled
if 'iser' in storage_data and storage_data['iser']['value']:
iser_new_name = 'eth_iser0'
node_attrs['neutron_mellanox'].update({
'storage_parent': nm.get_network_by_netname(
'storage', networks)['dev'],
'iser_interface_name': iser_new_name
})
storage_vlan = \
nm.get_network_by_netname('storage', networks).get('vlan')
if storage_vlan:
vlan_name = "vlan{0}".format(storage_vlan)
# Set storage rule to iSER interface vlan interface
node_attrs['network_scheme']['roles']['storage'] = \
vlan_name
# Set iSER interface vlan interface
node_attrs['network_scheme']['interfaces'][vlan_name] = \
{'L2': {'vlan_splinters': 'off'}}
node_attrs['network_scheme']['endpoints'][vlan_name] = \
node_attrs['network_scheme']['endpoints'].pop(
'br-storage', {})
node_attrs['network_scheme']['endpoints'][vlan_name][
'vlandev'] = iser_new_name
else:
# Set storage rule to iSER port
node_attrs['network_scheme']['roles'][
'storage'] = iser_new_name
node_attrs['network_scheme']['interfaces'][
iser_new_name] = {'L2': {'vlan_splinters': 'off'}}
node_attrs['network_scheme']['endpoints'][
iser_new_name] = node_attrs['network_scheme'][
'endpoints'].pop('br-storage', {})
@classmethod
def inject_mellanox_settings_for_provisioning(
cls, cluster_attrs, serialized_node):
"""Mellanox settings for provisioning
Serialize mellanox node attrs then it will be merged with common
node attributes
:param cluster_attrs: cluster attributes
:type cluster_attrs: dict
:param serialized_node: node attributes data for provisioning
:type serialized_node: dict
:returns: None
"""
mellanox_data = cluster_attrs.get('neutron_mellanox')
# We need to support mellanox for releases < 8.0. So if mellanox
# attributes exist (primary in old releases) then merge them with
# common node attributes.
if mellanox_data:
serialized_node['ks_meta'].update({
'mlnx_vf_num': mellanox_data.get('vf_num'),
'mlnx_plugin_mode': mellanox_data.get('plugin'),
'mlnx_iser_enabled': cluster_attrs.get(
'storage', {}).get('iser')
})
# Add relevant kernel parameter when using Mellanox SR-IOV
# and/or iSER (which works on top of a probed virtual function)
# unless it was explicitly added by the user
pm_data = serialized_node['ks_meta']['pm_data']
if ((mellanox_data['plugin'] == 'ethernet' or
cluster_attrs['storage']['iser'] is True) and
'intel_iommu=' not in pm_data['kernel_params']):
pm_data['kernel_params'] += ' intel_iommu=on'
class MuranoMetadataSerializerMixin(object):
def generate_test_vm_image_data(self, node):
return self.inject_murano_settings(super(
MuranoMetadataSerializerMixin,
self).generate_test_vm_image_data(node))
def inject_murano_settings(self, image_data):
"""Adds murano metadata to the test image"""
test_vm_image = image_data['test_vm_image']
existing_properties = test_vm_image['glance_properties']
murano_data = ' '.join(["""--property murano_image_info='{"title":"""
""" "Murano Demo", "type": "cirros.demo"}'"""])
test_vm_image['glance_properties'] = existing_properties + murano_data
return {'test_vm_image': test_vm_image}
class VmwareDeploymentSerializerMixin(object):
def generate_vmware_data(self, node):
"""Extend serialize data with vmware attributes"""
vmware_data = {}
allowed_roles = [
'controller',
'primary-controller',
'compute-vmware',
'cinder-vmware'
]
all_roles = objects.Node.all_roles(node)
use_vcenter = node.cluster.attributes.editable.get('common', {}) \
.get('use_vcenter', {}).get('value')
if (use_vcenter and any(role in allowed_roles for role in all_roles)):
compute_instances = []
cinder_instances = []
vmware_attributes = node.cluster.vmware_attributes.editable \
.get('value', {})
availability_zones = vmware_attributes \
.get('availability_zones', {})
glance_instance = vmware_attributes.get('glance', {})
network = vmware_attributes.get('network', {})
for zone in availability_zones:
vc_user = self.escape_dollar(zone.get('vcenter_username', ''))
vc_password = self.escape_dollar(zone.get('vcenter_password',
''))
for compute in zone.get('nova_computes', {}):
datastore_regex = \
self.escape_dollar(compute.get('datastore_regex', ''))
compute_item = {
'availability_zone_name': zone.get('az_name', ''),
'vc_host': zone.get('vcenter_host', ''),
'vc_user': vc_user,
'vc_password': vc_password,
'service_name': compute.get('service_name', ''),
'vc_cluster': compute.get('vsphere_cluster', ''),
'datastore_regex': datastore_regex,
'target_node': compute.get('target_node', {}).get(
'current', {}).get('id', 'controllers')
}
compute_instances.append(compute_item)
cinder_item = {
'availability_zone_name': zone.get('az_name', ''),
'vc_host': zone.get('vcenter_host', ''),
'vc_user': vc_user,
'vc_password': vc_password
}
cinder_instances.append(cinder_item)
vmware_data['use_vcenter'] = True
if compute_instances:
vmware_data['vcenter'] = {
'esxi_vlan_interface':
network.get('esxi_vlan_interface', ''),
'computes': compute_instances
}
if cinder_instances:
vmware_data['cinder'] = {
'instances': cinder_instances
}
if glance_instance:
glance_username = \
self.escape_dollar(glance_instance
.get('vcenter_username', ''))
glance_password = \
self.escape_dollar(glance_instance
.get('vcenter_password', ''))
vmware_data['glance'] = {
'vc_host': glance_instance.get('vcenter_host', ''),
'vc_user': glance_username,
'vc_password': glance_password,
'vc_datacenter': glance_instance.get('datacenter', ''),
'vc_datastore': glance_instance.get('datastore', '')
}
return vmware_data
@staticmethod
def escape_dollar(data):
"""Escape dollar symbol
In order to disable variable interpolation in
values that we write to configuration files during
deployment we must replace all $ (dollar sign) occurrences.
"""
return data.replace('$', '$$')
class NetworkDeploymentSerializer(object):
@classmethod
def update_nodes_net_info(cls, cluster, nodes):
"""Adds information about networks to each node."""
for node in objects.Cluster.get_nodes_not_for_deletion(cluster):
netw_data = node.network_data
addresses = {}
for net in node.cluster.network_groups:
if net.name == 'public' and \
not objects.Node.should_have_public_with_ip(node):
continue
if net.meta.get('render_addr_mask'):
addresses.update(cls.get_addr_mask(
netw_data,
net.name,
net.meta.get('render_addr_mask')))
[n.update(addresses) for n in nodes
if n['uid'] == str(node.uid)]
return nodes
@classmethod
def get_common_attrs(cls, cluster, attrs):
"""Cluster network attributes."""
common = cls.network_provider_cluster_attrs(cluster)
common.update(
cls.network_ranges(objects.Cluster.get_default_group(cluster).id))
common.update({'master_ip': settings.MASTER_IP})
common['nodes'] = deepcopy(attrs['nodes'])
common['nodes'] = cls.update_nodes_net_info(cluster, common['nodes'])
return common
@classmethod
def get_node_attrs(cls, node):
"""Node network attributes."""
return cls.network_provider_node_attrs(node.cluster, node)
@classmethod
def network_provider_cluster_attrs(cls, cluster):
raise NotImplementedError()
@classmethod
def network_provider_node_attrs(cls, cluster, node):
raise NotImplementedError()
@classmethod
def network_ranges(cls, group_id):
"""Returns ranges for network groups
except range for public network for each node
"""
ng_db = db().query(NetworkGroup).filter_by(group_id=group_id).all()
attrs = {}
for net in ng_db:
net_name = net.name + '_network_range'
if net.meta.get("render_type") == 'ip_ranges':
attrs[net_name] = cls.get_ip_ranges_first_last(net)
elif net.meta.get("render_type") == 'cidr' and net.cidr:
attrs[net_name] = net.cidr
return attrs
@classmethod
def get_ip_ranges_first_last(cls, network_group):
"""Get all ip ranges in "10.0.0.0-10.0.0.255" format"""
return [
"{0}-{1}".format(ip_range.first, ip_range.last)
for ip_range in network_group.ip_ranges
]
@classmethod
def get_addr_mask(cls, network_data, net_name, render_name):
"""Get addr for network by name"""
nets = filter(
lambda net: net['name'] == net_name,
network_data)
if not nets or 'ip' not in nets[0]:
raise errors.CanNotFindNetworkForNode(
'Cannot find network with name: %s' % net_name)
net = nets[0]['ip']
return {
render_name + '_address': str(IPNetwork(net).ip),
render_name + '_netmask': str(IPNetwork(net).netmask)
}
@staticmethod
def get_admin_ip_w_prefix(node):
"""Getting admin ip and assign prefix from admin network."""
network_manager = objects.Cluster.get_network_manager(node.cluster)
admin_ip = network_manager.get_admin_ip_for_node(node.id)
admin_ip = IPNetwork(admin_ip)
# Assign prefix from admin network
admin_net = IPNetwork(
objects.NetworkGroup.get_admin_network_group(node.id).cidr
)
admin_ip.prefixlen = admin_net.prefixlen
return str(admin_ip)
@classmethod
def add_bridge(cls, name, provider=None):
"""Add bridge to schema
It will take global provider if it is omitted here
"""
bridge = {
'action': 'add-br',
'name': name
}
if provider:
bridge['provider'] = provider
return bridge
@classmethod
def add_port(cls, name, bridge, provider=None):
"""Add port to schema
Bridge name may be None, port will not be connected to any bridge then
It will take global provider if it is omitted here
Port name can be in form "XX" or "XX.YY", where XX - NIC name,
YY - vlan id. E.g. "eth0", "eth0.1021". This will create corresponding
interface if name includes vlan id.
"""
port = {
'action': 'add-port',
'name': name
}
if bridge:
port['bridge'] = bridge
if provider:
port['provider'] = provider
return port
@classmethod
def add_bond(cls, iface, parameters):
"""Add bond to schema
All required parameters should be inside parameters dict. (e.g.
bond_properties, interface_properties, provider, bridge).
bond_properties is obligatory, others are optional.
bridge should be set if bridge for untagged network is to be connected
to bond. Ports are to be created for tagged networks which should be
connected to bond (e.g. port "bond-X.212" for bridge "br-ex").
"""
bond = {
'action': 'add-bond',
'name': iface.name,
'interfaces': sorted(x['name'] for x in iface.slaves),
}
if iface.interface_properties.get('mtu'):
bond['mtu'] = iface.interface_properties['mtu']
if parameters:
bond.update(parameters)
return bond
@classmethod
def add_patch(cls, bridges, provider=None, mtu=None):
"""Add patch to schema
Patch connects two bridges listed in 'bridges'.
OVS bridge must go first in 'bridges'.
It will take global provider if it is omitted here
"""
patch = {
'action': 'add-patch',
'bridges': bridges,
}
if provider:
patch['provider'] = provider
if mtu:
patch['mtu'] = mtu
return patch