Use new validator model for validation

- Update all validator rules to publish messages using
  new format
- Update unit tests to follow new validation framework
- Add validation for no tagged VLANs on non-trunked links
- Add validation for node interfaces attaching to networks
  not allowed on the interface link
- Unit test change to work around an issue where coverage testing
  and unit testing show different results

- Update tox to skip E126 to match YAPF formatting

Change-Id: Ifef21112896b88c2bd2b361630e041806ebb6663
This commit is contained in:
Scott Hussey 2018-03-06 13:41:13 -06:00
parent 8c2540c3f2
commit c27c8e6c9b
40 changed files with 649 additions and 713 deletions

View File

@ -56,10 +56,12 @@ def start_api(state_manager=None, ingester=None, orchestrator=None):
# v1.0 of Drydock API # v1.0 of Drydock API
v1_0_routes = [ v1_0_routes = [
# API for managing orchestrator tasks # API for managing orchestrator tasks
('/health', HealthResource(state_manager=state_manager, ('/health',
orchestrator=orchestrator)), HealthResource(
('/health/extended', HealthExtendedResource(state_manager=state_manager, state_manager=state_manager, orchestrator=orchestrator)),
orchestrator=orchestrator)), ('/health/extended',
HealthExtendedResource(
state_manager=state_manager, orchestrator=orchestrator)),
('/tasks', ('/tasks',
TasksResource(state_manager=state_manager, TasksResource(state_manager=state_manager,
orchestrator=orchestrator)), orchestrator=orchestrator)),

View File

@ -27,6 +27,7 @@ class HealthResource(StatefulResource):
""" """
Returns empty response body that Drydock is healthy Returns empty response body that Drydock is healthy
""" """
def __init__(self, orchestrator=None, **kwargs): def __init__(self, orchestrator=None, **kwargs):
"""Object initializer. """Object initializer.
@ -39,15 +40,18 @@ class HealthResource(StatefulResource):
""" """
Returns 204 on healthy, otherwise 503, without response body. Returns 204 on healthy, otherwise 503, without response body.
""" """
hc = HealthCheckCombined(state_manager=self.state_manager, hc = HealthCheckCombined(
orchestrator=self.orchestrator, state_manager=self.state_manager,
extended=False) orchestrator=self.orchestrator,
extended=False)
return hc.get(req, resp) return hc.get(req, resp)
class HealthExtendedResource(StatefulResource): class HealthExtendedResource(StatefulResource):
""" """
Returns response body that Drydock is healthy Returns response body that Drydock is healthy
""" """
def __init__(self, orchestrator=None, **kwargs): def __init__(self, orchestrator=None, **kwargs):
"""Object initializer. """Object initializer.
@ -61,15 +65,18 @@ class HealthExtendedResource(StatefulResource):
""" """
Returns 200 on success, otherwise 503, with a response body. Returns 200 on success, otherwise 503, with a response body.
""" """
hc = HealthCheckCombined(state_manager=self.state_manager, hc = HealthCheckCombined(
orchestrator=self.orchestrator, state_manager=self.state_manager,
extended=True) orchestrator=self.orchestrator,
extended=True)
return hc.get(req, resp) return hc.get(req, resp)
class HealthCheckCombined(object): class HealthCheckCombined(object):
""" """
Returns Drydock health check status. Returns Drydock health check status.
""" """
def __init__(self, state_manager=None, orchestrator=None, extended=False): def __init__(self, state_manager=None, orchestrator=None, extended=False):
"""Object initializer. """Object initializer.
@ -90,18 +97,22 @@ class HealthCheckCombined(object):
if now is None: if now is None:
raise Exception('None received from database for now()') raise Exception('None received from database for now()')
except Exception as ex: except Exception as ex:
hcm = HealthCheckMessage(msg='Unable to connect to database', error=True) hcm = HealthCheckMessage(
msg='Unable to connect to database', error=True)
health_check.add_detail_msg(msg=hcm) health_check.add_detail_msg(msg=hcm)
# Test MaaS connection # Test MaaS connection
try: try:
task = self.orchestrator.create_task(action=hd_fields.OrchestratorAction.Noop) task = self.orchestrator.create_task(
maas_validation = ValidateNodeServices(task, self.orchestrator, self.state_manager) action=hd_fields.OrchestratorAction.Noop)
maas_validation = ValidateNodeServices(task, self.orchestrator,
self.state_manager)
maas_validation.start() maas_validation.start()
if maas_validation.task.get_status() == ActionResult.Failure: if maas_validation.task.get_status() == ActionResult.Failure:
raise Exception('MaaS task failure') raise Exception('MaaS task failure')
except Exception as ex: except Exception as ex:
hcm = HealthCheckMessage(msg='Unable to connect to MaaS', error=True) hcm = HealthCheckMessage(
msg='Unable to connect to MaaS', error=True)
health_check.add_detail_msg(msg=hcm) health_check.add_detail_msg(msg=hcm)
if self.extended: if self.extended:

View File

@ -17,6 +17,8 @@ import json
from drydock_provisioner import policy from drydock_provisioner import policy
from drydock_provisioner.control.base import StatefulResource from drydock_provisioner.control.base import StatefulResource
from drydock_provisioner.objects import fields as hd_fields
import drydock_provisioner.error as errors import drydock_provisioner.error as errors
@ -36,21 +38,6 @@ class ValidationResource(StatefulResource):
@policy.ApiEnforcer('physical_provisioner:validate_site_design') @policy.ApiEnforcer('physical_provisioner:validate_site_design')
def on_post(self, req, resp): def on_post(self, req, resp):
# create resp message
resp_message = {
'kind': 'Status',
'apiVersion': 'v1.0',
'metaData': {},
'status': '',
'message': '',
'reason': 'Validation',
'details': {
'errorCount': 0,
'messageList': []
},
'code': '',
}
try: try:
json_data = self.req_json(req) json_data = self.req_json(req)
@ -68,29 +55,25 @@ class ValidationResource(StatefulResource):
self.error(req.context, err_message) self.error(req.context, err_message)
return self.return_error(resp, falcon.HTTP_400, err_message) return self.return_error(resp, falcon.HTTP_400, err_message)
message, design_data = self.orchestrator.get_effective_site( validation, design_data = self.orchestrator.get_effective_site(
design_ref) design_ref)
resp_message['details']['errorCount'] = message.error_count if validation.status == hd_fields.ValidationResult.Success:
resp_message['details']['messageList'] = [ resp_message = validation.to_dict()
m.to_dict() for m in message.message_list
]
if message.error_count == 0:
resp_message['status'] = 'Success'
resp_message['message'] = 'Drydock Validations succeeded'
resp_message['code'] = 200 resp_message['code'] = 200
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
resp.body = json.dumps(resp_message) resp.body = json.dumps(resp_message)
else: else:
resp_message['status'] = 'Failure' resp_message = validation.to_dict()
resp_message['message'] = 'Drydock Validations failed'
resp_message['code'] = 400 resp_message['code'] = 400
resp.status = falcon.HTTP_400 resp.status = falcon.HTTP_400
resp.body = json.dumps(resp_message) resp.body = json.dumps(resp_message)
except errors.InvalidFormat as e: except errors.InvalidFormat as e:
err_message = str(e) err_message = str(e)
resp.status = falcon.HTTP_400
self.error(req.context, err_message) self.error(req.context, err_message)
self.return_error(resp, falcon.HTTP_400, err_message) self.return_error(resp, falcon.HTTP_400, err_message)
except Exception as ex:
err_message = str(ex)
self.error(req.context, err_message)
self.return_error(resp, falcon.HTTP_500, err_message)

View File

@ -53,6 +53,7 @@ class InvalidDesignReference(DesignError):
""" """
pass pass
class UnsupportedDocumentType(DesignError): class UnsupportedDocumentType(DesignError):
""" """
**Message:** *Site definition document in an unknown format*. **Message:** *Site definition document in an unknown format*.

View File

@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
"""Models for representing health check status.""" """Models for representing health check status."""
class HealthCheck(object): class HealthCheck(object):
"""Specialized status for health check status.""" """Specialized status for health check status."""

View File

@ -60,7 +60,13 @@ class Validation(TaskStatus):
class ValidationMessage(TaskStatusMessage): class ValidationMessage(TaskStatusMessage):
"""Message describing details of a validation.""" """Message describing details of a validation."""
def __init__(self, msg, name, error=False, level=None, docs=None, diagnostic=None): def __init__(self,
msg,
name,
error=False,
level=None,
docs=None,
diagnostic=None):
self.name = name self.name = name
self.message = msg self.message = msg
self.error = error self.error = error
@ -100,7 +106,8 @@ class DocumentReference(base.DrydockObject):
super().__init__(**kwargs) super().__init__(**kwargs)
if (self.doc_type == hd_fields.DocumentType.Deckhand): if (self.doc_type == hd_fields.DocumentType.Deckhand):
if not all([self.doc_schema, self.doc_name]): if not all([self.doc_schema, self.doc_name]):
raise ValueError("doc_schema and doc_name required for Deckhand sources.") raise ValueError(
"doc_schema and doc_name required for Deckhand sources.")
else: else:
raise errors.UnsupportedDocumentType( raise errors.UnsupportedDocumentType(
"Document type %s not supported." % self.doc_type) "Document type %s not supported." % self.doc_type)

View File

@ -285,7 +285,7 @@ class Orchestrator(object):
val = Validator(self) val = Validator(self)
try: try:
status, site_design = self.get_described_site(design_ref) status, site_design = self.get_described_site(design_ref)
if status.status == hd_fields.ActionResult.Success: if status.status == hd_fields.ValidationResult.Success:
self.compute_model_inheritance(site_design) self.compute_model_inheritance(site_design)
self.compute_bootaction_targets(site_design) self.compute_bootaction_targets(site_design)
self.render_route_domains(site_design) self.render_route_domains(site_design)

View File

@ -15,98 +15,81 @@ from drydock_provisioner.orchestrator.validations.validators import Validators
import drydock_provisioner.error as errors import drydock_provisioner.error as errors
from drydock_provisioner.orchestrator.util import SimpleBytes from drydock_provisioner.orchestrator.util import SimpleBytes
from drydock_provisioner.objects.task import TaskStatusMessage
class BootStorageRational(Validators): class BootStorageRational(Validators):
def __init__(self): def __init__(self):
super().__init__('Boot Storage Rational', 1001) super().__init__('Rational Boot Storage', 'DD1001')
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
Ensures that root volume is defined and is at least 20GB and that boot volume is at least 1 GB 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.calculate_bytes('1GB') BYTES_IN_GB = SimpleBytes.calculate_bytes('1GB')
baremetal_node_list = site_design.get('baremetal_nodes', []) baremetal_node_list = site_design.baremetal_nodes or []
for baremetal_node in baremetal_node_list: for baremetal_node in baremetal_node_list:
storage_devices_list = baremetal_node.get('storage_devices', []) storage_devices_list = baremetal_node.storage_devices or []
root_set = False root_set = False
for storage_device in storage_devices_list: for storage_device in storage_devices_list:
partitions_list = storage_device.get('partitions', []) partitions_list = storage_device.partitions or []
for host_partition in partitions_list: for host_partition in partitions_list:
if host_partition.get('name') == 'root': if host_partition.name == 'root':
size = host_partition.get('size') size = host_partition.size
try: try:
cal_size = SimpleBytes.calculate_bytes(size) cal_size = SimpleBytes.calculate_bytes(size)
root_set = True root_set = True
# check if size < 20GB # check if size < 20GB
if cal_size < 20 * BYTES_IN_GB: if cal_size < 20 * BYTES_IN_GB:
msg = ( msg = (
'Boot Storage Error: Root volume must be > 20GB on BaremetalNode ' 'Root volume must be > 20GB on BaremetalNode '
'%s' % baremetal_node.get('name')) '%s' % baremetal_node.name)
message_list.append( self.report_error(
TaskStatusMessage( msg, [baremetal_node.doc_ref],
msg=msg, "Configure a larger root volume")
error=True,
ctx_type='NA',
ctx='NA'))
except errors.InvalidSizeFormat as e: except errors.InvalidSizeFormat as e:
msg = ( msg = (
'Boot Storage Error: Root volume has an invalid size format on BaremetalNode' 'Root volume has an invalid size format on BaremetalNode'
'%s.' % baremetal_node.get('name')) '%s.' % baremetal_node.name)
message_list.append( self.report_error(msg, [
TaskStatusMessage( baremetal_node.doc_ref
msg=msg, ], "Use a valid root volume storage specification."
error=True, )
ctx_type='NA',
ctx='NA'))
# check make sure root has been defined and boot volume > 1GB # check make sure root has been defined and boot volume > 1GB
if root_set and host_partition.get('name') == 'boot': if root_set and host_partition.name == 'boot':
size = host_partition.get('size') size = host_partition.size
try: try:
cal_size = SimpleBytes.calculate_bytes(size) cal_size = SimpleBytes.calculate_bytes(size)
# check if size < 1GB # check if size < 1GB
if cal_size < BYTES_IN_GB: if cal_size < BYTES_IN_GB:
msg = ( msg = (
'Boot Storage Error: Boot volume must be > 1GB on BaremetalNode ' 'Boot volume must be > 1GB on BaremetalNode '
'%s' % baremetal_node.get('name')) '%s' % baremetal_node.name)
message_list.append( self.report_error(
TaskStatusMessage( msg, [baremetal_node.doc_ref],
msg=msg, "Configure a larger boot volume.")
error=True,
ctx_type='NA',
ctx='NA'))
except errors.InvalidSizeFormat as e: except errors.InvalidSizeFormat as e:
msg = ( msg = (
'Boot Storage Error: Boot volume has an invalid size format on BaremetalNode ' 'Boot volume has an invalid size format on BaremetalNode '
'%s.' % baremetal_node.get('name')) '%s.' % baremetal_node.name)
message_list.append( self.report_error(msg, [
TaskStatusMessage( baremetal_node.doc_ref
msg=msg, ], "Use a valid boot volume storage specification."
error=True, )
ctx_type='NA',
ctx='NA'))
# This must be set # This must be set
if not root_set: if not root_set:
msg = ( msg = (
'Boot Storage Error: Root volume has to be set and must be > 20GB on BaremetalNode ' 'Root volume has to be set and must be > 20GB on BaremetalNode '
'%s' % baremetal_node.get('name')) '%s' % baremetal_node.name)
message_list.append( self.report_error(msg, [
TaskStatusMessage( baremetal_node.doc_ref
msg=msg, error=True, ctx_type='NA', ctx='NA')) ], "All nodes require a defined root volume at least 20GB in size."
)
if not message_list: return
message_list.append(
TaskStatusMessage(
msg='Boot Storage', error=False, ctx_type='NA', ctx='NA'))
return Validators.report_results(self, message_list)

View File

@ -15,34 +15,31 @@ from drydock_provisioner.orchestrator.validations.validators import Validators
from netaddr import IPNetwork, IPAddress from netaddr import IPNetwork, IPAddress
from drydock_provisioner.objects.task import TaskStatusMessage
class IpLocalityCheck(Validators): class IpLocalityCheck(Validators):
def __init__(self): def __init__(self):
super().__init__('IP Locality Check', 1002) super().__init__('IP Locality Check', "DD2002")
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
Ensures that each IP addresses assigned to a baremetal node is within the defined CIDR for the network. Also Ensures that each IP addresses assigned to a baremetal node is within the defined CIDR for the network. Also
verifies that the gateway IP for each static route of a network is within that network's CIDR. verifies that the gateway IP for each static route of a network is within that network's CIDR.
""" """
network_dict = {} # Dictionary Format - network name: cidr network_dict = {} # Dictionary Format - network name: cidr
message_list = []
site_design = site_design.obj_to_simple() baremetal_nodes_list = site_design.baremetal_nodes or []
baremetal_nodes_list = site_design.get('baremetal_nodes', []) network_list = site_design.networks or []
network_list = site_design.get('networks', [])
if not network_list: if not network_list:
msg = 'No networks found.' msg = 'No networks found.'
message_list.append( self.report_warn(
TaskStatusMessage( msg, [],
msg=msg, error=False, ctx_type='NA', ctx='NA')) 'Site design likely incomplete without defined networks')
else: else:
for net in network_list: for net in network_list:
name = net.get('name') name = net.name
cidr = net.get('cidr') cidr = net.cidr
routes = net.get('routes', []) routes = net.routes or []
cidr_range = IPNetwork(cidr) cidr_range = IPNetwork(cidr)
network_dict[name] = cidr_range network_dict[name] = cidr_range
@ -53,68 +50,52 @@ class IpLocalityCheck(Validators):
if not gateway: if not gateway:
msg = 'No gateway found for route %s.' % routes msg = 'No gateway found for route %s.' % routes
message_list.append( self.report_error(
TaskStatusMessage( msg, [net.doc_ref],
msg=msg, diagnostic=
error=True, "Define a network-local gateway for the route."
ctx_type='NA', )
ctx='NA'))
else: else:
ip = IPAddress(gateway) ip = IPAddress(gateway)
if ip not in cidr_range: if ip not in cidr_range:
msg = ( msg = (
'IP Locality Error: The gateway IP Address %s ' 'The gateway IP Address %s is not within the defined CIDR: %s of %s.'
'is not within the defined CIDR: %s of %s.'
% (gateway, cidr, name)) % (gateway, cidr, name))
message_list.append( self.report_error(
TaskStatusMessage( msg, [net.doc_ref],
msg=msg, "Route gateways must reside on the local network. Check "
error=True, "gateway IP and CIDR netmask.")
ctx_type='NA',
ctx='NA'))
if not baremetal_nodes_list: if not baremetal_nodes_list:
msg = 'No baremetal_nodes found.' msg = 'No baremetal_nodes found.'
message_list.append( self.report_warn(
TaskStatusMessage( msg, [],
msg=msg, error=False, ctx_type='NA', ctx='NA')) "site design likely incomplete without defined networks.")
else: else:
for node in baremetal_nodes_list: for node in baremetal_nodes_list:
addressing_list = node.get('addressing', []) addressing_list = node.addressing or []
for ip_address in addressing_list: for ip_address in addressing_list:
ip_address_network_name = ip_address.get('network') ip_address_network_name = ip_address.network
address = ip_address.get('address') address = ip_address.address
ip_type = ip_address.get('type') ip_type = ip_address.type
if ip_type is not 'dhcp': if ip_type is not 'dhcp':
if ip_address_network_name not in network_dict: if ip_address_network_name not in network_dict:
msg = 'IP Locality Error: %s is not a valid network.' \ msg = '%s is not a valid network.' \
% (ip_address_network_name) % (ip_address_network_name)
message_list.append( self.report_error(msg, [
TaskStatusMessage( node.doc_ref
msg=msg, ], "Define network or correct address definition.")
error=True,
ctx_type='NA',
ctx='NA'))
else: else:
if IPAddress(address) not in IPNetwork( if IPAddress(address) not in IPNetwork(
network_dict[ip_address_network_name]): network_dict[ip_address_network_name]):
msg = ( msg = (
'IP Locality Error: The IP Address %s ' 'The IP Address %s is not within the defined CIDR: %s of %s .'
'is not within the defined CIDR: %s of %s .'
% (address, % (address,
network_dict[ip_address_network_name], network_dict[ip_address_network_name],
ip_address_network_name)) ip_address_network_name))
message_list.append( self.report_error(
TaskStatusMessage( msg, [node.doc_ref],
msg=msg, "Define a valid address for this network.")
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 Validators.report_results(self, message_list) return

View File

@ -13,66 +13,63 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class MtuRational(Validators): class MtuRational(Validators):
def __init__(self): MIN_MTU_SIZE = 1280
super().__init__('MTU Rational', 1003) MAX_MTU_SIZE = 65536
def execute(self, site_design, orchestrator=None): def __init__(self):
super().__init__('MTU Rationality', 'DD2003')
def run_validation(self, site_design, orchestrator=None):
""" """
Ensure that the MTU for each network is equal or less than the MTU defined Ensure that the MTU for each network is equal or less than the MTU defined
for the parent NetworkLink for that network. for the parent NetworkLink for that network.
Ensure that each defined MTU is a rational size, say > 1400 and < 64000 Ensure that each defined MTU is a rational size, say > 1400 and < 64000
""" """
message_list = [] network_links = site_design.network_links or []
site_design = site_design.obj_to_simple() networks = site_design.networks or []
network_links = site_design.get('network_links', [])
networks = site_design.get('networks', [])
parent_mtu_check = {} parent_mtu_check = {}
for network_link in network_links: for network_link in network_links:
mtu = network_link.get('mtu') mtu = network_link.mtu
# check mtu > 1400 and < 64000 if mtu and (mtu < MtuRational.MIN_MTU_SIZE
if mtu and (mtu < 1400 or mtu > 64000): or mtu > MtuRational.MAX_MTU_SIZE):
msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network Link %s.' % network_link.get( msg = ("MTU must be between %d and %d, value is %d" %
'name') (MtuRational.MIN_MTU_SIZE, MtuRational.MAX_MTU_SIZE,
message_list.append( mtu))
TaskStatusMessage( self.report_error(
msg=msg, error=True, ctx_type='NA', ctx='NA')) msg, [network_link.doc_ref],
"Define a valid MTU. Standard is 1500, Jumbo is 9100.")
# add assigned network to dict with parent mtu # add assigned network to dict with parent mtu
assigned_network = network_link.get('native_network') assigned_network = network_link.native_network
parent_mtu_check[assigned_network] = mtu parent_mtu_check[assigned_network] = mtu
for network in networks: for network in networks:
network_mtu = network.get('mtu') network_mtu = network.mtu
# check mtu > 1400 and < 64000 if network_mtu and (network_mtu < MtuRational.MIN_MTU_SIZE
if network_mtu and (network_mtu < 1400 or network_mtu > 64000): or network_mtu > MtuRational.MAX_MTU_SIZE):
msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network %s.' % network.get( msg = ("MTU must be between %d and %d, value is %d" %
'name') (MtuRational.MIN_MTU_SIZE, MtuRational.MAX_MTU_SIZE,
message_list.append( mtu))
TaskStatusMessage( self.report_error(
msg=msg, error=True, ctx_type='NA', ctx='NA')) msg, [network.doc_ref],
"Define a valid MTU. Standard is 1500, Jumbo is 9100.")
name = network.get('name') name = network.name
parent_mtu = parent_mtu_check.get(name) parent_mtu = parent_mtu_check.get(name)
if network_mtu and parent_mtu: if network_mtu and parent_mtu:
# check to make sure mtu for network is <= parent network link # check to make sure mtu for network is <= parent network link
if network_mtu > parent_mtu: if network_mtu > parent_mtu:
msg = 'Mtu Error: Mtu must be <= the parent Network Link; for Network %s' % ( msg = 'MTU must be <= the parent Network Link; for Network %s' % (
network.get('name')) network.name)
message_list.append( self.report_error(msg, [
TaskStatusMessage( network.doc_ref
msg=msg, error=True, ctx_type='NA', ctx='NA')) ], "Define a MTU less than or equal to that of the carrying network link."
)
if not message_list: return
message_list.append(
TaskStatusMessage(
msg='Mtu', error=False, ctx_type='NA', ctx='NA'))
return Validators.report_results(self, message_list)

