diff --git a/drydock_provisioner/control/api.py b/drydock_provisioner/control/api.py index 62f5abd0..44af71a9 100644 --- a/drydock_provisioner/control/api.py +++ b/drydock_provisioner/control/api.py @@ -56,10 +56,12 @@ def start_api(state_manager=None, ingester=None, orchestrator=None): # v1.0 of Drydock API v1_0_routes = [ # API for managing orchestrator tasks - ('/health', HealthResource(state_manager=state_manager, - orchestrator=orchestrator)), - ('/health/extended', HealthExtendedResource(state_manager=state_manager, - orchestrator=orchestrator)), + ('/health', + HealthResource( + state_manager=state_manager, orchestrator=orchestrator)), + ('/health/extended', + HealthExtendedResource( + state_manager=state_manager, orchestrator=orchestrator)), ('/tasks', TasksResource(state_manager=state_manager, orchestrator=orchestrator)), diff --git a/drydock_provisioner/control/health.py b/drydock_provisioner/control/health.py index 01ef9cf8..d653a6ef 100644 --- a/drydock_provisioner/control/health.py +++ b/drydock_provisioner/control/health.py @@ -27,6 +27,7 @@ class HealthResource(StatefulResource): """ Returns empty response body that Drydock is healthy """ + def __init__(self, orchestrator=None, **kwargs): """Object initializer. @@ -39,15 +40,18 @@ class HealthResource(StatefulResource): """ Returns 204 on healthy, otherwise 503, without response body. """ - hc = HealthCheckCombined(state_manager=self.state_manager, - orchestrator=self.orchestrator, - extended=False) + hc = HealthCheckCombined( + state_manager=self.state_manager, + orchestrator=self.orchestrator, + extended=False) return hc.get(req, resp) + class HealthExtendedResource(StatefulResource): """ Returns response body that Drydock is healthy """ + def __init__(self, orchestrator=None, **kwargs): """Object initializer. @@ -61,15 +65,18 @@ class HealthExtendedResource(StatefulResource): """ Returns 200 on success, otherwise 503, with a response body. """ - hc = HealthCheckCombined(state_manager=self.state_manager, - orchestrator=self.orchestrator, - extended=True) + hc = HealthCheckCombined( + state_manager=self.state_manager, + orchestrator=self.orchestrator, + extended=True) return hc.get(req, resp) + class HealthCheckCombined(object): """ Returns Drydock health check status. """ + def __init__(self, state_manager=None, orchestrator=None, extended=False): """Object initializer. @@ -90,18 +97,22 @@ class HealthCheckCombined(object): if now is None: raise Exception('None received from database for now()') 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) # Test MaaS connection try: - task = self.orchestrator.create_task(action=hd_fields.OrchestratorAction.Noop) - maas_validation = ValidateNodeServices(task, self.orchestrator, self.state_manager) + task = self.orchestrator.create_task( + action=hd_fields.OrchestratorAction.Noop) + maas_validation = ValidateNodeServices(task, self.orchestrator, + self.state_manager) maas_validation.start() if maas_validation.task.get_status() == ActionResult.Failure: raise Exception('MaaS task failure') 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) if self.extended: diff --git a/drydock_provisioner/control/validation.py b/drydock_provisioner/control/validation.py index a2c1c678..73dd2f76 100644 --- a/drydock_provisioner/control/validation.py +++ b/drydock_provisioner/control/validation.py @@ -17,6 +17,8 @@ import json from drydock_provisioner import policy from drydock_provisioner.control.base import StatefulResource +from drydock_provisioner.objects import fields as hd_fields + import drydock_provisioner.error as errors @@ -36,21 +38,6 @@ class ValidationResource(StatefulResource): @policy.ApiEnforcer('physical_provisioner:validate_site_design') 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: json_data = self.req_json(req) @@ -68,29 +55,25 @@ class ValidationResource(StatefulResource): self.error(req.context, 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) - resp_message['details']['errorCount'] = message.error_count - resp_message['details']['messageList'] = [ - m.to_dict() for m in message.message_list - ] - - if message.error_count == 0: - resp_message['status'] = 'Success' - resp_message['message'] = 'Drydock Validations succeeded' + if validation.status == hd_fields.ValidationResult.Success: + resp_message = validation.to_dict() resp_message['code'] = 200 resp.status = falcon.HTTP_200 resp.body = json.dumps(resp_message) else: - resp_message['status'] = 'Failure' - resp_message['message'] = 'Drydock Validations failed' + resp_message = validation.to_dict() resp_message['code'] = 400 resp.status = falcon.HTTP_400 resp.body = json.dumps(resp_message) except errors.InvalidFormat as e: err_message = str(e) - resp.status = falcon.HTTP_400 self.error(req.context, 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) diff --git a/drydock_provisioner/error.py b/drydock_provisioner/error.py index 581af9ca..b9893ec9 100644 --- a/drydock_provisioner/error.py +++ b/drydock_provisioner/error.py @@ -53,6 +53,7 @@ class InvalidDesignReference(DesignError): """ pass + class UnsupportedDocumentType(DesignError): """ **Message:** *Site definition document in an unknown format*. diff --git a/drydock_provisioner/objects/healthcheck.py b/drydock_provisioner/objects/healthcheck.py index 9bdad8d4..8eeca4f8 100644 --- a/drydock_provisioner/objects/healthcheck.py +++ b/drydock_provisioner/objects/healthcheck.py @@ -13,6 +13,7 @@ # limitations under the License. """Models for representing health check status.""" + class HealthCheck(object): """Specialized status for health check status.""" diff --git a/drydock_provisioner/objects/validation.py b/drydock_provisioner/objects/validation.py index c37b1dbc..ff490f1f 100644 --- a/drydock_provisioner/objects/validation.py +++ b/drydock_provisioner/objects/validation.py @@ -60,7 +60,13 @@ class Validation(TaskStatus): class ValidationMessage(TaskStatusMessage): """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.message = msg self.error = error @@ -100,7 +106,8 @@ class DocumentReference(base.DrydockObject): super().__init__(**kwargs) if (self.doc_type == hd_fields.DocumentType.Deckhand): 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: raise errors.UnsupportedDocumentType( "Document type %s not supported." % self.doc_type) diff --git a/drydock_provisioner/orchestrator/orchestrator.py b/drydock_provisioner/orchestrator/orchestrator.py index 08c1ef73..c04f059c 100644 --- a/drydock_provisioner/orchestrator/orchestrator.py +++ b/drydock_provisioner/orchestrator/orchestrator.py @@ -285,7 +285,7 @@ class Orchestrator(object): val = Validator(self) try: 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_bootaction_targets(site_design) self.render_route_domains(site_design) diff --git a/drydock_provisioner/orchestrator/validations/boot_storage_rational.py b/drydock_provisioner/orchestrator/validations/boot_storage_rational.py index f257926f..982d3511 100644 --- a/drydock_provisioner/orchestrator/validations/boot_storage_rational.py +++ b/drydock_provisioner/orchestrator/validations/boot_storage_rational.py @@ -15,98 +15,81 @@ from drydock_provisioner.orchestrator.validations.validators import Validators import drydock_provisioner.error as errors from drydock_provisioner.orchestrator.util import SimpleBytes -from drydock_provisioner.objects.task import TaskStatusMessage + class BootStorageRational(Validators): 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 """ - message_list = [] - site_design = site_design.obj_to_simple() 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: - storage_devices_list = baremetal_node.get('storage_devices', []) + storage_devices_list = baremetal_node.storage_devices or [] root_set = False 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: - if host_partition.get('name') == 'root': - size = host_partition.get('size') + if host_partition.name == 'root': + size = host_partition.size try: cal_size = SimpleBytes.calculate_bytes(size) root_set = True # check if size < 20GB if cal_size < 20 * BYTES_IN_GB: msg = ( - 'Boot Storage Error: Root volume must be > 20GB on BaremetalNode ' - '%s' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + 'Root volume must be > 20GB on BaremetalNode ' + '%s' % baremetal_node.name) + self.report_error( + msg, [baremetal_node.doc_ref], + "Configure a larger root volume") except errors.InvalidSizeFormat as e: msg = ( - 'Boot Storage Error: Root volume has an invalid size format on BaremetalNode' - '%s.' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + 'Root volume has an invalid size format on BaremetalNode' + '%s.' % baremetal_node.name) + self.report_error(msg, [ + baremetal_node.doc_ref + ], "Use a valid root volume storage specification." + ) # check make sure root has been defined and boot volume > 1GB - if root_set and host_partition.get('name') == 'boot': - size = host_partition.get('size') + if root_set and host_partition.name == 'boot': + size = host_partition.size try: cal_size = SimpleBytes.calculate_bytes(size) # check if size < 1GB if cal_size < BYTES_IN_GB: msg = ( - 'Boot Storage Error: Boot volume must be > 1GB on BaremetalNode ' - '%s' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + 'Boot volume must be > 1GB on BaremetalNode ' + '%s' % baremetal_node.name) + self.report_error( + msg, [baremetal_node.doc_ref], + "Configure a larger boot volume.") except errors.InvalidSizeFormat as e: msg = ( - 'Boot Storage Error: Boot volume has an invalid size format on BaremetalNode ' - '%s.' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) - + 'Boot volume has an invalid size format on BaremetalNode ' + '%s.' % baremetal_node.name) + self.report_error(msg, [ + baremetal_node.doc_ref + ], "Use a valid boot volume storage specification." + ) # This must be set if not root_set: msg = ( - 'Boot Storage Error: Root volume has to be set and must be > 20GB on BaremetalNode ' - '%s' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + 'Root volume has to be set and must be > 20GB on BaremetalNode ' + '%s' % baremetal_node.name) + self.report_error(msg, [ + baremetal_node.doc_ref + ], "All nodes require a defined root volume at least 20GB in size." + ) - if not message_list: - message_list.append( - TaskStatusMessage( - msg='Boot Storage', error=False, ctx_type='NA', ctx='NA')) - - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/ip_locality_check.py b/drydock_provisioner/orchestrator/validations/ip_locality_check.py index 0123e049..172985b1 100644 --- a/drydock_provisioner/orchestrator/validations/ip_locality_check.py +++ b/drydock_provisioner/orchestrator/validations/ip_locality_check.py @@ -15,34 +15,31 @@ from drydock_provisioner.orchestrator.validations.validators import Validators from netaddr import IPNetwork, IPAddress -from drydock_provisioner.objects.task import TaskStatusMessage class IpLocalityCheck(Validators): 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 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 - message_list = [] - site_design = site_design.obj_to_simple() - baremetal_nodes_list = site_design.get('baremetal_nodes', []) - network_list = site_design.get('networks', []) + baremetal_nodes_list = site_design.baremetal_nodes or [] + network_list = site_design.networks or [] if not network_list: msg = 'No networks found.' - message_list.append( - TaskStatusMessage( - msg=msg, error=False, ctx_type='NA', ctx='NA')) + self.report_warn( + msg, [], + 'Site design likely incomplete without defined networks') else: for net in network_list: - name = net.get('name') - cidr = net.get('cidr') - routes = net.get('routes', []) + name = net.name + cidr = net.cidr + routes = net.routes or [] cidr_range = IPNetwork(cidr) network_dict[name] = cidr_range @@ -53,68 +50,52 @@ class IpLocalityCheck(Validators): if not gateway: msg = 'No gateway found for route %s.' % routes - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + self.report_error( + msg, [net.doc_ref], + diagnostic= + "Define a network-local gateway for the route." + ) else: ip = IPAddress(gateway) if ip not in cidr_range: msg = ( - 'IP Locality Error: The gateway IP Address %s ' - 'is not within the defined CIDR: %s of %s.' + 'The gateway IP Address %s is not within the defined CIDR: %s of %s.' % (gateway, cidr, name)) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + self.report_error( + msg, [net.doc_ref], + "Route gateways must reside on the local network. Check " + "gateway IP and CIDR netmask.") if not baremetal_nodes_list: msg = 'No baremetal_nodes found.' - message_list.append( - TaskStatusMessage( - msg=msg, error=False, ctx_type='NA', ctx='NA')) + self.report_warn( + msg, [], + "site design likely incomplete without defined networks.") else: for node in baremetal_nodes_list: - addressing_list = node.get('addressing', []) + addressing_list = node.addressing or [] for ip_address in addressing_list: - ip_address_network_name = ip_address.get('network') - address = ip_address.get('address') - ip_type = ip_address.get('type') + ip_address_network_name = ip_address.network + address = ip_address.address + ip_type = ip_address.type if ip_type is not 'dhcp': if ip_address_network_name not in network_dict: - msg = 'IP Locality Error: %s is not a valid network.' \ + msg = '%s is not a valid network.' \ % (ip_address_network_name) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + self.report_error(msg, [ + node.doc_ref + ], "Define network or correct address definition.") else: if IPAddress(address) not in IPNetwork( network_dict[ip_address_network_name]): msg = ( - 'IP Locality Error: The IP Address %s ' - 'is not within the defined CIDR: %s of %s .' + 'The IP Address %s is not within the defined CIDR: %s of %s .' % (address, network_dict[ip_address_network_name], ip_address_network_name)) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) - if not message_list: - msg = 'IP Locality Success' - message_list.append( - TaskStatusMessage( - msg=msg, error=False, ctx_type='NA', ctx='NA')) + self.report_error( + msg, [node.doc_ref], + "Define a valid address for this network.") - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/mtu_rational.py b/drydock_provisioner/orchestrator/validations/mtu_rational.py index 8f177ed1..719e0f6a 100644 --- a/drydock_provisioner/orchestrator/validations/mtu_rational.py +++ b/drydock_provisioner/orchestrator/validations/mtu_rational.py @@ -13,66 +13,63 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class MtuRational(Validators): - def __init__(self): - super().__init__('MTU Rational', 1003) + MIN_MTU_SIZE = 1280 + 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 for the parent NetworkLink for that network. Ensure that each defined MTU is a rational size, say > 1400 and < 64000 """ - message_list = [] - site_design = site_design.obj_to_simple() - - network_links = site_design.get('network_links', []) - networks = site_design.get('networks', []) + network_links = site_design.network_links or [] + networks = site_design.networks or [] parent_mtu_check = {} for network_link in network_links: - mtu = network_link.get('mtu') - # check mtu > 1400 and < 64000 - if mtu and (mtu < 1400 or mtu > 64000): - msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network Link %s.' % network_link.get( - 'name') - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + mtu = network_link.mtu + if mtu and (mtu < MtuRational.MIN_MTU_SIZE + or mtu > MtuRational.MAX_MTU_SIZE): + msg = ("MTU must be between %d and %d, value is %d" % + (MtuRational.MIN_MTU_SIZE, MtuRational.MAX_MTU_SIZE, + mtu)) + self.report_error( + msg, [network_link.doc_ref], + "Define a valid MTU. Standard is 1500, Jumbo is 9100.") # 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 for network in networks: - network_mtu = network.get('mtu') + network_mtu = network.mtu - # check mtu > 1400 and < 64000 - if network_mtu and (network_mtu < 1400 or network_mtu > 64000): - msg = 'Mtu Error: Mtu must be between 1400 and 64000; on Network %s.' % network.get( - 'name') - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + if network_mtu and (network_mtu < MtuRational.MIN_MTU_SIZE + or network_mtu > MtuRational.MAX_MTU_SIZE): + msg = ("MTU must be between %d and %d, value is %d" % + (MtuRational.MIN_MTU_SIZE, MtuRational.MAX_MTU_SIZE, + mtu)) + self.report_error( + 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) if network_mtu and parent_mtu: # check to make sure mtu for network is <= parent network link if network_mtu > parent_mtu: - msg = 'Mtu Error: Mtu must be <= the parent Network Link; for Network %s' % ( - network.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = 'MTU must be <= the parent Network Link; for Network %s' % ( + network.name) + self.report_error(msg, [ + network.doc_ref + ], "Define a MTU less than or equal to that of the carrying network link." + ) - if not message_list: - message_list.append( - TaskStatusMessage( - msg='Mtu', error=False, ctx_type='NA', ctx='NA')) - - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/network_trunking_rational.py b/drydock_provisioner/orchestrator/validations/network_trunking_rational.py index 96ef9eb9..d37cd9de 100644 --- a/drydock_provisioner/orchestrator/validations/network_trunking_rational.py +++ b/drydock_provisioner/orchestrator/validations/network_trunking_rational.py @@ -14,57 +14,49 @@ from drydock_provisioner.orchestrator.validations.validators import Validators import drydock_provisioner.objects.fields as hd_fields -from drydock_provisioner.objects.task import TaskStatusMessage + class NetworkTrunkingRational(Validators): 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 enabled. It also makes sure that if trunking mode is disabled then a default network is defined. """ - message_list = [] - site_design = site_design.obj_to_simple() - - network_link_list = site_design.get('network_links', []) + network_link_list = site_design.network_links or [] 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 (len(allowed_networks) > 1 and network_link.get('trunk_mode') == + if (len(allowed_networks) > 1 and network_link.trunk_mode == hd_fields.NetworkLinkTrunkingMode.Disabled): - - msg = ( - 'Rational Network Trunking Error: If there is more than 1 allowed network,' - 'trunking mode must be enabled; on NetworkLink %s' % - network_link.get('name')) - - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = ('If there is more than 1 allowed network,' + 'trunking mode must be enabled') + self.report_error(msg, [ + network_link.doc_ref + ], "Reduce the allowed network list to 1 or enable trunking on the link." + ) # trunking mode is disabled, default_network must be defined - if (network_link.get( - 'trunk_mode') == hd_fields.NetworkLinkTrunkingMode.Disabled - and network_link.get('native_network') is None): + if (network_link.trunk_mode == + hd_fields.NetworkLinkTrunkingMode.Disabled + and network_link.native_network is None): - msg = ( - 'Rational Network Trunking Error: Trunking mode is disabled, a trunking' - 'default_network must be defined; on NetworkLink %s' % - network_link.get('name')) + msg = 'Trunking mode is disabled, a trunking default_network must be defined' + self.report_error( + msg, [network_link.doc_ref], + "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( - 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) + return diff --git a/drydock_provisioner/orchestrator/validations/no_duplicate_ips_check.py b/drydock_provisioner/orchestrator/validations/no_duplicate_ips_check.py index 2e9c7f4d..8b21b7f8 100644 --- a/drydock_provisioner/orchestrator/validations/no_duplicate_ips_check.py +++ b/drydock_provisioner/orchestrator/validations/no_duplicate_ips_check.py @@ -13,51 +13,40 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class NoDuplicateIpsCheck(Validators): 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 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. """ found_ips = {} # Dictionary Format - IP address: BaremetalNode name - message_list = [] - site_design = site_design.obj_to_simple() - baremetal_nodes_list = site_design.get('baremetal_nodes', []) + baremetal_nodes_list = site_design.baremetal_nodes or [] if not baremetal_nodes_list: msg = 'No BaremetalNodes Found.' - message_list.append( - TaskStatusMessage( - msg=msg, error=False, ctx_type='NA', ctx='NA')) + self.report_warn( + msg, [], + "Site design unlikely complete with no defined baremetal nodes." + ) else: for node in baremetal_nodes_list: - addressing_list = node.get('addressing', []) + addressing_list = node.addressing or [] for ip_address in addressing_list: - address = ip_address.get('address') - node_name = node.get('name') + address = ip_address.address if address in found_ips and address is not None: - msg = ('Error! Duplicate IP Address Found: %s ' - 'is in use by both %s and %s.' % - (address, found_ips[address], node_name)) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = ('Duplicate IP Address Found: %s ' % address) + self.report_error( + msg, [node.doc_ref, found_ips[address].doc_ref], + "Select unique IP addresses for each node.") elif address is not None: - found_ips[address] = node_name + found_ips[address] = node - if not message_list: - 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) + return diff --git a/drydock_provisioner/orchestrator/validations/platform_selection.py b/drydock_provisioner/orchestrator/validations/platform_selection.py index 152d805f..e8cc792f 100644 --- a/drydock_provisioner/orchestrator/validations/platform_selection.py +++ b/drydock_provisioner/orchestrator/validations/platform_selection.py @@ -13,32 +13,28 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class PlatformSelection(Validators): 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. Each node specifies an ``image`` and a ``kernel`` to use for deployment. Check that these are valid for the image repository configured in MAAS. """ - message_list = list() - try: node_driver = orchestrator.enabled_drivers['node'] except KeyError: - message_list.append( - TaskStatusMessage( - msg="Platform Validation: No enabled node driver, image" - "and kernel selections not validated.", - error=False, - ctx_type='NA', - ctx='NA')) - return Validators.report_results(self, message_list) + msg = ("Platform Validation: No enabled node driver, image" + "and kernel selections not validated.") + self.report_warn( + msg, [], + "Cannot validate platform selection without accessing the node provisioner." + ) + return valid_images = node_driver.get_available_images() @@ -51,28 +47,14 @@ class PlatformSelection(Validators): if n.image in valid_images: if n.kernel in valid_kernels[n.image]: continue - message_list.append( - TaskStatusMessage( - msg="Platform Validation: invalid kernel %s for node %s." - % (n.kernel, n.name), - error=True, - ctx_type='NA', - ctx='NA')) + msg = "Platform Validation: invalid kernel %s" % (n.kernel) + self.report_error(msg, [n.doc_ref], + "Select a valid kernel from: %s" % ",".join( + valid_kernels[n.image])) continue - message_list.append( - TaskStatusMessage( - msg="Platform Validation: invalid image %s for node %s." % - (n.image, n.name), - 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')) + msg = "Platform Validation: invalid image %s" % (n.image) + self.report_error( + msg, [n.doc_ref], + "Select a valid image from: %s" % ",".join(valid_images)) - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/rational_network_bond.py b/drydock_provisioner/orchestrator/validations/rational_network_bond.py index 86ca2602..99a9124f 100644 --- a/drydock_provisioner/orchestrator/validations/rational_network_bond.py +++ b/drydock_provisioner/orchestrator/validations/rational_network_bond.py @@ -13,13 +13,12 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class RationalNetworkBond(Validators): 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. 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 bonding peer rate are both NOT defined. """ - message_list = [] - site_design = site_design.obj_to_simple() - - network_links = site_design.get('network_links', []) + network_links = site_design.network_links or [] for network_link in network_links: - bonding_mode = network_link.get('bonding_mode', []) + bonding_mode = network_link.bonding_mode if bonding_mode == 'disabled': # check to make sure nothing else is specified if any([ - network_link.get(x) for x in [ + getattr(network_link, x) for x in [ 'bonding_peer_rate', 'bonding_xmit_hash', 'bonding_mon_rate', 'bonding_up_delay', 'bonding_down_delay' ] ]): - - msg = ( - 'Network Link Bonding Error: If bonding mode is disabled no other bond option can be' - 'specified; on BaremetalNode %s' % - network_link.get('name')) - - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = 'If bonding mode is disabled no other bond option can be specified' + self.report_error( + msg, [network_link.doc_ref], + "Enable a bonding mode or remove the bond options.") elif bonding_mode == '802.3ad': # check if up_delay and down_delay are >= mon_rate - mon_rate = network_link.get('bonding_mon_rate') - if network_link.get('bonding_up_delay') < mon_rate: - msg = ('Network Link Bonding Error: Up delay is less ' - 'than mon rate on BaremetalNode %s' % - (network_link.get('name'))) + mon_rate = network_link.bonding_mon_rate + if network_link.bonding_up_delay < mon_rate: + msg = ('Up delay %d is less than mon rate %d' % + (network_link.bonding_up_delay, mon_rate)) + self.report_error(msg, [ + network_link.doc_ref + ], "Link up delay must be equal or greater than the mon_rate" + ) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) - - if network_link.get('bonding_down_delay') < mon_rate: - msg = ('Network Link Bonding Error: Down delay is ' - 'less than mon rate on BaremetalNode %s' % - (network_link.get('name'))) - - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + if network_link.bonding_down_delay < mon_rate: + msg = ('Down delay %d is less than mon rate %d' % + (network_link.bonding_down_delay, mon_rate)) + self.report_error(msg, [ + network_link.doc_ref + ], "Link down delay must be equal or greater than the mon_rate" + ) elif bonding_mode in ['active-backup', 'balanced-rr']: # make sure hash and peer_rate are NOT defined if network_link.get('bonding_xmit_hash'): - msg = ( - 'Network Link Bonding Error: Hash cannot be defined if bond mode is ' - '%s, on BaremetalNode %s' % (bonding_mode, - network_link.get('name'))) - - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = ('Hash cannot be defined if bond mode is %s' % + (bonding_mode)) + self.report_error( + msg, [network_link.doc_ref], + "Hash mode is only applicable to LACP (802.3ad)") if network_link.get('bonding_peer_rate'): - msg = ( - 'Network Link Bonding Error: Peer rate cannot be defined if bond mode is ' - '%s, on BaremetalNode %s' % (bonding_mode, - network_link.get('name'))) + msg = ('Peer rate cannot be defined if bond mode is %s' % + (bonding_mode)) + self.report_error( + msg, [network_link.doc_ref], + "Peer rate is only applicable to LACP (802.3ad)") - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) - - if not message_list: - message_list.append( - TaskStatusMessage( - msg='Network Link Bonding', - error=False, - ctx_type='NA', - ctx='NA')) - - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/storage_partititioning.py b/drydock_provisioner/orchestrator/validations/storage_partititioning.py index 8990592e..a03729ad 100644 --- a/drydock_provisioner/orchestrator/validations/storage_partititioning.py +++ b/drydock_provisioner/orchestrator/validations/storage_partititioning.py @@ -13,90 +13,69 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class StoragePartitioning(Validators): 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 list it ensures that a file system and partition volume group are not defined in the same partition. """ - message_list = [] - site_design = site_design.obj_to_simple() - - baremetal_nodes = site_design.get('baremetal_nodes', []) - + baremetal_nodes = site_design.baremetal_nodes or [] volume_group_check_list = [] 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: - partitions_list = storage_device.get('partitions') - volume_group = storage_device.get('volume_group') + partitions_list = storage_device.partitions or [] + volume_group = storage_device.volume_group # error if both or neither is defined if all([partitions_list, volume_group ]) or not any([partitions_list, volume_group]): - msg = ('Storage Partitioning Error: Either a volume group ' - 'OR partitions must be defined for each storage ' - 'device; on BaremetalNode ' - '%s' % baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = ( + 'Either a volume group OR partitions must be defined for each storage ' + 'device.') + self.report_error( + msg, [baremetal_node.doc_ref], + "A storage device must be used for exactly one of a volume group " + "physical volume or carved into partitions.") # if there is a volume group add to list if volume_group is not None: volume_group_check_list.append(volume_group) - if partitions_list is not None: - for partition in partitions_list: - partition_volume_group = partition.get('volume_group') - fstype = partition.get('fstype') + for partition in partitions_list: + partition_volume_group = partition.volume_group + fstype = partition.fstype - # error if both are defined - if all([fstype, partition_volume_group]): - msg = ( - 'Storage Partitioning Error: Both a volume group AND file system cannot be ' - 'defined in a sigle partition; on BaremetalNode %s' - % baremetal_node.get('name')) + # error if both are defined + if all([fstype, partition_volume_group]): + msg = ('Both a volume group AND file system cannot be ' + 'defined in a single partition') + self.report_error( + 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( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) - - # if there is a volume group add to list - if partition_volume_group is not None: - volume_group_check_list.append(volume_group) + # if there is a volume group add to list + if partition_volume_group is not None: + volume_group_check_list.append(volume_group) # checks all volume groups are assigned to a partition or storage device # if one exist that wasn't found earlier it is unassigned - all_volume_groups = baremetal_node.get('volume_groups', []) + all_volume_groups = baremetal_node.volume_groups or [] 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 = ( - 'Storage Partitioning Error: A volume group must be assigned to a storage device or ' - 'partition; volume group %s on BaremetalNode %s' % - (volume_group.get('name'), baremetal_node.get('name'))) + 'Volume group %s not assigned any physical volumes' % + (volume_group.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( - 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) + return diff --git a/drydock_provisioner/orchestrator/validations/storage_sizing.py b/drydock_provisioner/orchestrator/validations/storage_sizing.py index 5b730d4c..f86e03fb 100644 --- a/drydock_provisioner/orchestrator/validations/storage_sizing.py +++ b/drydock_provisioner/orchestrator/validations/storage_sizing.py @@ -13,91 +13,70 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class StorageSizing(Validators): 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 in a volume group, if sizing is a percentage then those percentages do not sum > 99% and have no negative values """ - message_list = [] - site_design = site_design.obj_to_simple() - - baremetal_nodes = site_design.get('baremetal_nodes', []) + baremetal_nodes = site_design.baremetal_nodes or [] 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: - partition_list = storage_device.get('partitions', []) + partition_list = storage_device.partitions or [] partition_sum = 0 for partition in partition_list: - size = partition.get('size') + size = partition.size percent = size.split('%') if len(percent) == 2: if int(percent[0]) < 0: msg = ( - 'Storage Sizing Error: Storage partition size is < 0 ' - 'on Baremetal Node %s' % - baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + 'Storage partition %s on device %s size is < 0' + % (partition.name, storage_device.name)) + self.report_error( + msg, [baremetal_node.doc_ref], + "Partition size must be a positive number.") partition_sum += int(percent[0]) if partition_sum > 99: msg = ( - 'Storage Sizing Error: Storage partition size is greater than ' - '99 on Baremetal Node %s' % - baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + 'Cumulative partition sizes on device %s is greater than 99%%.' + % (storage_device.name)) + self.report_error(msg, [ + baremetal_node.doc_ref + ], "Percentage-based sizes must sum to less than 100%." + ) - volume_groups = baremetal_node.get('volume_groups', []) + volume_groups = baremetal_node.volume_groups or [] volume_sum = 0 for volume_group in volume_groups: - logical_volume_list = volume_group.get( - 'logical_volumes', []) + logical_volume_list = volume_group.logical_volumes or [] for logical_volume in logical_volume_list: - size = logical_volume.get('size') + size = logical_volume.size percent = size.split('%') if len(percent) == 2: if int(percent[0]) < 0: - msg = ( - 'Storage Sizing Error: Storage volume size is < 0 ' - 'on Baremetal Node %s' % - baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, - error=True, - ctx_type='NA', - ctx='NA')) + msg = ('Logical Volume %s size is < 0 ' % + (logical_volume.name)) + self.report_error(msg, + [baremetal_node.doc_ref], "") volume_sum += int(percent[0]) if volume_sum > 99: - msg = ( - 'Storage Sizing Error: Storage volume size is greater ' - 'than 99 on Baremetal Node %s.' % - baremetal_node.get('name')) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + msg = ('Cumulative logical volume size is greater ' + 'than 99% in volume group %s' % + (volume_group.name)) + self.report_error(msg, [ + baremetal_node.doc_ref + ], "Percentage-based sizes must sum to less than 100%." + ) - if not message_list: - message_list.append( - TaskStatusMessage( - msg='Storage Sizing', error=False, ctx_type='NA', - ctx='NA')) - - return Validators.report_results(self, message_list) + return diff --git a/drydock_provisioner/orchestrator/validations/unique_network_check.py b/drydock_provisioner/orchestrator/validations/unique_network_check.py index 550b1969..cfaee617 100644 --- a/drydock_provisioner/orchestrator/validations/unique_network_check.py +++ b/drydock_provisioner/orchestrator/validations/unique_network_check.py @@ -13,39 +13,37 @@ # limitations under the License. from drydock_provisioner.orchestrator.validations.validators import Validators -from drydock_provisioner.objects.task import TaskStatusMessage class UniqueNetworkCheck(Validators): 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 allowed networks """ - message_list = [] - site_design = site_design.obj_to_simple() - network_link_list = site_design.get('network_links', []) - compare = {} + network_link_list = site_design.network_links or [] + link_allowed_nets = {} for network_link in network_link_list: - allowed_network_list = network_link.get('allowed_networks', []) - compare[network_link.get('name')] = allowed_network_list + allowed_network_list = network_link.allowed_networks + link_allowed_nets[network_link.name] = allowed_network_list # This checks the allowed networks for each network link against # the other allowed networks checked_pairs = [] - for network_link_name in compare: - allowed_network_list_1 = compare[network_link_name] + for network_link_name in link_allowed_nets: + 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 and sorted([network_link_name, network_link_name_2 ]) not in checked_pairs): checked_pairs.append( sorted([network_link_name, network_link_name_2])) - allowed_network_list_2 = compare[network_link_name_2] + allowed_network_list_2 = link_allowed_nets[ + network_link_name_2] # creates a list of duplicated allowed networks duplicated_names = [ i for i in allowed_network_list_1 @@ -54,17 +52,36 @@ class UniqueNetworkCheck(Validators): for name in duplicated_names: 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, network_link_name_2)) - message_list.append( - TaskStatusMessage( - msg=msg, error=True, ctx_type='NA', ctx='NA')) + self.report_error( + msg, [], + "Each network is only allowed to cross a single network link." + ) - if not message_list: - message_list.append( - TaskStatusMessage( - msg='Unique Network', error=False, ctx_type='NA', - ctx='NA')) + node_list = site_design.baremetal_nodes or [] - 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 diff --git a/drydock_provisioner/orchestrator/validations/validator.py b/drydock_provisioner/orchestrator/validations/validator.py index 265ea6d7..3f8df563 100644 --- a/drydock_provisioner/orchestrator/validations/validator.py +++ b/drydock_provisioner/orchestrator/validations/validator.py @@ -15,7 +15,7 @@ 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.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.unique_network_check import UniqueNetworkCheck + class Validator(): def __init__(self, orchestrator): """Create a validator with a reference to the orchestrator. @@ -36,7 +37,10 @@ class Validator(): """ 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. 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 """ if result_status is None: - result_status = TaskStatus() + result_status = Validation() validation_error = False - message_lists = [] for rule in rule_set: - results, message_list = rule.execute(site_design=site_design, orchestrator=self.orchestrator) - for item in message_list: - message_lists.append(item) - result_status.message_list.extend(results) - error_msg = [m for m in results if m.error] + message_list = rule.execute( + site_design=site_design, orchestrator=self.orchestrator) + result_status.message_list.extend(message_list) + error_msg = [m for m in message_list if m.error] result_status.error_count = result_status.error_count + len( error_msg) if len(error_msg) > 0: @@ -64,29 +66,12 @@ class Validator(): if validation_error: result_status.set_status(hd_fields.ValidationResult.Failure) + result_status.message = "Site design failed validation." + result_status.reason = "See detail messages." else: 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 diff --git a/drydock_provisioner/orchestrator/validations/validators.py b/drydock_provisioner/orchestrator/validations/validators.py index f7506dea..979b9615 100644 --- a/drydock_provisioner/orchestrator/validations/validators.py +++ b/drydock_provisioner/orchestrator/validations/validators.py @@ -13,28 +13,58 @@ # limitations under the License. """Business Logic Validation""" +from drydock_provisioner import objects +from drydock_provisioner.objects import fields as hd_fields + + class Validators: - def __init__(self, name, code): + def __init__(self, long_name, name): self.name = name - self.code = code + self.long_name = long_name + self.reset_message_list() - def report_results(self, results): - # https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure - 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 report_msg(self, msg, docs, diagnostic, error, level): + """Add a validation message to the result list. - def execute(site_design, orchestrator=None): - pass + :param msg: String - msg, will be prepended with validator long name + :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 diff --git a/drydock_provisioner/policy.py b/drydock_provisioner/policy.py index 970111a2..e1c06605 100644 --- a/drydock_provisioner/policy.py +++ b/drydock_provisioner/policy.py @@ -126,13 +126,12 @@ class DrydockPolicy(object): 'path': '/api/v1.0/designs/{design_id}/parts', 'method': 'POST' }]), - policy.DocumentedRuleDefault( - 'physical_provisioner:health_data', 'role:admin', - 'et health status', - [{ - 'path': '/api/v1.0/health/extended', - 'method': 'GET' - }]) + policy.DocumentedRuleDefault('physical_provisioner:health_data', + 'role:admin', 'et health status', + [{ + 'path': '/api/v1.0/health/extended', + 'method': 'GET' + }]) ] # Validate Design Policy diff --git a/drydock_provisioner/statemgmt/state.py b/drydock_provisioner/statemgmt/state.py index c025ef45..d5b2fbf6 100644 --- a/drydock_provisioner/statemgmt/state.py +++ b/drydock_provisioner/statemgmt/state.py @@ -684,7 +684,5 @@ class DrydockState(object): return None except Exception as ex: self.logger.error(str(ex)) - self.logger.error( - "Error querying for now()", - exc_info=True) + self.logger.error("Error querying for now()", exc_info=True) return None diff --git a/tests/integration/postgres/test_api_health.py b/tests/integration/postgres/test_api_health.py index c0a5d786..c4b38faf 100644 --- a/tests/integration/postgres/test_api_health.py +++ b/tests/integration/postgres/test_api_health.py @@ -19,7 +19,8 @@ import falcon 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 req = mocker.MagicMock(spec=falcon.Request) diff --git a/tests/integration/postgres/test_orch_generic.py b/tests/integration/postgres/test_orch_generic.py index afdfc567..ecafda06 100644 --- a/tests/integration/postgres/test_orch_generic.py +++ b/tests/integration/postgres/test_orch_generic.py @@ -20,13 +20,13 @@ import drydock_provisioner.objects.fields as hd_fields 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): - input_file = input_files.join("fullsite.yaml") + input_file = input_files.join("deckhand_fullsite.yaml") design_ref = "file://%s" % str(input_file) orchestrator = orch.Orchestrator( - state_manager=blank_state, ingester=yaml_ingester) + state_manager=blank_state, ingester=deckhand_ingester) orch_task = orchestrator.create_task( action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) orch_task.set_status(hd_fields.TaskStatus.Queued) @@ -45,13 +45,13 @@ class TestClass(object): orchestrator.stop_orchestrator() 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): - input_file = input_files.join("fullsite.yaml") + input_file = input_files.join("deckhand_fullsite.yaml") design_ref = "file://%s" % str(input_file) orchestrator = orch.Orchestrator( - state_manager=blank_state, ingester=yaml_ingester) + state_manager=blank_state, ingester=deckhand_ingester) orch_task = orchestrator.create_task( action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) diff --git a/tests/unit/test_api_validation.py b/tests/unit/test_api_validation.py index 0c2ee624..686de2c8 100644 --- a/tests/unit/test_api_validation.py +++ b/tests/unit/test_api_validation.py @@ -16,12 +16,15 @@ from falcon import testing import pytest import json +import logging from drydock_provisioner import policy from drydock_provisioner.control.api import start_api import falcon +LOG = logging.getLogger(__name__) + class TestValidationApi(object): def test_post_validation_resp(self, input_files, falcontest, drydock_state, @@ -46,6 +49,7 @@ class TestValidationApi(object): result = falcontest.simulate_post( url, headers=hdr, body=json.dumps(body)) + LOG.debug(result.text) assert result.status == falcon.HTTP_200 def test_href_error(self, input_files, falcontest): @@ -65,6 +69,7 @@ class TestValidationApi(object): result = falcontest.simulate_post( url, headers=hdr, body=json.dumps(body)) + LOG.debug(result.text) assert result.status == falcon.HTTP_400 def test_json_data_error(self, input_files, falcontest): @@ -80,6 +85,7 @@ class TestValidationApi(object): result = falcontest.simulate_post( url, headers=hdr, body=json.dumps(body)) + LOG.debug(result.text) assert result.status == falcon.HTTP_400 def test_invalid_post_resp(self, input_files, falcontest, drydock_state, diff --git a/tests/unit/test_ingester.py b/tests/unit/test_ingester.py index 8ac9db9a..96cd800a 100644 --- a/tests/unit/test_ingester.py +++ b/tests/unit/test_ingester.py @@ -31,7 +31,8 @@ class TestClass(object): assert len(design_data.host_profiles) == 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.""" input_file = input_files.join('deckhand_fullsite.yaml') diff --git a/tests/unit/test_validate_design.py b/tests/unit/test_validate_design.py index d0269fff..4ca953c3 100644 --- a/tests/unit/test_validate_design.py +++ b/tests/unit/test_validate_design.py @@ -33,7 +33,4 @@ class TestDesignValidator(object): val = Validator(orch) 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 diff --git a/tests/unit/test_validation_rule_boot_storage.py b/tests/unit/test_validation_rule_boot_storage.py index 956558ca..4da738d3 100644 --- a/tests/unit/test_validation_rule_boot_storage.py +++ b/tests/unit/test_validation_rule_boot_storage.py @@ -14,15 +14,17 @@ """Test Validation Rule Rational Boot Storage""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.boot_storage_rational import BootStorageRational +LOG = logging.getLogger(__name__) + class TestRationalBootStorage(object): def test_boot_storage_rational(self, deckhand_ingester, drydock_state, input_files, mock_get_build_data): - input_file = input_files.join("validation.yaml") design_ref = "file://%s" % str(input_file) @@ -32,16 +34,14 @@ class TestRationalBootStorage(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = BootStorageRational() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'Boot Storage' 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, input_files, mock_get_build_data): - input_file = input_files.join("invalid_boot_storage_small.yaml") design_ref = "file://%s" % str(input_file) @@ -51,21 +51,21 @@ class TestRationalBootStorage(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = BootStorageRational() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) - regex = re.compile( - 'Boot Storage Error: .+ volume must be > .+GB on BaremetalNode .+') + regex = re.compile('.+ volume must be > .+GB') - for msg in results: + for msg in message_list: 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 len(results) == 4 + assert len(message_list) == 4 def test_invalid_boot_storage_root_not_set(self, deckhand_ingester, drydock_state, input_files): - input_file = input_files.join("invalid_validation.yaml") design_ref = "file://%s" % str(input_file) @@ -75,15 +75,15 @@ class TestRationalBootStorage(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = BootStorageRational() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design) - regex = re.compile( - 'Boot Storage Error: Root volume has to be set and must be > 20GB on BaremetalNode .+' - ) + regex = re.compile('Root volume has to be set and must be > 20GB') - for msg in results: + for msg in message_list: 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 len(results) == 2 + assert len(message_list) == 2 diff --git a/tests/unit/test_validation_rule_ip_locality.py b/tests/unit/test_validation_rule_ip_locality.py index 472eae6f..ad02d646 100644 --- a/tests/unit/test_validation_rule_ip_locality.py +++ b/tests/unit/test_validation_rule_ip_locality.py @@ -13,10 +13,13 @@ # limitations under the License. import re +import logging from drydock_provisioner.orchestrator.validations.ip_locality_check import IpLocalityCheck from drydock_provisioner.orchestrator.orchestrator import Orchestrator +LOG = logging.getLogger(__name__) + class TestIPLocality(object): 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) validator = IpLocalityCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'IP Locality Success' assert msg.get('error') is False 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) validator = IpLocalityCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + 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 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) validator = IpLocalityCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() assert 'No gateway found' in msg.get('message') assert msg.get('error') is True @@ -80,10 +82,10 @@ class TestIPLocality(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = IpLocalityCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + 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 def test_invalid_ip_locality_invalid_network( @@ -97,20 +99,24 @@ class TestIPLocality(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = IpLocalityCheck() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) 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( - '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 - for msg in results: + assert len(message_list) == 3 + + for msg in message_list: msg = msg.to_dict() + LOG.debug(msg) + assert len(msg.get('documents')) > 0 assert msg.get('error') - assert (regex.match(msg.get('message')) is not None - or regex_1.match(msg.get('message')) is not None - or regex_2.match(msg.get('message')) is not None) + assert any([ + regex.search(msg.get('message')), + regex_1.search(msg.get('message')), + regex_2.search(msg.get('message')) + ]) diff --git a/tests/unit/test_validation_rule_mtu_rational.py b/tests/unit/test_validation_rule_mtu_rational.py index 98324b1c..2928c809 100644 --- a/tests/unit/test_validation_rule_mtu_rational.py +++ b/tests/unit/test_validation_rule_mtu_rational.py @@ -14,14 +14,16 @@ """Test Validation Rule Unique Network""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.mtu_rational import MtuRational +LOG = logging.getLogger(__name__) + class TestMtu(object): def test_mtu(self, mocker, deckhand_ingester, drydock_state, input_files): - input_file = input_files.join("validation.yaml") design_ref = "file://%s" % str(input_file) @@ -31,12 +33,12 @@ class TestMtu(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = MtuRational() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'Mtu' + assert 'MTU' in msg.get('message') 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, input_files): @@ -50,19 +52,19 @@ class TestMtu(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = MtuRational() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) - regex = re.compile( - 'Mtu Error: Mtu must be between 1400 and 64000; on Network .+') - regex_1 = re.compile( - 'Mtu Error: Mtu must be <= the parent Network Link; for Network .+' - ) + regex = re.compile('MTU must be between \d+ and \d+') + regex_1 = re.compile('MTU must be <= the parent Network Link') - for msg in results: + for msg in message_list: msg = msg.to_dict() + LOG.debug(msg) assert msg.get('error') - assert regex.match( - msg.get('message')) is not None or regex_1.match( - msg.get('message')) is not None + assert len(msg.get('documents')) > 0 + assert any([ + regex.search(msg.get('message')), + regex_1.search(msg.get('message')) + ]) - assert len(results) == 4 + assert len(message_list) == 4 diff --git a/tests/unit/test_validation_rule_network_bond.py b/tests/unit/test_validation_rule_network_bond.py index a85f44d2..fce2c2a8 100644 --- a/tests/unit/test_validation_rule_network_bond.py +++ b/tests/unit/test_validation_rule_network_bond.py @@ -14,10 +14,13 @@ """Test Validation Rule Rational Network Bond""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.rational_network_bond import RationalNetworkBond +LOG = logging.getLogger(__name__) + class TestRationalNetworkLinkBond(object): 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) validator = RationalNetworkBond() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'Network Link Bonding' 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, drydock_state, input_files): @@ -50,20 +53,19 @@ class TestRationalNetworkLinkBond(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = RationalNetworkBond() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) - regex = re.compile( - 'Network Link Bonding Error: Down delay is less than mon rate on BaremetalNode .+' - ) - regex_1 = re.compile( - 'Network Link Bonding Error: Up delay is less than mon rate on BaremetalNode .+' - ) + regex = re.compile('Down delay \S+ is less than mon rate \S+') + regex_1 = re.compile('Up delay \S+ is less than mon rate \S+') - for msg in results: + for msg in message_list: msg = msg.to_dict() - assert msg.get('error') is True - assert regex.match( - msg.get('message')) is not None or regex_1.match( - msg.get('message')) is not None + LOG.debug(msg) + assert msg.get('error') + assert len(msg.get('documents')) > 0 + assert any([ + regex.search(msg.get('message')), + regex_1.search(msg.get('message')) + ]) - assert len(results) == 2 + assert len(message_list) == 2 diff --git a/tests/unit/test_validation_rule_network_trunking.py b/tests/unit/test_validation_rule_network_trunking.py index 9c928aa3..4e75bd13 100644 --- a/tests/unit/test_validation_rule_network_trunking.py +++ b/tests/unit/test_validation_rule_network_trunking.py @@ -14,10 +14,13 @@ """Test Validation Rule Rational Network Trunking""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.network_trunking_rational import NetworkTrunkingRational +LOG = logging.getLogger(__name__) + class TestRationalNetworkTrunking(object): 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) validator = NetworkTrunkingRational() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'Rational Network Trunking' assert msg.get('error') is False 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) validator = NetworkTrunkingRational() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) regex = re.compile( - 'Rational Network Trunking Error: Trunking mode is disabled, a trunking' - 'default_network must be defined; on NetworkLink .+') + 'Trunking mode is disabled, a trunking default_network must be defined' + ) regex_1 = re.compile( - 'Rational Network Trunking Error: If there is more than 1 allowed network,' - 'trunking mode must be enabled; on NetworkLink .+') + 'If there is more than 1 allowed network,trunking mode must be enabled' + ) - for msg in results: + regex_2 = re.compile('native network has a defined VLAN tag') + + for msg in message_list: msg = msg.to_dict() + LOG.debug(msg) assert msg.get('error') - assert regex.match( - msg.get('message')) is not None or regex_1.match( - msg.get('message')) is not None + assert len(msg.get('documents')) > 0 + assert any([ + 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 diff --git a/tests/unit/test_validation_rule_no_duplicate_IPs.py b/tests/unit/test_validation_rule_no_duplicate_IPs.py index d2dd0621..ba028d9b 100644 --- a/tests/unit/test_validation_rule_no_duplicate_IPs.py +++ b/tests/unit/test_validation_rule_no_duplicate_IPs.py @@ -13,10 +13,13 @@ # limitations under the License. import re +import logging from drydock_provisioner.orchestrator.validations.no_duplicate_ips_check import NoDuplicateIpsCheck from drydock_provisioner.orchestrator.orchestrator import Orchestrator +LOG = logging.getLogger(__name__) + class TestDuplicateIPs(object): 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) validator = NoDuplicateIpsCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'No Duplicate IP Addresses.' assert msg.get('error') is False 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) validator = NoDuplicateIpsCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + 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 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) validator = NoDuplicateIpsCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + 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 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) validator = NoDuplicateIpsCheck() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design) - regex = re.compile( - 'Error! Duplicate IP Address Found: .+ is in use by both .+ and .+.' - ) - for msg in results: + regex = re.compile('Duplicate IP Address Found: [0-9.]+') + + for msg in message_list: msg = msg.to_dict() + LOG.debug(msg) + assert len(msg.get('documents')) > 0 assert msg.get('error') is True - assert regex.match(msg.get('message')) is not None + assert regex.search(msg.get('message')) is not None diff --git a/tests/unit/test_validation_rule_storage_partitioning.py b/tests/unit/test_validation_rule_storage_partitioning.py index b8f77a35..b542dcd7 100644 --- a/tests/unit/test_validation_rule_storage_partitioning.py +++ b/tests/unit/test_validation_rule_storage_partitioning.py @@ -14,10 +14,13 @@ """Test Validation Rule Storage Partitioning""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.storage_partititioning import StoragePartitioning +LOG = logging.getLogger(__name__) + class TestRationalNetworkTrunking(object): 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) validator = StoragePartitioning() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert len(results) == 1 - assert msg.get('message') == 'Storage Partitioning' + assert len(message_list) == 1 assert msg.get('error') is False def test_storage_partitioning_unassigned_partition( @@ -50,11 +52,10 @@ class TestRationalNetworkTrunking(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = StoragePartitioning() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert len(results) == 1 - assert msg.get('message') == 'Storage Partitioning' + assert len(message_list) == 1 assert msg.get('error') is False 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) validator = StoragePartitioning() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design) - regex = re.compile( - 'Storage Partitioning Error: A volume group must be assigned to a storage device or ' - 'partition; volume group .+ on BaremetalNode .+') + regex = re.compile('Volume group .+ not assigned any physical volumes') - for msg in results: + for msg in message_list: msg = msg.to_dict() + LOG.debug(msg) + assert len(msg.get('documents')) > 0 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 diff --git a/tests/unit/test_validation_rule_storage_sizing.py b/tests/unit/test_validation_rule_storage_sizing.py index 267ad75f..2bc4aa86 100644 --- a/tests/unit/test_validation_rule_storage_sizing.py +++ b/tests/unit/test_validation_rule_storage_sizing.py @@ -14,10 +14,13 @@ """Test Validation Rule Rational Network Trunking""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.storage_sizing import StorageSizing +LOG = logging.getLogger(__name__) + class TestStorageSizing(object): 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) validator = StorageSizing() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert len(results) == 1 - assert msg.get('message') == 'Storage Sizing' + assert len(message_list) == 1 assert msg.get('error') is False 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) validator = StorageSizing() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) regex = re.compile( - 'Storage Sizing Error: Storage .+ size is < 0 on Baremetal Node .+' - ) - regex_1 = re.compile( - 'Storage Sizing Error: Storage .+ size is greater than 99 on Baremetal Node .+' - ) + '(Storage partition)|(Logical Volume) .+ size is < 0') + regex_1 = re.compile('greater than 99%') - assert len(results) == 6 - for msg in results: + assert len(message_list) == 6 + for msg in message_list: msg = msg.to_dict() - assert regex.match( - msg.get('message')) is not None or regex_1.match( + LOG.debug(msg) + assert regex.search( + msg.get('message')) is not None or regex_1.search( msg.get('message')) is not None assert msg.get('error') is True diff --git a/tests/unit/test_validation_rule_unique_network.py b/tests/unit/test_validation_rule_unique_network.py index fe45c59d..65dd8887 100644 --- a/tests/unit/test_validation_rule_unique_network.py +++ b/tests/unit/test_validation_rule_unique_network.py @@ -14,10 +14,13 @@ """Test Validation Rule Unique Network""" import re +import logging from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.unique_network_check import UniqueNetworkCheck +LOG = logging.getLogger(__name__) + class TestUniqueNetwork(object): 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) validator = UniqueNetworkCheck() - results, message_list = validator.execute(site_design) - msg = results[0].to_dict() + message_list = validator.execute(site_design, orchestrator=orch) + msg = message_list[0].to_dict() - assert msg.get('message') == 'Unique Network' assert msg.get('error') is False - assert len(results) == 1 + assert len(message_list) == 1 def test_invalid_unique_network(self, mocker, deckhand_ingester, drydock_state, input_files): @@ -51,15 +53,22 @@ class TestUniqueNetwork(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = UniqueNetworkCheck() - results, message_list = validator.execute(site_design) + message_list = validator.execute(site_design, orchestrator=orch) 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: - msg = msg.to_dict() - assert msg.get('error') - assert regex.match(msg.get('message')) is not None + assert len(message_list) >= 2 - 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')) + ]) diff --git a/tests/unit/test_validation_rule_valid_platform.py b/tests/unit/test_validation_rule_valid_platform.py index 62b4aa1a..18676f92 100644 --- a/tests/unit/test_validation_rule_valid_platform.py +++ b/tests/unit/test_validation_rule_valid_platform.py @@ -13,11 +13,15 @@ # limitations under the License. """Test Validation Rule Rational Boot Storage""" +import logging + import drydock_provisioner.config as config from drydock_provisioner.orchestrator.orchestrator import Orchestrator from drydock_provisioner.orchestrator.validations.platform_selection import PlatformSelection +LOG = logging.getLogger(__name__) + class TestValidPlatform(object): 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) validator = PlatformSelection() - results, message_list = validator.execute( - site_design, orchestrator=orch) - for r in results: - print(r.to_dict()) + message_list = validator.execute(site_design, orchestrator=orch) - 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 len(results) == 1 + assert len(message_list) == 1 def test_invalid_platform(self, mocker, deckhand_ingester, drydock_state, input_files): @@ -75,13 +78,13 @@ class TestValidPlatform(object): status, site_design = Orchestrator.get_effective_site(orch, design_ref) validator = PlatformSelection() - results, message_list = validator.execute( - site_design, orchestrator=orch) + message_list = validator.execute(site_design, orchestrator=orch) - for r in results: - print(r.to_dict()) + for r in message_list: + 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 msg.get('error') - assert len(results) == 1 + assert len(msg.get('documents')) > 0 + assert len(message_list) == 1 diff --git a/tests/yaml_samples/deckhand_fullsite.yaml b/tests/yaml_samples/deckhand_fullsite.yaml index 555178cf..ea28c929 100644 --- a/tests/yaml_samples/deckhand_fullsite.yaml +++ b/tests/yaml_samples/deckhand_fullsite.yaml @@ -93,6 +93,7 @@ data: default_network: mgmt allowed_networks: - public + - private - mgmt --- schema: 'drydock/Rack/v1' diff --git a/tests/yaml_samples/invalid_unique_network.yaml b/tests/yaml_samples/invalid_unique_network.yaml index 28239418..050fd5aa 100644 --- a/tests/yaml_samples/invalid_unique_network.yaml +++ b/tests/yaml_samples/invalid_unique_network.yaml @@ -308,6 +308,10 @@ data: networks: - mgmt - private +############ +### This fails here +############ + - foo metadata: tags: - 'test' diff --git a/tests/yaml_samples/validation.yaml b/tests/yaml_samples/validation.yaml index 7f38685b..6def063c 100644 --- a/tests/yaml_samples/validation.yaml +++ b/tests/yaml_samples/validation.yaml @@ -93,6 +93,7 @@ data: default_network: mgmt allowed_networks: - public + - private - mgmt --- schema: 'drydock/Rack/v1' diff --git a/tox.ini b/tox.ini index 9842b2d1..8098f8cd 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ commands = flake8 \ commands = bandit -r drydock_provisioner -n 5 [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/ max-line-length=119