388 lines
15 KiB
Python
388 lines
15 KiB
Python
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
|
#
|
|
# 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 copy
|
|
import json
|
|
import logging
|
|
import os
|
|
import pkg_resources
|
|
import pprint
|
|
import sys
|
|
import tempfile
|
|
|
|
import jsonschema
|
|
import netaddr
|
|
import yaml
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class ProcessDataSource():
|
|
def __init__(self, sitetype):
|
|
# Initialize intermediary and save site type
|
|
self._initialize_intermediary()
|
|
self.region_name = sitetype
|
|
|
|
@staticmethod
|
|
def _read_file(file_name):
|
|
with open(file_name, 'r') as f:
|
|
raw_data = f.read()
|
|
return raw_data
|
|
|
|
def _initialize_intermediary(self):
|
|
self.host_type = {}
|
|
self.data = {
|
|
'network': {},
|
|
'baremetal': {},
|
|
'region_name': '',
|
|
'storage': {},
|
|
'site_info': {},
|
|
}
|
|
self.sitetype = None
|
|
self.genesis_node = None
|
|
self.region_name = None
|
|
self.network_subnets = None
|
|
|
|
def _get_network_subnets(self):
|
|
""" Extract subnet information for networks.
|
|
|
|
|
|
In some networks, there are multiple subnets, in that case
|
|
we assign only the first subnet """
|
|
LOG.info("Extracting network subnets")
|
|
network_subnets = {}
|
|
for net_type in self.data['network']['vlan_network_data']:
|
|
# One of the type is ingress and we don't want that here
|
|
if (net_type != 'ingress'):
|
|
network_subnets[net_type] = netaddr.IPNetwork(
|
|
self.data['network']['vlan_network_data'][net_type]
|
|
['subnet'][0])
|
|
|
|
LOG.debug("Network subnets:\n{}".format(
|
|
pprint.pformat(network_subnets)))
|
|
return network_subnets
|
|
|
|
def _get_genesis_node_details(self):
|
|
# Get genesis host node details from the hosts based on host type
|
|
for racks in self.data['baremetal'].keys():
|
|
rack_hosts = self.data['baremetal'][racks]
|
|
for host in rack_hosts:
|
|
if rack_hosts[host]['type'] == 'genesis':
|
|
self.genesis_node = rack_hosts[host]
|
|
self.genesis_node['name'] = host
|
|
LOG.debug("Genesis Node Details:\n{}".format(
|
|
pprint.pformat(self.genesis_node)))
|
|
|
|
def _get_genesis_node_ip(self):
|
|
""" Returns the genesis node ip """
|
|
ip = '0.0.0.0'
|
|
LOG.info("Getting Genesis Node IP")
|
|
if not self.genesis_node:
|
|
self._get_genesis_node_details()
|
|
ips = self.genesis_node.get('ip', '')
|
|
if ips:
|
|
ip = ips.get('oam', '0.0.0.0')
|
|
return ip
|
|
|
|
def _validate_intermediary_data(self, data):
|
|
""" Validates the intermediary data before generating manifests.
|
|
|
|
|
|
It checks wether the data types and data format are as expected.
|
|
The method validates this with regex pattern defined for each
|
|
data type.
|
|
"""
|
|
LOG.info('Validating Intermediary data')
|
|
temp_data = {}
|
|
# Peforming a deep copy
|
|
temp_data = copy.deepcopy(data)
|
|
# Converting baremetal dict to list.
|
|
baremetal_list = []
|
|
for rack in temp_data['baremetal'].keys():
|
|
temp = [{k: v} for k, v in temp_data['baremetal'][rack].items()]
|
|
baremetal_list = baremetal_list + temp
|
|
|
|
temp_data['baremetal'] = baremetal_list
|
|
schema_dir = pkg_resources.resource_filename('spyglass', 'schemas/')
|
|
schema_file = schema_dir + "data_schema.json"
|
|
json_data = json.loads(json.dumps(temp_data))
|
|
with open(schema_file, 'r') as f:
|
|
json_schema = json.load(f)
|
|
try:
|
|
# Suppressing writing of data2.json. Can use it for debugging
|
|
with open('data2.json', 'w') as outfile:
|
|
json.dump(temp_data, outfile, sort_keys=True, indent=4)
|
|
jsonschema.validate(json_data, json_schema)
|
|
except jsonschema.exceptions.ValidationError as e:
|
|
LOG.error("Validation Error")
|
|
LOG.error("Message:{}".format(e.message))
|
|
LOG.error("Validator_path:{}".format(e.path))
|
|
LOG.error("Validator_pattern:{}".format(e.validator_value))
|
|
LOG.error("Validator:{}".format(e.validator))
|
|
sys.exit()
|
|
except jsonschema.exceptions.SchemaError as e:
|
|
LOG.error("Schema Validation Error!!")
|
|
LOG.error("Message:{}".format(e.message))
|
|
LOG.error("Schema:{}".format(e.schema))
|
|
LOG.error("Validator_value:{}".format(e.validator_value))
|
|
LOG.error("Validator:{}".format(e.validator))
|
|
LOG.error("path:{}".format(e.path))
|
|
sys.exit()
|
|
|
|
LOG.info("Data validation Passed!")
|
|
|
|
def _apply_design_rules(self):
|
|
""" Applies design rules from rules.yaml
|
|
|
|
|
|
These rules are used to determine ip address allocation ranges,
|
|
host profile interfaces and also to create hardware profile
|
|
information. The method calls corresponding rule hander function
|
|
based on rule name and applies them to appropriate data objects.
|
|
"""
|
|
LOG.info("Apply design rules")
|
|
rules_dir = pkg_resources.resource_filename('spyglass', 'config/')
|
|
rules_file = rules_dir + 'rules.yaml'
|
|
rules_data_raw = self._read_file(rules_file)
|
|
rules_yaml = yaml.safe_load(rules_data_raw)
|
|
rules_data = {}
|
|
rules_data.update(rules_yaml)
|
|
for rule in rules_data.keys():
|
|
rule_name = rules_data[rule]['name']
|
|
function_str = "_apply_rule_" + rule_name
|
|
rule_data_name = rules_data[rule][rule_name]
|
|
function = getattr(self, function_str)
|
|
function(rule_data_name)
|
|
LOG.info("Applying rule:{}".format(rule_name))
|
|
|
|
def _apply_rule_host_profile_interfaces(self, rule_data):
|
|
# TODO(pg710r)Nothing to do as of now since host profile
|
|
# information is already present in plugin data.
|
|
# This function shall be defined if plugin data source
|
|
# doesn't provide host profile information.
|
|
pass
|
|
|
|
def _apply_rule_hardware_profile(self, rule_data):
|
|
""" Apply rules to define host type from hardware profile info.
|
|
|
|
|
|
Host profile will define host types as "controller, compute or
|
|
genesis". The rule_data has pre-defined information to define
|
|
compute or controller based on host_profile. For defining 'genesis'
|
|
the first controller host is defined as genesis."""
|
|
is_genesis = False
|
|
hardware_profile = rule_data[self.data['site_info']['sitetype']]
|
|
# Getting individual racks. The racks are sorted to ensure that the
|
|
# first controller of the first rack is assigned as 'genesis' node.
|
|
for rack in sorted(self.data['baremetal'].keys()):
|
|
# Getting individual hosts in each rack. Sorting of the hosts are
|
|
# done to determine the genesis node.
|
|
for host in sorted(self.data['baremetal'][rack].keys()):
|
|
host_info = self.data['baremetal'][rack][host]
|
|
if (host_info['host_profile'] == hardware_profile[
|
|
'profile_name']['ctrl']):
|
|
if not is_genesis:
|
|
host_info['type'] = 'genesis'
|
|
is_genesis = True
|
|
else:
|
|
host_info['type'] = 'controller'
|
|
else:
|
|
host_info['type'] = 'compute'
|
|
|
|
def _apply_rule_ip_alloc_offset(self, rule_data):
|
|
""" Apply offset rules to update baremetal host ip's and vlan network
|
|
data """
|
|
|
|
# Get network subnets
|
|
self.network_subnets = self._get_network_subnets()
|
|
|
|
self._update_vlan_net_data(rule_data)
|
|
self._update_baremetal_host_ip_data(rule_data)
|
|
|
|
def _update_baremetal_host_ip_data(self, rule_data):
|
|
""" Update baremetal host ip's for applicable networks.
|
|
|
|
|
|
The applicable networks are oob, oam, ksn, storage and overlay.
|
|
These IPs are assigned based on network subnets ranges.
|
|
If a particular ip exists it is overridden."""
|
|
|
|
# Ger defult ip offset
|
|
default_ip_offset = rule_data['default']
|
|
|
|
host_idx = 0
|
|
LOG.info("Update baremetal host ip's")
|
|
for racks in self.data['baremetal'].keys():
|
|
rack_hosts = self.data['baremetal'][racks]
|
|
for host in rack_hosts:
|
|
host_networks = rack_hosts[host]['ip']
|
|
for net in host_networks:
|
|
ips = list(self.network_subnets[net])
|
|
host_networks[net] = str(ips[host_idx + default_ip_offset])
|
|
host_idx = host_idx + 1
|
|
|
|
LOG.debug("Updated baremetal host:\n{}".format(
|
|
pprint.pformat(self.data['baremetal'])))
|
|
|
|
def _update_vlan_net_data(self, rule_data):
|
|
""" Offset allocation rules to determine ip address range(s)
|
|
|
|
|
|
This rule is applied to incoming network data to determine
|
|
network address, gateway ip and other address ranges
|
|
"""
|
|
LOG.info("Apply network design rules")
|
|
|
|
# Collect Rules
|
|
default_ip_offset = rule_data['default']
|
|
oob_ip_offset = rule_data['oob']
|
|
gateway_ip_offset = rule_data['gateway']
|
|
ingress_vip_offset = rule_data['ingress_vip']
|
|
# static_ip_end_offset for non pxe network
|
|
static_ip_end_offset = rule_data['static_ip_end']
|
|
# dhcp_ip_end_offset for pxe network
|
|
dhcp_ip_end_offset = rule_data['dhcp_ip_end']
|
|
|
|
# Set ingress vip and CIDR for bgp
|
|
LOG.info("Apply network design rules:bgp")
|
|
subnet = netaddr.IPNetwork(
|
|
self.data['network']['vlan_network_data']['ingress']['subnet'][0])
|
|
ips = list(subnet)
|
|
self.data['network']['bgp']['ingress_vip'] = str(
|
|
ips[ingress_vip_offset])
|
|
self.data['network']['bgp']['public_service_cidr'] = self.data[
|
|
'network']['vlan_network_data']['ingress']['subnet'][0]
|
|
LOG.debug("Updated network bgp data:\n{}".format(
|
|
pprint.pformat(self.data['network']['bgp'])))
|
|
|
|
LOG.info("Apply network design rules:vlan")
|
|
# Apply rules to vlan networks
|
|
for net_type in self.network_subnets:
|
|
if net_type == 'oob':
|
|
ip_offset = oob_ip_offset
|
|
else:
|
|
ip_offset = default_ip_offset
|
|
|
|
subnet = self.network_subnets[net_type]
|
|
ips = list(subnet)
|
|
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'gateway'] = str(ips[gateway_ip_offset])
|
|
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'reserved_start'] = str(ips[1])
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'reserved_end'] = str(ips[ip_offset])
|
|
|
|
static_start = str(ips[ip_offset + 1])
|
|
static_end = str(ips[static_ip_end_offset])
|
|
|
|
if net_type == 'pxe':
|
|
mid = len(ips) // 2
|
|
static_end = str(ips[mid - 1])
|
|
dhcp_start = str(ips[mid])
|
|
dhcp_end = str(ips[dhcp_ip_end_offset])
|
|
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'dhcp_start'] = dhcp_start
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'dhcp_end'] = dhcp_end
|
|
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'static_start'] = static_start
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'static_end'] = static_end
|
|
|
|
# There is no vlan for oob network
|
|
if (net_type != 'oob'):
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'vlan'] = self.data['network']['vlan_network_data'][
|
|
net_type]['vlan']
|
|
|
|
# OAM have default routes. Only for cruiser. TBD
|
|
if (net_type == 'oam'):
|
|
routes = ["0.0.0.0/0"]
|
|
else:
|
|
routes = []
|
|
self.data['network']['vlan_network_data'][net_type][
|
|
'routes'] = routes
|
|
|
|
LOG.debug("Updated vlan network data:\n{}".format(
|
|
pprint.pformat(self.data['network']['vlan_network_data'])))
|
|
|
|
def load_extracted_data_from_data_source(self, extracted_data):
|
|
"""
|
|
Function called from spyglass.py to pass extracted data
|
|
from input data source
|
|
"""
|
|
# TBR(pg710r): for internal testing
|
|
"""
|
|
raw_data = self._read_file('extracted_data.yaml')
|
|
extracted_data = yaml.safe_load(raw_data)
|
|
"""
|
|
|
|
LOG.info("Loading plugin data source")
|
|
self.data = extracted_data
|
|
LOG.debug("Extracted data from plugin:\n{}".format(
|
|
pprint.pformat(extracted_data)))
|
|
extracted_file = "extracted_file.yaml"
|
|
yaml_file = yaml.dump(extracted_data, default_flow_style=False)
|
|
with open(extracted_file, 'w') as f:
|
|
f.write(yaml_file)
|
|
f.close()
|
|
|
|
# Append region_data supplied from CLI to self.data
|
|
self.data['region_name'] = self.region_name
|
|
|
|
def dump_intermediary_file(self, intermediary_dir):
|
|
""" Writing intermediary yaml """
|
|
LOG.info("Writing intermediary yaml")
|
|
intermediary_file = "{}_intermediary.yaml".format(
|
|
self.data['region_name'])
|
|
# Check of if output dir = intermediary_dir exists
|
|
if intermediary_dir is not None:
|
|
outfile = "{}/{}".format(intermediary_dir, intermediary_file)
|
|
else:
|
|
outfile = intermediary_file
|
|
LOG.info("Intermediary file:{}".format(outfile))
|
|
yaml_file = yaml.dump(self.data, default_flow_style=False)
|
|
with open(outfile, 'w') as f:
|
|
f.write(yaml_file)
|
|
f.close()
|
|
|
|
def generate_intermediary_yaml(self, edit_intermediary=False):
|
|
""" Generating intermediary yaml """
|
|
LOG.info("Start: Generate Intermediary")
|
|
self._apply_design_rules()
|
|
self._get_genesis_node_details()
|
|
# This will validate the extracted data from different sources.
|
|
self._validate_intermediary_data(self.data)
|
|
if edit_intermediary:
|
|
self.edit_intermediary_yaml()
|
|
# This will check if user edited changes are in order.
|
|
self._validate_intermediary_data(self.data)
|
|
self.intermediary_yaml = self.data
|
|
return self.intermediary_yaml
|
|
|
|
def edit_intermediary_yaml(self):
|
|
""" Edit generated data using on browser """
|
|
LOG.info(
|
|
"edit_intermediary_yaml: Invoking web server for yaml editing")
|
|
with tempfile.NamedTemporaryFile(mode='r+') as file_obj:
|
|
yaml.safe_dump(self.data, file_obj, default_flow_style=False)
|
|
host = self._get_genesis_node_ip()
|
|
os.system('yaml-editor -f {0} -h {1}'.format(file_obj.name, host))
|
|
file_obj.seek(0)
|
|
self.data = yaml.safe_load(file_obj)
|