View File

@ -14,57 +14,49 @@
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.fields as hd_fields
from drydock_provisioner.objects.task import TaskStatusMessage
class NetworkTrunkingRational(Validators): class NetworkTrunkingRational(Validators):
def __init__(self): def __init__(self):
super().__init__('Network Trunking Rational', 1004) super().__init__('Network Trunking Rationalty', "DD2004")
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
This check ensures that for each NetworkLink if the allowed networks are greater then 1 trunking mode is This check ensures that for each NetworkLink if the allowed networks are greater then 1 trunking mode is
enabled. It also makes sure that if trunking mode is disabled then a default network is defined. enabled. It also makes sure that if trunking mode is disabled then a default network is defined.
""" """
message_list = [] network_link_list = site_design.network_links or []
site_design = site_design.obj_to_simple()
network_link_list = site_design.get('network_links', [])
for network_link in network_link_list: for network_link in network_link_list:
allowed_networks = network_link.get('allowed_networks', []) allowed_networks = network_link.allowed_networks
# if allowed networks > 1 trunking must be enabled # if allowed networks > 1 trunking must be enabled
if (len(allowed_networks) > 1 and network_link.get('trunk_mode') == if (len(allowed_networks) > 1 and network_link.trunk_mode ==
hd_fields.NetworkLinkTrunkingMode.Disabled): hd_fields.NetworkLinkTrunkingMode.Disabled):
msg = ('If there is more than 1 allowed network,'
msg = ( 'trunking mode must be enabled')
'Rational Network Trunking Error: If there is more than 1 allowed network,' self.report_error(msg, [
'trunking mode must be enabled; on NetworkLink %s' % network_link.doc_ref
network_link.get('name')) ], "Reduce the allowed network list to 1 or enable trunking on the link."
)
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
# trunking mode is disabled, default_network must be defined # trunking mode is disabled, default_network must be defined
if (network_link.get( if (network_link.trunk_mode ==
'trunk_mode') == hd_fields.NetworkLinkTrunkingMode.Disabled hd_fields.NetworkLinkTrunkingMode.Disabled
and network_link.get('native_network') is None): and network_link.native_network is None):
msg = ( msg = 'Trunking mode is disabled, a trunking default_network must be defined'
'Rational Network Trunking Error: Trunking mode is disabled, a trunking' self.report_error(
'default_network must be defined; on NetworkLink %s' % msg, [network_link.doc_ref],
network_link.get('name')) "Non-trunked links must have a native network defined.")
elif (network_link.trunk_mode ==
hd_fields.NetworkLinkTrunkingMode.Disabled
and network_link.native_network is not None):
network = site_design.get_network(network_link.native_network)
if network and network.vlan_id:
msg = "Network link native network has a defined VLAN tag."
self.report_error(msg, [
network.doc_ref, network_link.doc_ref
], "Tagged network not allowed on non-trunked network links."
)
message_list.append( return
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 Validators.report_results(self, message_list)

View File

@ -13,51 +13,40 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class NoDuplicateIpsCheck(Validators): class NoDuplicateIpsCheck(Validators):
def __init__(self): def __init__(self):
super().__init__('No Duplicate IPs Check', 1005) super().__init__('Duplicated IP Check', "DD2005")
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
Ensures that the same IP is not assigned to multiple baremetal node definitions by checking each new IP against Ensures that the same IP is not assigned to multiple baremetal node definitions by checking each new IP against
the list of known IPs. If the IP is unique no error is thrown and the new IP will be added to the list to be the list of known IPs. If the IP is unique no error is thrown and the new IP will be added to the list to be
checked against in the future. checked against in the future.
""" """
found_ips = {} # Dictionary Format - IP address: BaremetalNode name found_ips = {} # Dictionary Format - IP address: BaremetalNode name
message_list = []
site_design = site_design.obj_to_simple() baremetal_nodes_list = site_design.baremetal_nodes or []
baremetal_nodes_list = site_design.get('baremetal_nodes', [])
if not baremetal_nodes_list: if not baremetal_nodes_list:
msg = 'No BaremetalNodes Found.' msg = 'No BaremetalNodes Found.'
message_list.append( self.report_warn(
TaskStatusMessage( msg, [],
msg=msg, error=False, ctx_type='NA', ctx='NA')) "Site design unlikely complete with no defined baremetal nodes."
)
else: else:
for node in baremetal_nodes_list: for node in baremetal_nodes_list:
addressing_list = node.get('addressing', []) addressing_list = node.addressing or []
for ip_address in addressing_list: for ip_address in addressing_list:
address = ip_address.get('address') address = ip_address.address
node_name = node.get('name')
if address in found_ips and address is not None: if address in found_ips and address is not None:
msg = ('Error! Duplicate IP Address Found: %s ' msg = ('Duplicate IP Address Found: %s ' % address)
'is in use by both %s and %s.' % self.report_error(
(address, found_ips[address], node_name)) msg, [node.doc_ref, found_ips[address].doc_ref],
message_list.append( "Select unique IP addresses for each node.")
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
elif address is not None: elif address is not None:
found_ips[address] = node_name found_ips[address] = node
if not message_list: return
msg = 'No Duplicate IP Addresses.'
message_list.append(
TaskStatusMessage(
msg=msg, error=False, ctx_type='NA', ctx='NA'))
return Validators.report_results(self, message_list)

View File

@ -13,32 +13,28 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class PlatformSelection(Validators): class PlatformSelection(Validators):
def __init__(self): def __init__(self):
super().__init__('Platform Selection', 1006) super().__init__('Platform Selection', 'DD3001')
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
"""Validate that the platform selection for all nodes is valid. """Validate that the platform selection for all nodes is valid.
Each node specifies an ``image`` and a ``kernel`` to use for Each node specifies an ``image`` and a ``kernel`` to use for
deployment. Check that these are valid for the image repository deployment. Check that these are valid for the image repository
configured in MAAS. configured in MAAS.
""" """
message_list = list()
try: try:
node_driver = orchestrator.enabled_drivers['node'] node_driver = orchestrator.enabled_drivers['node']
except KeyError: except KeyError:
message_list.append( msg = ("Platform Validation: No enabled node driver, image"
TaskStatusMessage( "and kernel selections not validated.")
msg="Platform Validation: No enabled node driver, image" self.report_warn(
"and kernel selections not validated.", msg, [],
error=False, "Cannot validate platform selection without accessing the node provisioner."
ctx_type='NA', )
ctx='NA')) return
return Validators.report_results(self, message_list)
valid_images = node_driver.get_available_images() valid_images = node_driver.get_available_images()
@ -51,28 +47,14 @@ class PlatformSelection(Validators):
if n.image in valid_images: if n.image in valid_images:
if n.kernel in valid_kernels[n.image]: if n.kernel in valid_kernels[n.image]:
continue continue
message_list.append( msg = "Platform Validation: invalid kernel %s" % (n.kernel)
TaskStatusMessage( self.report_error(msg, [n.doc_ref],
msg="Platform Validation: invalid kernel %s for node %s." "Select a valid kernel from: %s" % ",".join(
% (n.kernel, n.name), valid_kernels[n.image]))
error=True,
ctx_type='NA',
ctx='NA'))
continue continue
message_list.append( msg = "Platform Validation: invalid image %s" % (n.image)
TaskStatusMessage( self.report_error(
msg="Platform Validation: invalid image %s for node %s." % msg, [n.doc_ref],
(n.image, n.name), "Select a valid image from: %s" % ",".join(valid_images))
error=True,
ctx_type='NA',
ctx='NA'))
if not message_list:
message_list.append(
TaskStatusMessage(
msg="Platform Validation: all nodes have valid "
"image and kernel selections.",
error=False,
ctx_type='NA',
ctx='NA'))
return Validators.report_results(self, message_list) return

View File

@ -13,13 +13,12 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class RationalNetworkBond(Validators): class RationalNetworkBond(Validators):
def __init__(self): def __init__(self):
super().__init__('Rational Network Bond', 1007) super().__init__('Network Bond Rationality', 'DD1006')
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
This check ensures that each NetworkLink has a rational bonding setup. This check ensures that each NetworkLink has a rational bonding setup.
If the bonding mode is set to 'disabled' then it ensures that no other options are specified. If the bonding mode is set to 'disabled' then it ensures that no other options are specified.
@ -28,82 +27,58 @@ class RationalNetworkBond(Validators):
If the bonding mode is set to active-backup or balanced-rr then it ensures that the bonding hash and the If the bonding mode is set to active-backup or balanced-rr then it ensures that the bonding hash and the
bonding peer rate are both NOT defined. bonding peer rate are both NOT defined.
""" """
message_list = [] network_links = site_design.network_links or []
site_design = site_design.obj_to_simple()
network_links = site_design.get('network_links', [])
for network_link in network_links: for network_link in network_links:
bonding_mode = network_link.get('bonding_mode', []) bonding_mode = network_link.bonding_mode
if bonding_mode == 'disabled': if bonding_mode == 'disabled':
# check to make sure nothing else is specified # check to make sure nothing else is specified
if any([ if any([
network_link.get(x) for x in [ getattr(network_link, x) for x in [
'bonding_peer_rate', 'bonding_xmit_hash', 'bonding_peer_rate', 'bonding_xmit_hash',
'bonding_mon_rate', 'bonding_up_delay', 'bonding_mon_rate', 'bonding_up_delay',
'bonding_down_delay' 'bonding_down_delay'
] ]
]): ]):
msg = 'If bonding mode is disabled no other bond option can be specified'
msg = ( self.report_error(
'Network Link Bonding Error: If bonding mode is disabled no other bond option can be' msg, [network_link.doc_ref],
'specified; on BaremetalNode %s' % "Enable a bonding mode or remove the bond options.")
network_link.get('name'))
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
elif bonding_mode == '802.3ad': elif bonding_mode == '802.3ad':
# check if up_delay and down_delay are >= mon_rate # check if up_delay and down_delay are >= mon_rate
mon_rate = network_link.get('bonding_mon_rate') mon_rate = network_link.bonding_mon_rate
if network_link.get('bonding_up_delay') < mon_rate: if network_link.bonding_up_delay < mon_rate:
msg = ('Network Link Bonding Error: Up delay is less ' msg = ('Up delay %d is less than mon rate %d' %
'than mon rate on BaremetalNode %s' % (network_link.bonding_up_delay, mon_rate))
(network_link.get('name'))) self.report_error(msg, [
network_link.doc_ref
], "Link up delay must be equal or greater than the mon_rate"
)
message_list.append( if network_link.bonding_down_delay < mon_rate:
TaskStatusMessage( msg = ('Down delay %d is less than mon rate %d' %
msg=msg, error=True, ctx_type='NA', ctx='NA')) (network_link.bonding_down_delay, mon_rate))
self.report_error(msg, [
if network_link.get('bonding_down_delay') < mon_rate: network_link.doc_ref
msg = ('Network Link Bonding Error: Down delay is ' ], "Link down delay must be equal or greater than the mon_rate"
'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']: elif bonding_mode in ['active-backup', 'balanced-rr']:
# make sure hash and peer_rate are NOT defined # make sure hash and peer_rate are NOT defined
if network_link.get('bonding_xmit_hash'): if network_link.get('bonding_xmit_hash'):
msg = ( msg = ('Hash cannot be defined if bond mode is %s' %
'Network Link Bonding Error: Hash cannot be defined if bond mode is ' (bonding_mode))
'%s, on BaremetalNode %s' % (bonding_mode, self.report_error(
network_link.get('name'))) msg, [network_link.doc_ref],
"Hash mode is only applicable to LACP (802.3ad)")
message_list.append(
TaskStatusMessage(
msg=msg, error=True, ctx_type='NA', ctx='NA'))
if network_link.get('bonding_peer_rate'): if network_link.get('bonding_peer_rate'):
msg = ( msg = ('Peer rate cannot be defined if bond mode is %s' %
'Network Link Bonding Error: Peer rate cannot be defined if bond mode is ' (bonding_mode))
'%s, on BaremetalNode %s' % (bonding_mode, self.report_error(
network_link.get('name'))) msg, [network_link.doc_ref],
"Peer rate is only applicable to LACP (802.3ad)")
message_list.append( return
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 Validators.report_results(self, message_list)

View File

@ -13,90 +13,69 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class StoragePartitioning(Validators): class StoragePartitioning(Validators):
def __init__(self): def __init__(self):
super().__init__('Storage Partitioning', 1008) super().__init__('Storage Partitioning', "DD2002")
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
This checks that for each storage device a partition list OR volume group is defined. Also for each partition This checks that for each storage device a partition list OR volume group is defined. Also for each partition
list it ensures that a file system and partition volume group are not defined in the same partition. list it ensures that a file system and partition volume group are not defined in the same partition.
""" """
message_list = [] baremetal_nodes = site_design.baremetal_nodes or []
site_design = site_design.obj_to_simple()
baremetal_nodes = site_design.get('baremetal_nodes', [])
volume_group_check_list = [] volume_group_check_list = []
for baremetal_node in baremetal_nodes: for baremetal_node in baremetal_nodes:
storage_devices_list = baremetal_node.get('storage_devices', []) storage_devices_list = baremetal_node.storage_devices or []
for storage_device in storage_devices_list: for storage_device in storage_devices_list:
partitions_list = storage_device.get('partitions') partitions_list = storage_device.partitions or []
volume_group = storage_device.get('volume_group') volume_group = storage_device.volume_group
# error if both or neither is defined # error if both or neither is defined
if all([partitions_list, volume_group if all([partitions_list, volume_group
]) or not any([partitions_list, volume_group]): ]) or not any([partitions_list, volume_group]):
msg = ('Storage Partitioning Error: Either a volume group ' msg = (
'OR partitions must be defined for each storage ' 'Either a volume group OR partitions must be defined for each storage '
'device; on BaremetalNode ' 'device.')
'%s' % baremetal_node.get('name')) self.report_error(
message_list.append( msg, [baremetal_node.doc_ref],
TaskStatusMessage( "A storage device must be used for exactly one of a volume group "
msg=msg, error=True, ctx_type='NA', ctx='NA')) "physical volume or carved into partitions.")
# if there is a volume group add to list # if there is a volume group add to list
if volume_group is not None: if volume_group is not None:
volume_group_check_list.append(volume_group) volume_group_check_list.append(volume_group)
if partitions_list is not None: for partition in partitions_list:
for partition in partitions_list: partition_volume_group = partition.volume_group
partition_volume_group = partition.get('volume_group') fstype = partition.fstype
fstype = partition.get('fstype')
# error if both are defined # error if both are defined
if all([fstype, partition_volume_group]): if all([fstype, partition_volume_group]):
msg = ( msg = ('Both a volume group AND file system cannot be '
'Storage Partitioning Error: Both a volume group AND file system cannot be ' 'defined in a single partition')
'defined in a sigle partition; on BaremetalNode %s' self.report_error(
% baremetal_node.get('name')) msg, [baremetal_node.doc_ref],
"A partition can be used for only one of a volume group "
"physical volume or formatted as a filesystem.")
message_list.append( # if there is a volume group add to list
TaskStatusMessage( if partition_volume_group is not None:
msg=msg, volume_group_check_list.append(volume_group)
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 # checks all volume groups are assigned to a partition or storage device
# if one exist that wasn't found earlier it is unassigned # if one exist that wasn't found earlier it is unassigned
all_volume_groups = baremetal_node.get('volume_groups', []) all_volume_groups = baremetal_node.volume_groups or []
for volume_group in all_volume_groups: for volume_group in all_volume_groups:
if volume_group.get('name') not in volume_group_check_list: if volume_group.name not in volume_group_check_list:
msg = ( msg = (
'Storage Partitioning Error: A volume group must be assigned to a storage device or ' 'Volume group %s not assigned any physical volumes' %
'partition; volume group %s on BaremetalNode %s' % (volume_group.name))
(volume_group.get('name'), baremetal_node.get('name'))) self.report_error(msg, [
baremetal_node.doc_ref
], "Each volume group should be assigned at least one storage device "
"or partition as a physical volume.")
message_list.append( return
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 Validators.report_results(self, message_list)

View File

@ -13,91 +13,70 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class StorageSizing(Validators): class StorageSizing(Validators):
def __init__(self): def __init__(self):
super().__init__('Storage Sizing', 1009) super().__init__('Storage Sizing', 'DD2003')
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
Ensures that for a partitioned physical device or logical volumes Ensures that for a partitioned physical device or logical volumes
in a volume group, if sizing is a percentage then those percentages in a volume group, if sizing is a percentage then those percentages
do not sum > 99% and have no negative values do not sum > 99% and have no negative values
""" """
message_list = [] baremetal_nodes = site_design.baremetal_nodes or []
site_design = site_design.obj_to_simple()
baremetal_nodes = site_design.get('baremetal_nodes', [])
for baremetal_node in baremetal_nodes: for baremetal_node in baremetal_nodes:
storage_device_list = baremetal_node.get('storage_devices', []) storage_device_list = baremetal_node.storage_devices or []
for storage_device in storage_device_list: for storage_device in storage_device_list:
partition_list = storage_device.get('partitions', []) partition_list = storage_device.partitions or []
partition_sum = 0 partition_sum = 0
for partition in partition_list: for partition in partition_list:
size = partition.get('size') size = partition.size
percent = size.split('%') percent = size.split('%')
if len(percent) == 2: if len(percent) == 2:
if int(percent[0]) < 0: if int(percent[0]) < 0:
msg = ( msg = (
'Storage Sizing Error: Storage partition size is < 0 ' 'Storage partition %s on device %s size is < 0'
'on Baremetal Node %s' % % (partition.name, storage_device.name))
baremetal_node.get('name')) self.report_error(
message_list.append( msg, [baremetal_node.doc_ref],
TaskStatusMessage( "Partition size must be a positive number.")
msg=msg,
error=True,
ctx_type='NA',
ctx='NA'))
partition_sum += int(percent[0]) partition_sum += int(percent[0])
if partition_sum > 99: if partition_sum > 99:
msg = ( msg = (
'Storage Sizing Error: Storage partition size is greater than ' 'Cumulative partition sizes on device %s is greater than 99%%.'
'99 on Baremetal Node %s' % % (storage_device.name))
baremetal_node.get('name')) self.report_error(msg, [
message_list.append( baremetal_node.doc_ref
TaskStatusMessage( ], "Percentage-based sizes must sum to less than 100%."
msg=msg, error=True, ctx_type='NA', ctx='NA')) )
volume_groups = baremetal_node.get('volume_groups', []) volume_groups = baremetal_node.volume_groups or []
volume_sum = 0 volume_sum = 0
for volume_group in volume_groups: for volume_group in volume_groups:
logical_volume_list = volume_group.get( logical_volume_list = volume_group.logical_volumes or []
'logical_volumes', [])
for logical_volume in logical_volume_list: for logical_volume in logical_volume_list:
size = logical_volume.get('size') size = logical_volume.size
percent = size.split('%') percent = size.split('%')
if len(percent) == 2: if len(percent) == 2:
if int(percent[0]) < 0: if int(percent[0]) < 0:
msg = ( msg = ('Logical Volume %s size is < 0 ' %
'Storage Sizing Error: Storage volume size is < 0 ' (logical_volume.name))
'on Baremetal Node %s' % self.report_error(msg,
baremetal_node.get('name')) [baremetal_node.doc_ref], "")
message_list.append(
TaskStatusMessage(
msg=msg,
error=True,
ctx_type='NA',
ctx='NA'))
volume_sum += int(percent[0]) volume_sum += int(percent[0])
if volume_sum > 99: if volume_sum > 99:
msg = ( msg = ('Cumulative logical volume size is greater '
'Storage Sizing Error: Storage volume size is greater ' 'than 99% in volume group %s' %
'than 99 on Baremetal Node %s.' % (volume_group.name))
baremetal_node.get('name')) self.report_error(msg, [
message_list.append( baremetal_node.doc_ref
TaskStatusMessage( ], "Percentage-based sizes must sum to less than 100%."
msg=msg, error=True, ctx_type='NA', ctx='NA')) )
if not message_list: return
message_list.append(
TaskStatusMessage(
msg='Storage Sizing', error=False, ctx_type='NA',
ctx='NA'))
return Validators.report_results(self, message_list)

View File

@ -13,39 +13,37 @@
# limitations under the License. # limitations under the License.
from drydock_provisioner.orchestrator.validations.validators import Validators from drydock_provisioner.orchestrator.validations.validators import Validators
from drydock_provisioner.objects.task import TaskStatusMessage
class UniqueNetworkCheck(Validators): class UniqueNetworkCheck(Validators):
def __init__(self): def __init__(self):
super().__init__('Unique Network Check', 1010) super().__init__('Allowed Network Check', 'DD1007')
def execute(self, site_design, orchestrator=None): def run_validation(self, site_design, orchestrator=None):
""" """
Ensures that each network name appears at most once between all NetworkLink Ensures that each network name appears at most once between all NetworkLink
allowed networks allowed networks
""" """
message_list = [] network_link_list = site_design.network_links or []
site_design = site_design.obj_to_simple() link_allowed_nets = {}
network_link_list = site_design.get('network_links', [])
compare = {}
for network_link in network_link_list: for network_link in network_link_list:
allowed_network_list = network_link.get('allowed_networks', []) allowed_network_list = network_link.allowed_networks
compare[network_link.get('name')] = allowed_network_list link_allowed_nets[network_link.name] = allowed_network_list
# This checks the allowed networks for each network link against # This checks the allowed networks for each network link against
# the other allowed networks # the other allowed networks
checked_pairs = [] checked_pairs = []
for network_link_name in compare: for network_link_name in link_allowed_nets:
allowed_network_list_1 = compare[network_link_name] allowed_network_list_1 = link_allowed_nets[network_link_name]
for network_link_name_2 in compare: for network_link_name_2 in link_allowed_nets:
if (network_link_name is not network_link_name_2 if (network_link_name is not network_link_name_2
and sorted([network_link_name, network_link_name_2 and sorted([network_link_name, network_link_name_2
]) not in checked_pairs): ]) not in checked_pairs):
checked_pairs.append( checked_pairs.append(
sorted([network_link_name, network_link_name_2])) sorted([network_link_name, network_link_name_2]))
allowed_network_list_2 = compare[network_link_name_2] allowed_network_list_2 = link_allowed_nets[
network_link_name_2]
# creates a list of duplicated allowed networks # creates a list of duplicated allowed networks
duplicated_names = [ duplicated_names = [
i for i in allowed_network_list_1 i for i in allowed_network_list_1
@ -54,17 +52,36 @@ class UniqueNetworkCheck(Validators):
for name in duplicated_names: for name in duplicated_names:
msg = ( msg = (
'Unique Network Error: Allowed network %s duplicated on NetworkLink %s and NetworkLink ' 'Allowed network %s duplicated on NetworkLink %s and NetworkLink '
'%s' % (name, network_link_name, '%s' % (name, network_link_name,
network_link_name_2)) network_link_name_2))
message_list.append( self.report_error(
TaskStatusMessage( msg, [],
msg=msg, error=True, ctx_type='NA', ctx='NA')) "Each network is only allowed to cross a single network link."
)
if not message_list: node_list = site_design.baremetal_nodes or []
message_list.append(
TaskStatusMessage(
msg='Unique Network', error=False, ctx_type='NA',
ctx='NA'))
return Validators.report_results(self, message_list) for n in node_list:
node_interfaces = n.interfaces or []
for i in node_interfaces:
nic_link = i.network_link
for nw in i.networks:
try:
if nw not in link_allowed_nets[nic_link]:
msg = (
"Interface %s attached to network %s not allowed on interface link"
% (i.get_name(), nw))
self.report_error(msg, [
n.doc_ref
], "Interfaces can only be attached to networks allowed on the network link "
"connected to the interface.")
except KeyError:
msg = (
"Interface %s connected to undefined network link %s."
% (i.get_name(), nic_link))
self.report_error(msg, [
n.doc_ref
], "Define the network link attached to this interface."
)
return

View File

@ -15,7 +15,7 @@
import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.fields as hd_fields
from drydock_provisioner.objects.task import TaskStatus from drydock_provisioner.objects.validation import Validation
from drydock_provisioner.orchestrator.validations.boot_storage_rational import BootStorageRational from drydock_provisioner.orchestrator.validations.boot_storage_rational import BootStorageRational
from drydock_provisioner.orchestrator.validations.ip_locality_check import IpLocalityCheck from drydock_provisioner.orchestrator.validations.ip_locality_check import IpLocalityCheck
@ -28,6 +28,7 @@ from drydock_provisioner.orchestrator.validations.storage_partititioning import
from drydock_provisioner.orchestrator.validations.storage_sizing import StorageSizing from drydock_provisioner.orchestrator.validations.storage_sizing import StorageSizing
from drydock_provisioner.orchestrator.validations.unique_network_check import UniqueNetworkCheck from drydock_provisioner.orchestrator.validations.unique_network_check import UniqueNetworkCheck
class Validator(): class Validator():
def __init__(self, orchestrator): def __init__(self, orchestrator):
"""Create a validator with a reference to the orchestrator. """Create a validator with a reference to the orchestrator.
@ -36,7 +37,10 @@ class Validator():
""" """
self.orchestrator = orchestrator self.orchestrator = orchestrator
def validate_design(self, site_design, result_status=None, include_output=False): def validate_design(self,
site_design,
result_status=None,
include_output=False):
"""Validate the design in site_design passes all validation rules. """Validate the design in site_design passes all validation rules.
Apply all validation rules to the design in site_design. If result_status is Apply all validation rules to the design in site_design. If result_status is
@ -47,16 +51,14 @@ class Validator():
:param result_status: instance of objects.TaskStatus :param result_status: instance of objects.TaskStatus
""" """
if result_status is None: if result_status is None:
result_status = TaskStatus() result_status = Validation()
validation_error = False validation_error = False
message_lists = []
for rule in rule_set: for rule in rule_set:
results, message_list = rule.execute(site_design=site_design, orchestrator=self.orchestrator) message_list = rule.execute(
for item in message_list: site_design=site_design, orchestrator=self.orchestrator)
message_lists.append(item) result_status.message_list.extend(message_list)
result_status.message_list.extend(results) error_msg = [m for m in message_list if m.error]
error_msg = [m for m in results if m.error]
result_status.error_count = result_status.error_count + len( result_status.error_count = result_status.error_count + len(
error_msg) error_msg)
if len(error_msg) > 0: if len(error_msg) > 0:
@ -64,29 +66,12 @@ class Validator():
if validation_error: if validation_error:
result_status.set_status(hd_fields.ValidationResult.Failure) result_status.set_status(hd_fields.ValidationResult.Failure)
result_status.message = "Site design failed validation."
result_status.reason = "See detail messages."
else: else:
result_status.set_status(hd_fields.ValidationResult.Success) result_status.set_status(hd_fields.ValidationResult.Success)
result_status.message = "Site design passed validation"
if include_output:
output = {
"kind": "Status",
"api_version": "v1.0",
"metadata": {},
"status": "Success",
"message": "Drydock validations succeeded",
"reason": "Validation",
"details": {
"error_count": 0,
"message_list": []
},
"code": 200
}
if len(message_lists) > 0:
output['status'] = "Failure"
output['details']['error_count'] = len(message_lists)
output['details']['message_list'] = message_lists
output['code'] = 400
return output
return result_status return result_status

View File

@ -13,28 +13,58 @@
# limitations under the License. # limitations under the License.
"""Business Logic Validation""" """Business Logic Validation"""
from drydock_provisioner import objects
from drydock_provisioner.objects import fields as hd_fields
class Validators: class Validators:
def __init__(self, name, code): def __init__(self, long_name, name):
self.name = name self.name = name
self.code = code self.long_name = long_name
self.reset_message_list()
def report_results(self, results): def report_msg(self, msg, docs, diagnostic, error, level):
# https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure """Add a validation message to the result list.
message_list = []
for result in results:
rd = result.to_dict()
if isinstance(rd, dict) and rd['error']:
item = {
"message": rd['message'],
"error": True,
"name": self.name,
"documents": [],
"level": "Error",
"diagnostic": "Context Type = %s, Context = %s" % (rd['context_type'], rd['context']),
"kind": "ValidationMessage"
}
message_list.append(item)
return results, message_list
def execute(site_design, orchestrator=None): :param msg: String - msg, will be prepended with validator long name
pass :param docs: List - List of document references related to this validation message
:param diagnostic: String - Diagnostic information for t/s this error
:param error: Bool - Whether this message indicates an error
:param level: String - More detailed of the severity level of this message
"""
fmt_msg = "%s: %s" % (self.long_name, msg)
msg_obj = objects.ValidationMessage(
fmt_msg,
self.name,
error=error,
level=level,
docs=docs,
diagnostic=diagnostic)
self.messages.append(msg_obj)
def report_error(self, msg, docs, diagnostic):
self.report_msg(msg, docs, diagnostic, True,
hd_fields.MessageLevels.ERROR)
def report_warn(self, msg, docs, diagnostic):
self.report_msg(msg, docs, diagnostic, False,
hd_fields.MessageLevels.WARN)
def report_info(self, msg, docs, diagnostic):
self.report_msg(msg, docs, diagnostic, False,
hd_fields.MessageLevels.INFO)
def error_count(self):
errors = [x for x in self.messages if x.error]
return len(errors)
def reset_message_list(self):
self.messages = []
def execute(self, site_design, orchestrator=None):
self.reset_message_list()
self.run_validation(site_design, orchestrator=orchestrator)
if self.error_count() == 0:
self.report_info("Validation successful.", [], "")
return self.messages

View File

@ -126,13 +126,12 @@ class DrydockPolicy(object):
'path': '/api/v1.0/designs/{design_id}/parts', 'path': '/api/v1.0/designs/{design_id}/parts',
'method': 'POST' 'method': 'POST'
}]), }]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault('physical_provisioner:health_data',
'physical_provisioner:health_data', 'role:admin', 'role:admin', 'et health status',
'et health status', [{
[{ 'path': '/api/v1.0/health/extended',
'path': '/api/v1.0/health/extended', 'method': 'GET'
'method': 'GET' }])
}])
] ]
# Validate Design Policy # Validate Design Policy

View File

@ -684,7 +684,5 @@ class DrydockState(object):
return None return None
except Exception as ex: except Exception as ex:
self.logger.error(str(ex)) self.logger.error(str(ex))
self.logger.error( self.logger.error("Error querying for now()", exc_info=True)
"Error querying for now()",
exc_info=True)
return None return None

View File

@ -19,7 +19,8 @@ import falcon
def test_get_health(mocker, deckhand_orchestrator, drydock_state): def test_get_health(mocker, deckhand_orchestrator, drydock_state):
api = HealthResource(state_manager=drydock_state, orchestrator=deckhand_orchestrator) api = HealthResource(
state_manager=drydock_state, orchestrator=deckhand_orchestrator)
# Configure mocked request and response # Configure mocked request and response
req = mocker.MagicMock(spec=falcon.Request) req = mocker.MagicMock(spec=falcon.Request)

View File

@ -20,13 +20,13 @@ import drydock_provisioner.objects.fields as hd_fields
class TestClass(object): class TestClass(object):
def test_task_complete(self, yaml_ingester, input_files, setup, def test_task_complete(self, deckhand_ingester, input_files, setup,
blank_state, mock_get_build_data): blank_state, mock_get_build_data):
input_file = input_files.join("fullsite.yaml") input_file = input_files.join("deckhand_fullsite.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
orchestrator = orch.Orchestrator( orchestrator = orch.Orchestrator(
state_manager=blank_state, ingester=yaml_ingester) state_manager=blank_state, ingester=deckhand_ingester)
orch_task = orchestrator.create_task( orch_task = orchestrator.create_task(
action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref)
orch_task.set_status(hd_fields.TaskStatus.Queued) orch_task.set_status(hd_fields.TaskStatus.Queued)
@ -45,13 +45,13 @@ class TestClass(object):
orchestrator.stop_orchestrator() orchestrator.stop_orchestrator()
orch_thread.join(10) orch_thread.join(10)
def test_task_termination(self, input_files, yaml_ingester, setup, def test_task_termination(self, input_files, deckhand_ingester, setup,
blank_state): blank_state):
input_file = input_files.join("fullsite.yaml") input_file = input_files.join("deckhand_fullsite.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
orchestrator = orch.Orchestrator( orchestrator = orch.Orchestrator(
state_manager=blank_state, ingester=yaml_ingester) state_manager=blank_state, ingester=deckhand_ingester)
orch_task = orchestrator.create_task( orch_task = orchestrator.create_task(
action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref)

View File

@ -16,12 +16,15 @@ from falcon import testing
import pytest import pytest
import json import json
import logging
from drydock_provisioner import policy from drydock_provisioner import policy
from drydock_provisioner.control.api import start_api from drydock_provisioner.control.api import start_api
import falcon import falcon
LOG = logging.getLogger(__name__)
class TestValidationApi(object): class TestValidationApi(object):
def test_post_validation_resp(self, input_files, falcontest, drydock_state, def test_post_validation_resp(self, input_files, falcontest, drydock_state,
@ -46,6 +49,7 @@ class TestValidationApi(object):
result = falcontest.simulate_post( result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body)) url, headers=hdr, body=json.dumps(body))
LOG.debug(result.text)
assert result.status == falcon.HTTP_200 assert result.status == falcon.HTTP_200
def test_href_error(self, input_files, falcontest): def test_href_error(self, input_files, falcontest):
@ -65,6 +69,7 @@ class TestValidationApi(object):
result = falcontest.simulate_post( result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body)) url, headers=hdr, body=json.dumps(body))
LOG.debug(result.text)
assert result.status == falcon.HTTP_400 assert result.status == falcon.HTTP_400
def test_json_data_error(self, input_files, falcontest): def test_json_data_error(self, input_files, falcontest):
@ -80,6 +85,7 @@ class TestValidationApi(object):
result = falcontest.simulate_post( result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body)) url, headers=hdr, body=json.dumps(body))
LOG.debug(result.text)
assert result.status == falcon.HTTP_400 assert result.status == falcon.HTTP_400
def test_invalid_post_resp(self, input_files, falcontest, drydock_state, def test_invalid_post_resp(self, input_files, falcontest, drydock_state,

View File

@ -31,7 +31,8 @@ class TestClass(object):
assert len(design_data.host_profiles) == 2 assert len(design_data.host_profiles) == 2
assert len(design_data.baremetal_nodes) == 2 assert len(design_data.baremetal_nodes) == 2
def test_ingest_deckhand_docref_exists(self, input_files, setup, deckhand_ingester): def test_ingest_deckhand_docref_exists(self, input_files, setup,
deckhand_ingester):
"""Test that each processed document has a doc_ref.""" """Test that each processed document has a doc_ref."""
input_file = input_files.join('deckhand_fullsite.yaml') input_file = input_files.join('deckhand_fullsite.yaml')

View File

@ -33,7 +33,4 @@ class TestDesignValidator(object):
val = Validator(orch) val = Validator(orch)
response = val.validate_design(site_design) response = val.validate_design(site_design)
for msg in response.message_list:
assert msg.error is False
assert response.error_count == 0
assert response.status == hd_fields.ValidationResult.Success assert response.status == hd_fields.ValidationResult.Success

View File

@ -14,15 +14,17 @@
"""Test Validation Rule Rational Boot Storage""" """Test Validation Rule Rational Boot Storage"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.boot_storage_rational import BootStorageRational from drydock_provisioner.orchestrator.validations.boot_storage_rational import BootStorageRational
LOG = logging.getLogger(__name__)
class TestRationalBootStorage(object): class TestRationalBootStorage(object):
def test_boot_storage_rational(self, deckhand_ingester, drydock_state, def test_boot_storage_rational(self, deckhand_ingester, drydock_state,
input_files, mock_get_build_data): input_files, mock_get_build_data):
input_file = input_files.join("validation.yaml") input_file = input_files.join("validation.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
@ -32,16 +34,14 @@ class TestRationalBootStorage(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = BootStorageRational() validator = BootStorageRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'Boot Storage'
assert msg.get('error') is False assert msg.get('error') is False
assert len(results) == 1 assert len(message_list) == 1
def test_invalid_boot_storage_small(self, deckhand_ingester, drydock_state, def test_invalid_boot_storage_small(self, deckhand_ingester, drydock_state,
input_files, mock_get_build_data): input_files, mock_get_build_data):
input_file = input_files.join("invalid_boot_storage_small.yaml") input_file = input_files.join("invalid_boot_storage_small.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
@ -51,21 +51,21 @@ class TestRationalBootStorage(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = BootStorageRational() validator = BootStorageRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile('.+ volume must be > .+GB')
'Boot Storage Error: .+ volume must be > .+GB on BaremetalNode .+')
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
assert regex.match(msg.get('message')) is not None LOG.debug(msg)
assert len(msg.get('documents')) > 0
assert regex.search(msg.get('message')) is not None
assert msg.get('error') assert msg.get('error')
assert len(results) == 4 assert len(message_list) == 4
def test_invalid_boot_storage_root_not_set(self, deckhand_ingester, def test_invalid_boot_storage_root_not_set(self, deckhand_ingester,
drydock_state, input_files): drydock_state, input_files):
input_file = input_files.join("invalid_validation.yaml") input_file = input_files.join("invalid_validation.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
@ -75,15 +75,15 @@ class TestRationalBootStorage(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = BootStorageRational() validator = BootStorageRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design)
regex = re.compile( regex = re.compile('Root volume has to be set and must be > 20GB')
'Boot Storage Error: Root volume has to be set and must be > 20GB on BaremetalNode .+'
)
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
assert regex.match(msg.get('message')) is not None LOG.debug(msg)
assert len(msg.get('documents')) > 0
assert regex.search(msg.get('message')) is not None
assert msg.get('error') assert msg.get('error')
assert len(results) == 2 assert len(message_list) == 2

View File

@ -13,10 +13,13 @@
# limitations under the License. # limitations under the License.
import re import re
import logging
from drydock_provisioner.orchestrator.validations.ip_locality_check import IpLocalityCheck from drydock_provisioner.orchestrator.validations.ip_locality_check import IpLocalityCheck
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
LOG = logging.getLogger(__name__)
class TestIPLocality(object): class TestIPLocality(object):
def test_ip_locality(self, input_files, drydock_state, deckhand_ingester): def test_ip_locality(self, input_files, drydock_state, deckhand_ingester):
@ -29,10 +32,9 @@ class TestIPLocality(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = IpLocalityCheck() validator = IpLocalityCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'IP Locality Success'
assert msg.get('error') is False assert msg.get('error') is False
def test_ip_locality_no_networks(self, input_files, drydock_state, def test_ip_locality_no_networks(self, input_files, drydock_state,
@ -46,10 +48,10 @@ class TestIPLocality(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = IpLocalityCheck() validator = IpLocalityCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'No networks found.' assert 'No networks found' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
def test_ip_locality_no_gateway(self, input_files, drydock_state, def test_ip_locality_no_gateway(self, input_files, drydock_state,
@ -63,8 +65,8 @@ class TestIPLocality(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = IpLocalityCheck() validator = IpLocalityCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert 'No gateway found' in msg.get('message') assert 'No gateway found' in msg.get('message')
assert msg.get('error') is True assert msg.get('error') is True
@ -80,10 +82,10 @@ class TestIPLocality(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = IpLocalityCheck() validator = IpLocalityCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'No baremetal_nodes found.' assert 'No baremetal_nodes found' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
def test_invalid_ip_locality_invalid_network( def test_invalid_ip_locality_invalid_network(
@ -97,20 +99,24 @@ class TestIPLocality(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = IpLocalityCheck() validator = IpLocalityCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile(
'IP Locality Error: The gateway IP Address .+ is not within the defined CIDR: .+ of .+' 'The gateway IP Address .+ is not within the defined CIDR: .+ of .+'
) )
regex_1 = re.compile('IP Locality Error: .+ is not a valid network.') regex_1 = re.compile('.+ is not a valid network.')
regex_2 = re.compile( regex_2 = re.compile(
'IP Locality Error: The IP Address .+ is not within the defined CIDR: .+ of .+ .' 'The IP Address .+ is not within the defined CIDR: .+ of .+ .')
)
assert len(results) == 3 assert len(message_list) == 3
for msg in results:
for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
LOG.debug(msg)
assert len(msg.get('documents')) > 0
assert msg.get('error') assert msg.get('error')
assert (regex.match(msg.get('message')) is not None assert any([
or regex_1.match(msg.get('message')) is not None regex.search(msg.get('message')),
or regex_2.match(msg.get('message')) is not None) regex_1.search(msg.get('message')),
regex_2.search(msg.get('message'))
])

View File

@ -14,14 +14,16 @@
"""Test Validation Rule Unique Network""" """Test Validation Rule Unique Network"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.mtu_rational import MtuRational from drydock_provisioner.orchestrator.validations.mtu_rational import MtuRational
LOG = logging.getLogger(__name__)
class TestMtu(object): class TestMtu(object):
def test_mtu(self, mocker, deckhand_ingester, drydock_state, input_files): def test_mtu(self, mocker, deckhand_ingester, drydock_state, input_files):
input_file = input_files.join("validation.yaml") input_file = input_files.join("validation.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
@ -31,12 +33,12 @@ class TestMtu(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = MtuRational() validator = MtuRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'Mtu' assert 'MTU' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
assert len(results) == 1 assert len(message_list) == 1
def test_invalid_mtu(self, mocker, deckhand_ingester, drydock_state, def test_invalid_mtu(self, mocker, deckhand_ingester, drydock_state,
input_files): input_files):
@ -50,19 +52,19 @@ class TestMtu(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = MtuRational() validator = MtuRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile('MTU must be between \d+ and \d+')
'Mtu Error: Mtu must be between 1400 and 64000; on Network .+') regex_1 = re.compile('MTU must be <= the parent Network Link')
regex_1 = re.compile(
'Mtu Error: Mtu must be <= the parent Network Link; for Network .+'
)
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
LOG.debug(msg)
assert msg.get('error') assert msg.get('error')
assert regex.match( assert len(msg.get('documents')) > 0
msg.get('message')) is not None or regex_1.match( assert any([
msg.get('message')) is not None regex.search(msg.get('message')),
regex_1.search(msg.get('message'))
])
assert len(results) == 4 assert len(message_list) == 4

View File

@ -14,10 +14,13 @@
"""Test Validation Rule Rational Network Bond""" """Test Validation Rule Rational Network Bond"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.rational_network_bond import RationalNetworkBond from drydock_provisioner.orchestrator.validations.rational_network_bond import RationalNetworkBond
LOG = logging.getLogger(__name__)
class TestRationalNetworkLinkBond(object): class TestRationalNetworkLinkBond(object):
def test_rational_network_bond(self, mocker, deckhand_ingester, def test_rational_network_bond(self, mocker, deckhand_ingester,
@ -31,12 +34,12 @@ class TestRationalNetworkLinkBond(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = RationalNetworkBond() validator = RationalNetworkBond()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'Network Link Bonding'
assert msg.get('error') is False assert msg.get('error') is False
assert len(results) == 1 assert len(message_list) == 1
assert len(msg.get('documents')) == 0
def test_invalid_rational_network_bond(self, mocker, deckhand_ingester, def test_invalid_rational_network_bond(self, mocker, deckhand_ingester,
drydock_state, input_files): drydock_state, input_files):
@ -50,20 +53,19 @@ class TestRationalNetworkLinkBond(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = RationalNetworkBond() validator = RationalNetworkBond()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile('Down delay \S+ is less than mon rate \S+')
'Network Link Bonding Error: Down delay is less than mon rate on BaremetalNode .+' regex_1 = re.compile('Up delay \S+ is less than mon rate \S+')
)
regex_1 = re.compile(
'Network Link Bonding Error: Up delay is less than mon rate on BaremetalNode .+'
)
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
assert msg.get('error') is True LOG.debug(msg)
assert regex.match( assert msg.get('error')
msg.get('message')) is not None or regex_1.match( assert len(msg.get('documents')) > 0
msg.get('message')) is not None assert any([
regex.search(msg.get('message')),
regex_1.search(msg.get('message'))
])
assert len(results) == 2 assert len(message_list) == 2

View File

@ -14,10 +14,13 @@
"""Test Validation Rule Rational Network Trunking""" """Test Validation Rule Rational Network Trunking"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.network_trunking_rational import NetworkTrunkingRational from drydock_provisioner.orchestrator.validations.network_trunking_rational import NetworkTrunkingRational
LOG = logging.getLogger(__name__)
class TestRationalNetworkTrunking(object): class TestRationalNetworkTrunking(object):
def test_rational_network_trunking(self, deckhand_ingester, drydock_state, def test_rational_network_trunking(self, deckhand_ingester, drydock_state,
@ -31,10 +34,9 @@ class TestRationalNetworkTrunking(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NetworkTrunkingRational() validator = NetworkTrunkingRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'Rational Network Trunking'
assert msg.get('error') is False assert msg.get('error') is False
def test_invalid_rational_network_trunking(self, deckhand_ingester, def test_invalid_rational_network_trunking(self, deckhand_ingester,
@ -48,21 +50,27 @@ class TestRationalNetworkTrunking(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NetworkTrunkingRational() validator = NetworkTrunkingRational()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile(
'Rational Network Trunking Error: Trunking mode is disabled, a trunking' 'Trunking mode is disabled, a trunking default_network must be defined'
'default_network must be defined; on NetworkLink .+') )
regex_1 = re.compile( regex_1 = re.compile(
'Rational Network Trunking Error: If there is more than 1 allowed network,' 'If there is more than 1 allowed network,trunking mode must be enabled'
'trunking mode must be enabled; on NetworkLink .+') )
for msg in results: regex_2 = re.compile('native network has a defined VLAN tag')
for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
LOG.debug(msg)
assert msg.get('error') assert msg.get('error')
assert regex.match( assert len(msg.get('documents')) > 0
msg.get('message')) is not None or regex_1.match( assert any([
msg.get('message')) is not None regex.search(msg.get('message')),
regex_1.search(msg.get('message')),
regex_2.search(msg.get('message'))
])
assert len(results) == 2 assert len(message_list) == 3

View File

@ -13,10 +13,13 @@
# limitations under the License. # limitations under the License.
import re import re
import logging
from drydock_provisioner.orchestrator.validations.no_duplicate_ips_check import NoDuplicateIpsCheck from drydock_provisioner.orchestrator.validations.no_duplicate_ips_check import NoDuplicateIpsCheck
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
LOG = logging.getLogger(__name__)
class TestDuplicateIPs(object): class TestDuplicateIPs(object):
def test_no_duplicate_IPs(self, input_files, drydock_state, def test_no_duplicate_IPs(self, input_files, drydock_state,
@ -30,10 +33,9 @@ class TestDuplicateIPs(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NoDuplicateIpsCheck() validator = NoDuplicateIpsCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'No Duplicate IP Addresses.'
assert msg.get('error') is False assert msg.get('error') is False
def test_no_duplicate_IPs_no_baremetal_node( def test_no_duplicate_IPs_no_baremetal_node(
@ -47,10 +49,10 @@ class TestDuplicateIPs(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NoDuplicateIpsCheck() validator = NoDuplicateIpsCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'No BaremetalNodes Found.' assert 'No BaremetalNodes Found.' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
def test_no_duplicate_IPs_no_addressing(self, input_files, drydock_state, def test_no_duplicate_IPs_no_addressing(self, input_files, drydock_state,
@ -64,10 +66,10 @@ class TestDuplicateIPs(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NoDuplicateIpsCheck() validator = NoDuplicateIpsCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'No BaremetalNodes Found.' assert 'No BaremetalNodes Found.' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
def test_invalid_no_duplicate_IPs(self, input_files, drydock_state, def test_invalid_no_duplicate_IPs(self, input_files, drydock_state,
@ -81,12 +83,13 @@ class TestDuplicateIPs(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = NoDuplicateIpsCheck() validator = NoDuplicateIpsCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design)
regex = re.compile( regex = re.compile('Duplicate IP Address Found: [0-9.]+')
'Error! Duplicate IP Address Found: .+ is in use by both .+ and .+.'
) for msg in message_list:
for msg in results:
msg = msg.to_dict() msg = msg.to_dict()
LOG.debug(msg)
assert len(msg.get('documents')) > 0
assert msg.get('error') is True assert msg.get('error') is True
assert regex.match(msg.get('message')) is not None assert regex.search(msg.get('message')) is not None

View File

@ -14,10 +14,13 @@
"""Test Validation Rule Storage Partitioning""" """Test Validation Rule Storage Partitioning"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.storage_partititioning import StoragePartitioning from drydock_provisioner.orchestrator.validations.storage_partititioning import StoragePartitioning
LOG = logging.getLogger(__name__)
class TestRationalNetworkTrunking(object): class TestRationalNetworkTrunking(object):
def test_storage_partitioning(self, deckhand_ingester, drydock_state, def test_storage_partitioning(self, deckhand_ingester, drydock_state,
@ -31,11 +34,10 @@ class TestRationalNetworkTrunking(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = StoragePartitioning() validator = StoragePartitioning()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert len(results) == 1 assert len(message_list) == 1
assert msg.get('message') == 'Storage Partitioning'
assert msg.get('error') is False assert msg.get('error') is False
def test_storage_partitioning_unassigned_partition( def test_storage_partitioning_unassigned_partition(
@ -50,11 +52,10 @@ class TestRationalNetworkTrunking(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = StoragePartitioning() validator = StoragePartitioning()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert len(results) == 1 assert len(message_list) == 1
assert msg.get('message') == 'Storage Partitioning'
assert msg.get('error') is False assert msg.get('error') is False
def test_invalid_storage_partitioning(self, deckhand_ingester, def test_invalid_storage_partitioning(self, deckhand_ingester,
@ -70,15 +71,15 @@ class TestRationalNetworkTrunking(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = StoragePartitioning() validator = StoragePartitioning()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design)
regex = re.compile( regex = re.compile('Volume group .+ not assigned any physical volumes')
'Storage Partitioning Error: A volume group must be assigned to a storage device or '
'partition; volume group .+ on BaremetalNode .+')
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
LOG.debug(msg)
assert len(msg.get('documents')) > 0
assert msg.get('error') assert msg.get('error')
assert regex.match(msg.get('message')) is not None assert regex.search(msg.get('message')) is not None
assert len(results) == 2 assert len(message_list) == 2

View File

@ -14,10 +14,13 @@
"""Test Validation Rule Rational Network Trunking""" """Test Validation Rule Rational Network Trunking"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.storage_sizing import StorageSizing from drydock_provisioner.orchestrator.validations.storage_sizing import StorageSizing
LOG = logging.getLogger(__name__)
class TestStorageSizing(object): class TestStorageSizing(object):
def test_storage_sizing(self, deckhand_ingester, drydock_state, def test_storage_sizing(self, deckhand_ingester, drydock_state,
@ -32,11 +35,10 @@ class TestStorageSizing(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = StorageSizing() validator = StorageSizing()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert len(results) == 1 assert len(message_list) == 1
assert msg.get('message') == 'Storage Sizing'
assert msg.get('error') is False assert msg.get('error') is False
def test_invalid_storage_sizing(self, deckhand_ingester, drydock_state, def test_invalid_storage_sizing(self, deckhand_ingester, drydock_state,
@ -51,19 +53,17 @@ class TestStorageSizing(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = StorageSizing() validator = StorageSizing()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile(
'Storage Sizing Error: Storage .+ size is < 0 on Baremetal Node .+' '(Storage partition)|(Logical Volume) .+ size is < 0')
) regex_1 = re.compile('greater than 99%')
regex_1 = re.compile(
'Storage Sizing Error: Storage .+ size is greater than 99 on Baremetal Node .+'
)
assert len(results) == 6 assert len(message_list) == 6
for msg in results: for msg in message_list:
msg = msg.to_dict() msg = msg.to_dict()
assert regex.match( LOG.debug(msg)
msg.get('message')) is not None or regex_1.match( assert regex.search(
msg.get('message')) is not None or regex_1.search(
msg.get('message')) is not None msg.get('message')) is not None
assert msg.get('error') is True assert msg.get('error') is True

View File

@ -14,10 +14,13 @@
"""Test Validation Rule Unique Network""" """Test Validation Rule Unique Network"""
import re import re
import logging
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.unique_network_check import UniqueNetworkCheck from drydock_provisioner.orchestrator.validations.unique_network_check import UniqueNetworkCheck
LOG = logging.getLogger(__name__)
class TestUniqueNetwork(object): class TestUniqueNetwork(object):
def test_unique_network(self, mocker, deckhand_ingester, drydock_state, def test_unique_network(self, mocker, deckhand_ingester, drydock_state,
@ -32,12 +35,11 @@ class TestUniqueNetwork(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = UniqueNetworkCheck() validator = UniqueNetworkCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert msg.get('message') == 'Unique Network'
assert msg.get('error') is False assert msg.get('error') is False
assert len(results) == 1 assert len(message_list) == 1
def test_invalid_unique_network(self, mocker, deckhand_ingester, def test_invalid_unique_network(self, mocker, deckhand_ingester,
drydock_state, input_files): drydock_state, input_files):
@ -51,15 +53,22 @@ class TestUniqueNetwork(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = UniqueNetworkCheck() validator = UniqueNetworkCheck()
results, message_list = validator.execute(site_design) message_list = validator.execute(site_design, orchestrator=orch)
regex = re.compile( regex = re.compile(
'Unique Network Error: Allowed network .+ duplicated on NetworkLink .+ and NetworkLink .+' 'Allowed network .+ duplicated on NetworkLink .+ and NetworkLink .+'
)
regex_1 = re.compile(
'Interface \S+ attached to network \S+ not allowed on interface link'
) )
for msg in results: assert len(message_list) >= 2
msg = msg.to_dict()
assert msg.get('error')
assert regex.match(msg.get('message')) is not None
assert len(results) == 1 for msg in message_list:
msg = msg.to_dict()
LOG.debug(msg)
assert msg.get('error')
assert any([
regex.search(msg.get('message')),
regex_1.search(msg.get('message'))
])

View File

@ -13,11 +13,15 @@
# limitations under the License. # limitations under the License.
"""Test Validation Rule Rational Boot Storage""" """Test Validation Rule Rational Boot Storage"""
import logging
import drydock_provisioner.config as config import drydock_provisioner.config as config
from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.orchestrator import Orchestrator
from drydock_provisioner.orchestrator.validations.platform_selection import PlatformSelection from drydock_provisioner.orchestrator.validations.platform_selection import PlatformSelection
LOG = logging.getLogger(__name__)
class TestValidPlatform(object): class TestValidPlatform(object):
def test_valid_platform(self, mocker, deckhand_ingester, drydock_state, def test_valid_platform(self, mocker, deckhand_ingester, drydock_state,
@ -42,16 +46,15 @@ class TestValidPlatform(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = PlatformSelection() validator = PlatformSelection()
results, message_list = validator.execute( message_list = validator.execute(site_design, orchestrator=orch)
site_design, orchestrator=orch)
for r in results:
print(r.to_dict())
msg = results[0].to_dict() for r in message_list:
LOG.debug(r.to_dict())
msg = message_list[0].to_dict()
assert 'all nodes have valid' in msg.get('message')
assert msg.get('error') is False assert msg.get('error') is False
assert len(results) == 1 assert len(message_list) == 1
def test_invalid_platform(self, mocker, deckhand_ingester, drydock_state, def test_invalid_platform(self, mocker, deckhand_ingester, drydock_state,
input_files): input_files):
@ -75,13 +78,13 @@ class TestValidPlatform(object):
status, site_design = Orchestrator.get_effective_site(orch, design_ref) status, site_design = Orchestrator.get_effective_site(orch, design_ref)
validator = PlatformSelection() validator = PlatformSelection()
results, message_list = validator.execute( message_list = validator.execute(site_design, orchestrator=orch)
site_design, orchestrator=orch)
for r in results: for r in message_list:
print(r.to_dict()) LOG.debug(r.to_dict())
msg = results[0].to_dict() msg = message_list[0].to_dict()
assert 'invalid kernel lts' in msg.get('message') assert 'invalid kernel lts' in msg.get('message')
assert msg.get('error') assert msg.get('error')
assert len(results) == 1 assert len(msg.get('documents')) > 0
assert len(message_list) == 1

View File

@ -93,6 +93,7 @@ data:
default_network: mgmt default_network: mgmt
allowed_networks: allowed_networks:
- public - public
- private
- mgmt - mgmt
--- ---
schema: 'drydock/Rack/v1' schema: 'drydock/Rack/v1'

View File

@ -308,6 +308,10 @@ data:
networks: networks:
- mgmt - mgmt
- private - private
############
### This fails here
############
- foo
metadata: metadata:
tags: tags:
- 'test' - 'test'

View File

@ -93,6 +93,7 @@ data:
default_network: mgmt default_network: mgmt
allowed_networks: allowed_networks:
- public - public
- private
- mgmt - mgmt
--- ---
schema: 'drydock/Rack/v1' schema: 'drydock/Rack/v1'

View File

@ -69,7 +69,7 @@ commands = flake8 \
commands = bandit -r drydock_provisioner -n 5 commands = bandit -r drydock_provisioner -n 5
[flake8] [flake8]
ignore=E302,H306,H304,W503,E251 ignore=E302,H306,H304,W503,E251,E126
exclude= venv,.venv,.git,.idea,.tox,*.egg-info,*.eggs,bin,dist,./build/,alembic/ exclude= venv,.venv,.git,.idea,.tox,*.egg-info,*.eggs,bin,dist,./build/,alembic/
max-line-length=119 max-line-length=119