drydock/drydock_provisioner/orchestrator/validations/validator.py

616 lines
27 KiB
Python

# Copyright 2017 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.
"""Business Logic Validation"""
import drydock_provisioner.objects.fields as hd_fields
import drydock_provisioner.error as errors
from netaddr import IPNetwork, IPAddress
from drydock_provisioner.orchestrator.util import SimpleBytes
from drydock_provisioner.objects.task import TaskStatus, TaskStatusMessage
class Validator():
def validate_design(self, site_design, result_status=None):
"""Validate the design in site_design passes all validation rules.
Apply all validation rules to the design in site_design. If result_status is
defined, update it with validation messages. Otherwise a new status instance
will be created and returned.
:param site_design: instance of objects.SiteDesign
:param result_status: instance of objects.TaskStatus
"""
if result_status is None:
result_status = TaskStatus()
validation_error = False
for rule in rule_set:
output = rule(site_design)
result_status.message_list.extend(output)
error_msg = [m for m in output if m.error]
result_status.error_count = result_status.error_count + len(
error_msg)
if len(error_msg) > 0:
validation_error = True
if validation_error:
result_status.set_status(hd_fields.ValidationResult.Failure)
else:
result_status.set_status(hd_fields.ValidationResult.Success)
return result_status
@classmethod
def rational_network_bond(cls, site_design):
"""
Ensures that NetworkLink 'bonding' is rational
"""
message_list = []
site_design = site_design.obj_to_simple()
network_links = site_design.get('network_links', [])
for network_link in network_links:
bonding_mode = network_link.get('bonding_mode', [])
if bonding_mode == 'disabled':
# check to make sure nothing else is specified
if any([
network_link.get(x)
for x in [
'bonding_peer_rate', 'bonding_xmit_hash',
'bonding_mon_rate', 'bonding_up_delay',
'bonding_down_delay'
]
]):
msg = (
'Network Link Bonding Error: If bonding mode is disabled no other bond option can be'
'specified; on BaremetalNode %s' %
network_link.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
elif bonding_mode == '802.3ad':
# check if up_delay and down_delay are >= mon_rate
mon_rate = network_link.get('bonding_mon_rate')
if network_link.get('bonding_up_delay') < mon_rate:
msg = ('Network Link Bonding Error: Up delay is less '
'than mon rate on BaremetalNode %s' %
(network_link.get('name')))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if network_link.get('bonding_down_delay') < mon_rate:
msg = ('Network Link Bonding Error: Down delay is '
'less than mon rate on BaremetalNode %s' %
(network_link.get('name')))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
elif bonding_mode in ['active-backup', 'balanced-rr']:
# make sure hash and peer_rate are NOT defined
if network_link.get('bonding_xmit_hash'):
msg = (
'Network Link Bonding Error: Hash cannot be defined if bond mode is '
'%s, on BaremetalNode %s' % (bonding_mode,
network_link.get('name')))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if network_link.get('bonding_peer_rate'):
msg = (
'Network Link Bonding Error: Peer rate cannot be defined if bond mode is '
'%s, on BaremetalNode %s' % (bonding_mode,
network_link.get('name')))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(
TaskStatusMessage(
msg='Network Link Bonding',
error=False,
ctx_type='NA',
ctx='NA'))
return message_list
@classmethod
def network_trunking_rational(cls, site_design):
"""
Ensures that Network Trunking is Rational
"""
message_list = []
site_design = site_design.obj_to_simple()
network_link_list = site_design.get('network_links', [])
for network_link in network_link_list:
allowed_networks = network_link.get('allowed_networks', [])
# if allowed networks > 1 trunking must be enabled
if (len(allowed_networks) > 1 and network_link.get('trunk_mode') ==
hd_fields.NetworkLinkTrunkingMode.Disabled):
msg = (
'Rational Network Trunking Error: If there is more than 1 allowed network,'
'trunking mode must be enabled; on NetworkLink %s' %
network_link.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
# trunking mode is disabled, default_network must be defined
if (network_link.get(
'trunk_mode') == hd_fields.NetworkLinkTrunkingMode.Disabled
and network_link.get('native_network') is None):
msg = (
'Rational Network Trunking Error: Trunking mode is disabled, a trunking'
'default_network must be defined; on NetworkLink %s' %
network_link.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(
TaskStatusMessage(
msg='Rational Network Trunking',
error=False,
ctx_type='NA',
ctx='NA'))
return message_list
@classmethod
def storage_partitioning(cls, site_design):
"""
Checks storage partitioning
"""
message_list = []
site_design = site_design.obj_to_simple()
baremetal_nodes = site_design.get('baremetal_nodes', [])
volume_group_check_list = []
for baremetal_node in baremetal_nodes:
storage_devices_list = baremetal_node.get('storage_devices', [])
for storage_device in storage_devices_list:
partitions_list = storage_device.get('partitions')
volume_group = storage_device.get('volume_group')
# error if both or neither is defined
if all([partitions_list, volume_group
]) or not any([partitions_list, volume_group]):
msg = ('Storage Partitioning Error: Either a volume group '
'OR partitions must be defined for each storage '
'device; on BaremetalNode '
'%s' % baremetal_node.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
# if there is a volume group add to list
if volume_group is not None:
volume_group_check_list.append(volume_group)
if partitions_list is not None:
for partition in partitions_list:
partition_volume_group = partition.get('volume_group')
fstype = partition.get('fstype')
# error if both are defined
if all([fstype, partition_volume_group]):
msg = ('Storage Partitioning Error: Both a volume group AND file system cannot be '
'defined in a sigle partition; on BaremetalNode %s' % baremetal_node.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg,
error=True,
ctx_type='NA',
ctx='NA'))
# if there is a volume group add to list
if partition_volume_group is not None:
volume_group_check_list.append(volume_group)
# checks all volume groups are assigned to a partition or storage device
# if one exist that wasn't found earlier it is unassigned
all_volume_groups = baremetal_node.get('volume_groups', [])
for volume_group in all_volume_groups:
if volume_group.get('name') not in volume_group_check_list:
msg = ('Storage Partitioning Error: A volume group must be assigned to a storage device or '
'partition; volume group %s on BaremetalNode %s' %
(volume_group.get('name'), baremetal_node.get('name')))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(
TaskStatusMessage(
msg='Storage Partitioning',
error=False,
ctx_type='NA',
ctx='NA'))
return message_list
@classmethod
def unique_network_check(cls, site_design):
"""
Ensures that each network name appears at most once between all NetworkLink
allowed networks
"""
message_list = []
site_design = site_design.obj_to_simple()
network_link_list = site_design.get('network_links', [])
compare = {}
for network_link in network_link_list:
allowed_network_list = network_link.get('allowed_networks', [])
compare[network_link.get('name')] = allowed_network_list
# This checks the allowed networks for each network link aginst
# the other allowed networks
checked_pairs = []
for network_link_name in compare:
allowed_network_list_1 = compare[network_link_name]
for network_link_name_2 in compare:
if (network_link_name is not network_link_name_2
and sorted([network_link_name, network_link_name_2
]) not in checked_pairs):
checked_pairs.append(
sorted([network_link_name, network_link_name_2]))
allowed_network_list_2 = compare[network_link_name_2]
# creates a list of duplicated allowed networks
duplicated_names = [
i for i in allowed_network_list_1
if i in allowed_network_list_2
]
for name in duplicated_names:
msg = ('Unique Network Error: Allowed network %s duplicated on NetworkLink %s and NetworkLink '
'%s' % (name, network_link_name, network_link_name_2))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(
TaskStatusMessage(
msg='Unique Network', error=False, ctx_type='NA',
ctx='NA'))
return message_list
@classmethod
def mtu_rational(cls, site_design):
"""
Ensure that the MTU for each network is equal or less than the MTU defined
for the parent NetworkLink for that network.
Ensure that each defined MTU is a rational size, say > 1400 and < 64000
"""
message_list = []
site_design = site_design.obj_to_simple()
network_links = site_design.get('network_links', [])
networks = site_design.get('networks', [])
parent_mtu_check = {}
for network_link in network_links:
mtu = network_link.get('mtu')
# check mtu > 1400 and < 64000
if mtu and (mtu < 1400 or mtu > 64000):
msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network Link %s.' % network_link.get('name')
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
# add assigned network to dict with parent mtu
assigned_network = network_link.get('native_network')
parent_mtu_check[assigned_network] = mtu
for network in networks:
network_mtu = network.get('mtu')
# check mtu > 1400 and < 64000
if network_mtu and (network_mtu < 1400 or network_mtu > 64000):
msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network %s.' % network.get('name')
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
name = network.get('name')
parent_mtu = parent_mtu_check.get(name)
if network_mtu and parent_mtu:
# check to make sure mtu for network is <= parent network link
if network_mtu > parent_mtu:
msg = 'Mtu Error: Mtu must be <= the parent Network Link; for Network %s' % (network.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(TaskStatusMessage(msg='Mtu', error=False, ctx_type='NA', ctx='NA'))
return message_list
@classmethod
def storage_sizing(cls, site_design):
"""
Ensures that for a partitioned physical device or logical volumes
in a volume group, if sizing is a percentage then those percentages
do not sum > 99% and have no negitive values
"""
message_list = []
site_design = site_design.obj_to_simple()
baremetal_nodes = site_design.get('baremetal_nodes', [])
for baremetal_node in baremetal_nodes:
storage_device_list = baremetal_node.get('storage_devices', [])
for storage_device in storage_device_list:
partition_list = storage_device.get('partitions', [])
partition_sum = 0
for partition in partition_list:
size = partition.get('size')
percent = size.split('%')
if len(percent) == 2:
if int(percent[0]) < 0:
msg = ('Storage Sizing Error: Storage partition size is < 0 '
'on Baremetal Node %s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
partition_sum += int(percent[0])
if partition_sum > 99:
msg = ('Storage Sizing Error: Storage partition size is greater than '
'99 on Baremetal Node %s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
volume_groups = baremetal_node.get('volume_groups', [])
volume_sum = 0
for volume_group in volume_groups:
logical_volume_list = volume_group.get('logical_volumes', [])
for logical_volume in logical_volume_list:
size = logical_volume.get('size')
percent = size.split('%')
if len(percent) == 2:
if int(percent[0]) < 0:
msg = ('Storage Sizing Error: Storage volume size is < 0 '
'on Baremetal Node %s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
volume_sum += int(percent[0])
if volume_sum > 99:
msg = ('Storage Sizing Error: Storage volume size is greater '
'than 99 on Baremetal Node %s.' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(TaskStatusMessage(msg='Storage Sizing', error=False, ctx_type='NA', ctx='NA'))
return message_list
@classmethod
def no_duplicate_IPs_check(cls, site_design):
"""
Ensures that the same IP is not assigned to multiple baremetal nodes.
"""
found_ips = {} # Dictionary Format - IP address: BaremetalNode name
message_list = []
site_design = site_design.obj_to_simple()
baremetal_nodes_list = site_design.get('baremetal_nodes', [])
if not baremetal_nodes_list:
msg = 'No BaremetalNodes Found.'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
else:
for node in baremetal_nodes_list:
addressing_list = node.get('addressing', [])
for ip_address in addressing_list:
address = ip_address.get('address')
node_name = node.get('name')
if address in found_ips and address is not None:
msg = ('Error! Duplicate IP Address Found: %s '
'is in use by both %s and %s.'
% (address, found_ips[address], node_name))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
elif address is not None:
found_ips[address] = node_name
if not message_list:
msg = 'No Duplicate IP Addresses.'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
return message_list
@classmethod
def boot_storage_rational(cls, site_design):
"""
Ensures that root volume is defined and is at least 20GB and that boot volume is at least 1 GB
"""
message_list = []
site_design = site_design.obj_to_simple()
BYTES_IN_GB = SimpleBytes.calulate_bytes('1GB')
baremetal_node_list = site_design.get('baremetal_nodes', [])
for baremetal_node in baremetal_node_list:
storage_devices_list = baremetal_node.get('storage_devices', [])
root_set = False
for storage_device in storage_devices_list:
partitions_list = storage_device.get('partitions', [])
for host_partition in partitions_list:
if host_partition.get('name') == 'root':
size = host_partition.get('size')
try:
cal_size = SimpleBytes.calulate_bytes(size)
root_set = True
# check if size < 20GB
if cal_size < 20 * BYTES_IN_GB:
msg = ('Boot Storage Error: Root volume must be > 20GB on BaremetalNode '
'%s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
except errors.InvalidSizeFormat as e:
msg = ('Boot Storage Error: Root volume has an invalid size format on BaremetalNode'
'%s.' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
# check make sure root has been defined and boot volume > 1GB
if root_set and host_partition.get('name') == 'boot':
size = host_partition.get('size')
try:
cal_size = SimpleBytes.calulate_bytes(size)
# check if size < 1GB
if cal_size < BYTES_IN_GB:
msg = ('Boot Storage Error: Boot volume must be > 1GB on BaremetalNode '
'%s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
except errors.InvalidSizeFormat as e:
msg = ('Boot Storage Error: Boot volume has an invalid size format on BaremetalNode '
'%s.' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
# This must be set
if not root_set:
msg = ('Boot Storage Error: Root volume has to be set and must be > 20GB on BaremetalNode '
'%s' % baremetal_node.get('name'))
message_list.append(TaskStatusMessage(msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
message_list.append(TaskStatusMessage(msg='Boot Storage', error=False, ctx_type='NA', ctx='NA'))
return message_list
@classmethod
def ip_locality_check(cls, site_design):
"""
Ensures that IP addresses are within defined CIDR ranges.
"""
network_dict = {} # Dictionary Format - network name: cidr
message_list = []
site_design = site_design.obj_to_simple()
baremetal_nodes_list = site_design.get('baremetal_nodes', [])
network_list = site_design.get('networks', [])
if not network_list:
msg = 'No networks found.'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
else:
for net in network_list:
name = net.get('name')
cidr = net.get('cidr')
routes = net.get('routes', [])
cidr_range = IPNetwork(cidr)
network_dict[name] = cidr_range
if routes:
for r in routes:
gateway = r.get('gateway')
if not gateway:
msg = 'No gateway found for route %s.' % routes
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
else:
ip = IPAddress(gateway)
if ip not in cidr_range:
msg = ('IP Locality Error: The gateway IP Address %s '
'is not within the defined CIDR: %s of %s.'
% (gateway, cidr, name))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not baremetal_nodes_list:
msg = 'No baremetal_nodes found.'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
else:
for node in baremetal_nodes_list:
addressing_list = node.get('addressing', [])
for ip_address in addressing_list:
ip_address_network_name = ip_address.get('network')
address = ip_address.get('address')
ip_type = ip_address.get('type')
if ip_type is not 'dhcp':
if ip_address_network_name not in network_dict:
msg = 'IP Locality Error: %s is not a valid network.' \
% (ip_address_network_name)
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
else:
if IPAddress(address) not in IPNetwork(network_dict[ip_address_network_name]):
msg = ('IP Locality Error: The IP Address %s '
'is not within the defined CIDR: %s of %s .'
% (address, network_dict[ip_address_network_name],
ip_address_network_name))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if not message_list:
msg = 'IP Locality Success'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
return message_list
rule_set = [
Validator.rational_network_bond,
Validator.network_trunking_rational,
Validator.storage_partitioning,
Validator.unique_network_check,
Validator.mtu_rational,
Validator.storage_sizing,
Validator.ip_locality_check,
Validator.no_duplicate_IPs_check,
Validator.boot_storage_rational,
]