Exemple #1
0
class InstanceValidator:
    def __init__(self):
        self.instance_table = InstanceTable()
        self.model_table = ModelTable()
        self.rack_table = RackTable()
        self.user_table = UserTable()
        self.dc_table = DatacenterTable()
        self.rack_height = 42

    def create_instance_validation(self, instance, queue=None):
        basic_val_result = self.basic_validations(instance, -1)
        if basic_val_result != Constants.API_SUCCESS:
            return basic_val_result

        model_template = self.model_table.get_model(instance.model_id)
        if model_template is None:
            return "The model does not exist."

        dc_template = self.dc_table.get_datacenter(instance.datacenter_id)
        if dc_template is None:
            return "The datacenter does not exist."

        # Check that connections are only within a single datacenter
        net_cons = instance.network_connections
        for port in net_cons.keys():
            dest_hostname = net_cons[port][Constants.CONNECTION_HOSTNAME]
            if dest_hostname != "" and dest_hostname is not None:
                dc_id = (InstanceTable().get_instance_by_hostname(
                    dest_hostname).datacenter_id)
                if dc_id != instance.datacenter_id:
                    raise InvalidInputsError(
                        "Network connections cannot span multiple datacenters")

        if dc_template.is_offline_storage:
            return Constants.API_SUCCESS

        if instance.mount_type == Constants.BLADE_KEY:
            return self.blade_validation(instance, -1, queue)
        else:
            return self.rackmount_validation(instance, -1, model_template,
                                             dc_template)

    def edit_instance_validation(self, instance, original_asset_number):
        basic_val_result = self.basic_validations(instance,
                                                  original_asset_number)
        if basic_val_result != Constants.API_SUCCESS:
            return basic_val_result

        model_template = self.model_table.get_model(instance.model_id)
        if model_template is None:
            return "The model does not exist."

        dc_template = self.dc_table.get_datacenter(instance.datacenter_id)
        if dc_template is None:
            return "The datacenter does not exist."

        # Check that connections are only within a single datacenter
        net_cons = instance.network_connections
        for port in net_cons.keys():
            dest_hostname = net_cons[port][Constants.CONNECTION_HOSTNAME]
            if dest_hostname != "" and dest_hostname is not None:
                dc_id = (InstanceTable().get_instance_by_hostname(
                    dest_hostname).datacenter_id)
                if dc_id != instance.datacenter_id:
                    raise InvalidInputsError(
                        "Network connections cannot span multiple datacenters")

        if dc_template.is_offline_storage:
            return Constants.API_SUCCESS

        if instance.mount_type == Constants.BLADE_KEY:
            return self.blade_validation(instance, original_asset_number)
        else:
            return self.rackmount_validation(instance, original_asset_number,
                                             model_template, dc_template)

        return Constants.API_SUCCESS

    def basic_validations(self, instance, original_asset_number):
        if (instance.asset_number != original_asset_number
                and original_asset_number != -1):
            asset_pattern = re.compile("[0-9]{6}")
            if asset_pattern.fullmatch(str(instance.asset_number)) is None:
                return "Asset numbers must be 6 digits long and only contain numbers."
            if (self.instance_table.get_instance_by_asset_number(
                    instance.asset_number) is not None):
                return f"Asset numbers must be unique. An asset with asset number {instance.asset_number} already exists."

        if instance.hostname != "" and instance.hostname is not None:
            duplicate_hostname = self.instance_table.get_instance_by_hostname(
                instance.hostname)

            if duplicate_hostname is not None:
                if duplicate_hostname.asset_number != original_asset_number:
                    print("DUPLICATE NUMBER", duplicate_hostname.asset_number)
                    print("ORIGINAL NUMBER", original_asset_number)
                    return f"An asset with hostname {duplicate_hostname.hostname} already exists. Please provide a unique hostname."

            if len(instance.hostname) > 64:
                return "Hostnames must be 64 characters or less"

            host_pattern = re.compile("[a-zA-Z]*[A-Za-z0-9-]*[A-Za-z0-9]")
            if host_pattern.fullmatch(instance.hostname) is None:
                return "Hostnames must start with a letter, only contain letters, numbers, periods, and hyphens, and end with a letter or number."

        if instance.owner != "" and self.user_table.get_user(
                instance.owner) is None:
            return f"The owner {instance.owner} is not an existing user. Please enter the username of an existing user."

        return Constants.API_SUCCESS

    def rackmount_validation(self, instance, original_asset_number,
                             model_template, dc_template):
        if instance.mount_type == Constants.CHASIS_KEY and instance.hostname == "":
            if (self.instance_table.get_blades_by_chassis_hostname(
                    instance.hostname) is not None):
                return "Blade chassis with blades require a hostname."

        rack = self.rack_table.get_rack(instance.rack_label,
                                        instance.datacenter_id)
        if rack is None and not dc_template.is_offline_storage:
            return f"Rack {instance.rack_label} does not exist in datacenter {dc_template}. Assets must be created on preexisting racks"

        p_connection_set = set()
        for p_connection in instance.power_connections:
            if p_connection in p_connection_set:
                return "Cannot connect two asset power ports to the same PDU port."
            else:
                p_connection_set.add(p_connection)
            char1 = p_connection[0].upper()
            print("PRINGINDISNFISNF")
            print(p_connection)
            num = int(p_connection[1:])
            if char1 == "L":
                pdu_arr = rack.pdu_left
            elif char1 == "R":
                pdu_arr = rack.pdu_right
            else:
                return "Invalid power connection. Please specify left or right PDU."

            if pdu_arr[num - 1] == 1:
                return f"There is already an asset connected at PDU {char1}{num}. Please pick an empty PDU port."

        pattern = re.compile("[0-9]+")
        if pattern.fullmatch(str(instance.rack_position)) is None:
            return "The value for Rack U must be a positive integer."

        instance_bottom = int(instance.rack_position)
        instance_top = instance_bottom + int(model_template.height) - 1

        if instance_top > self.rack_height:
            return "The placement of the instance exceeds the height of the rack."

        instance_list = self.instance_table.get_instances_by_rack(
            instance.rack_label, instance.datacenter_id)
        if instance_list is not None:
            for current_instance in instance_list:
                if current_instance.asset_number == original_asset_number:
                    continue

                model = self.model_table.get_model(current_instance.model_id)
                current_instance_top = current_instance.rack_position + model.height - 1
                if (current_instance.rack_position >= instance_bottom
                        and current_instance.rack_position <= instance_top):
                    return self.return_conflict(current_instance)
                elif (current_instance_top >= instance_bottom
                      and current_instance_top <= instance_top):
                    return self.return_conflict(current_instance)

        connection_validation_result = self.validate_connections(
            instance.network_connections, instance.hostname)
        if connection_validation_result != Constants.API_SUCCESS:
            return connection_validation_result

        return Constants.API_SUCCESS

    def blade_validation(self, instance, original_asset_number, queue=None):
        blade_chassis = self.instance_table.get_instance_by_hostname(
            instance.chassis_hostname)

        print("\n\n")
        print("CHECKING THE QUEUE")
        #  In the case of import instance, need to check queue of instances as well as db
        if queue is not None and blade_chassis is None:
            for item in queue:
                print(item)
                if item.hostname == instance.chassis_hostname:
                    blade_chassis = item

        print("BLADE CHASSIS")
        print(blade_chassis)

        if blade_chassis is None:
            return f"No blade chassis exists with hostname {instance.chassis_hostname}."
        elif blade_chassis.mount_type != Constants.CHASIS_KEY:
            return f"Asset with hostname {instance.chassis_hostname} is not a blade chassis. Blades can only be installed in a blade chassis."

        if blade_chassis.datacenter_id != instance.datacenter_id:
            return f"The blade chassis selected is in datacenter {blade_chassis.datacenter_id}. A blade and its chassis must be in the same datacenter."

        if instance.chassis_slot > 14 or instance.chassis_slot < 1:
            return f"A blade chassis contains 14 slots. Please provide a position between 1 and 14."

        blade_conflict = self.instance_table.get_blade_by_chassis_and_slot(
            instance.chassis_hostname, instance.chassis_slot)
        if (blade_conflict is not None
                and blade_conflict.asset_number != original_asset_number):
            return f"Blade with asset number {blade_conflict.asset_number} already exists at slot {instance.chassis_slot} of chassis with hostname {instance.chassis_hostname}."

        return Constants.API_SUCCESS

    def validate_connections(self, network_connections, hostname):
        # print("validating connections")
        result = ""
        new_connections = {}
        for my_port in network_connections:
            mac_adress = network_connections[my_port][
                Constants.MAC_ADDRESS_KEY]
            connection_hostname = network_connections[my_port][
                "connection_hostname"]
            connection_port = network_connections[my_port]["connection_port"]

            if connection_hostname in new_connections.keys():
                if new_connections[connection_hostname] == connection_port:
                    result += "Cannot make two network connections to the same port."
            elif connection_hostname != "" and connection_port != "":
                new_connections[connection_hostname] = connection_port

            mac_pattern = re.compile(
                "[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}"
            )
            if mac_adress != "" and mac_pattern.fullmatch(
                    mac_adress.lower()) is None:
                result += f"Invalid MAC address for port {my_port}. MAC addresses must be 6 byte, colon separated hexidecimal strings (i.e. a1:b2:c3:d4:e5:f6). \n"

            if (connection_hostname == "" or connection_hostname is None) and (
                    connection_port == "" or connection_port is None):
                continue

            if not (connection_hostname != "" and connection_port != ""):
                result += "Connections require both a hostname and connection port."

            other_instance = self.instance_table.get_instance_by_hostname(
                connection_hostname)
            if other_instance is None:
                result += f"The asset with hostname {connection_hostname} does not exist. Connections must be between assets with existing hostnames. \n"
                continue

            if connection_port in other_instance.network_connections:
                if (other_instance.network_connections[connection_port]
                    ["connection_port"] != my_port) and (
                        other_instance.network_connections[connection_port]
                        ["connection_port"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is connected to another asset. \n"
                    continue
                if (other_instance.network_connections[connection_port]
                    ["connection_hostname"] != hostname) and (
                        other_instance.network_connections[connection_port]
                        ["connection_hostname"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is already connected to another asset. \n"

        # print("finished connection validation")
        if result == "":
            return Constants.API_SUCCESS
        else:
            return result

    def return_conflict(self, current_instance):
        result = f"The asset placement conflicts with asset with asset number {current_instance.asset_number} "
        result += f"on rack {current_instance.rack_label} at height U{current_instance.rack_position}."
        return result
Exemple #2
0
class ChangePlanValidator:
    def __init__(self):
        self.instance_table = InstanceTable()
        self.model_table = ModelTable()
        self.rack_table = RackTable()
        self.user_table = UserTable()
        self.dc_table = DatacenterTable()
        self.cp_table = ChangePlanTable()
        self.cp_action_table = ChangePlanActionTable()
        self.instance_manager = InstanceManager()
        self.rack_height = 42

        self.cp_asset_set = set()
        self.decom_asset_set = set()
        self.no_pow_conflict = False

    def validate_action(self, cp_action, all_cp_actions):
        self.cp_asset_set = set()
        self.decom_asset_set = set()
        self.no_pow_conflict = False
        cp_validation_result = self._validate_change_plan(
            cp_action.change_plan_id)
        if cp_validation_result != Constants.API_SUCCESS:
            return cp_validation_result

        if cp_action.action == Constants.CREATE_KEY:
            return self._create_action_validate(cp_action, all_cp_actions)
        elif cp_action.action == Constants.UPDATE_KEY:
            return self._edit_action_validate(cp_action, all_cp_actions)
        elif cp_action.action == Constants.DECOMMISSION_KEY:
            return self._decom_action_validate(cp_action, all_cp_actions)
        else:
            return Constants.API_SUCCESS

    def _validate_change_plan(self, cp_id: int):
        change_plan = self.cp_table.get_change_plan(cp_id)
        if change_plan is None:
            return "Change plan actions must correspond to an existing change plan."

        if change_plan.executed:
            return "Cannot modify a change plan that has already been executed"

        return Constants.API_SUCCESS

    def _create_action_validate(self, cp_action, all_cp_actions):
        instance = self.instance_manager.make_instance(cp_action.new_record)
        datacenter = self.dc_table.get_datacenter(instance.datacenter_id)
        if datacenter is None:
            return "The datacenter does not exist. Please select a valid datacenter."

        input_validation_result = self._validate_inputs(instance, datacenter)
        if input_validation_result != Constants.API_SUCCESS:
            return input_validation_result

        model_template = self.model_table.get_model(instance.model_id)
        if model_template is None:
            return "The model does not exist."

        if (instance.mount_type !=
                Constants.BLADE_KEY) and (not datacenter.is_offline_storage):
            instance_bottom = int(instance.rack_position)
            instance_top = instance_bottom + int(model_template.height) - 1

            if instance_top > self.rack_height:
                return "The placement of the instance exceeds the height of the rack."
        else:
            instance_bottom = 0
            instance_top = 0

        new_asset_number = instance.asset_number
        new_hostname = instance.hostname
        pow_connections = set(instance.power_connections)

        prev_action_val_result = self._validate_vs_prev_actions(
            instance,
            all_cp_actions,
            cp_action,
            new_asset_number,
            new_hostname,
            pow_connections,
            instance_bottom,
            instance_top,
            datacenter,
        )
        if prev_action_val_result != Constants.API_SUCCESS:
            return prev_action_val_result

        common_val_result = self._common_validations(instance, cp_action,
                                                     instance_bottom,
                                                     instance_top, datacenter)
        if common_val_result != Constants.API_SUCCESS:
            return common_val_result

        if (not datacenter.is_offline_storage) and (instance.mount_type !=
                                                    Constants.BLADE_KEY):
            if not self.no_pow_conflict:
                rack = self.rack_table.get_rack(instance.rack_label,
                                                instance.datacenter_id)
                for p_connection in instance.power_connections:
                    char1 = p_connection[0].upper()
                    num = int(p_connection[1:])
                    if char1 == "L":
                        pdu_arr = rack.pdu_left
                    elif char1 == "R":
                        pdu_arr = rack.pdu_right
                    else:
                        return "Invalid power connection. Please specify left or right PDU."

                    if pdu_arr[num - 1] == 1:
                        return f"There is already an asset connected at PDU {char1}{num}. Please pick an empty PDU port."

            connection_validation_result = self._validate_connections(
                instance.network_connections,
                instance.hostname,
                cp_action,
                all_cp_actions,
            )
            if connection_validation_result != Constants.API_SUCCESS:
                return connection_validation_result

        return Constants.API_SUCCESS

    def _edit_action_validate(self, cp_action, all_cp_actions):
        instance = self.instance_manager.make_instance(cp_action.new_record)
        datacenter = self.dc_table.get_datacenter(instance.datacenter_id)
        if datacenter is None:
            return "The datacenter does not exist. Please select a valid datacenter."

        input_validation_result = self._validate_inputs(instance, datacenter)
        if input_validation_result != Constants.API_SUCCESS:
            return input_validation_result

        model_template = self.model_table.get_model(instance.model_id)
        if model_template is None:
            return "The model does not exist."

        if (instance.mount_type !=
                Constants.BLADE_KEY) and (not datacenter.is_offline_storage):
            instance_bottom = int(instance.rack_position)
            instance_top = instance_bottom + int(model_template.height) - 1

            if instance_top > self.rack_height:
                return "The placement of the instance exceeds the height of the rack."
        else:
            instance_bottom = 0
            instance_top = 0

        prev_update_in_plan = self.cp_action_table.get_newest_asset_record_in_plan(
            cp_action.change_plan_id,
            cp_action.original_asset_number,
            999999999,
        )
        if (prev_update_in_plan is not None
                and prev_update_in_plan.action != Constants.COLLATERAL_KEY
                and prev_update_in_plan.step != cp_action.step):
            return f"This asset is already being modified in step {prev_update_in_plan.step}. Please update your desired information there."

        new_asset_number = instance.asset_number
        new_hostname = instance.hostname
        pow_connections = set(instance.power_connections)

        prev_action_val_result = self._validate_vs_prev_actions(
            instance,
            all_cp_actions,
            cp_action,
            new_asset_number,
            new_hostname,
            pow_connections,
            instance_bottom,
            instance_top,
            datacenter,
        )
        if prev_action_val_result != Constants.API_SUCCESS:
            return prev_action_val_result

        common_val_result = self._common_validations(instance, cp_action,
                                                     instance_bottom,
                                                     instance_top, datacenter)
        if common_val_result != Constants.API_SUCCESS:
            return common_val_result

        if (not datacenter.is_offline_storage) and (instance.mount_type !=
                                                    Constants.BLADE_KEY):
            if prev_update_in_plan is not None:
                prev_connections = prev_update_in_plan.new_record.get(
                    Constants.NETWORK_CONNECTIONS_KEY)
            else:
                prev_instance = self.instance_table.get_instance_by_asset_number(
                    cp_action.original_asset_number)
                prev_connections = prev_instance.network_connections

            connection_validation_result = self._validate_connections_edit(
                instance.network_connections,
                instance.hostname,
                cp_action,
                all_cp_actions,
                prev_connections,
            )
            if connection_validation_result != Constants.API_SUCCESS:
                return connection_validation_result

        return Constants.API_SUCCESS

    def _decom_action_validate(self, cp_action, all_cp_actions):
        asset = self.instance_table.get_instance_by_asset_number(
            cp_action.original_asset_number)
        if asset is None:
            return f"Asset with asset number {cp_action.original_asset_number} does not exist."

        prev_update_in_plan = self.cp_action_table.get_newest_asset_record_in_plan(
            cp_action.change_plan_id,
            cp_action.original_asset_number,
            999999999,
        )
        if (prev_update_in_plan is not None
                and prev_update_in_plan.action != Constants.COLLATERAL_KEY
                and prev_update_in_plan.step != cp_action.step):
            return f"This asset is already being modified in step {prev_update_in_plan.step}. Please update your desired information there."

        if asset.mount_type == Constants.CHASIS_KEY:
            no_conflict = set()
            for a in all_cp_actions:
                if a.new_record.get(
                        Constants.CHASSIS_HOSTNAME_KEY) == asset.hostname:
                    return f"A blade chassis must be empty before it can be decommissioned. A blade is placed in the chassis in step {a.step} of the change plan."
                else:
                    no_conflict.add(a.original_asset_number)

            blade_list = self.instance_table.get_blades_by_chassis_hostname(
                asset.hostname)
            if blade_list is not None:
                for blade in blade_list:
                    if not blade.asset_number in no_conflict:
                        return f"A blade chassis must be empty before it can be decommissioned. Blade with asset number {blade.asset_number} is in the chassis."

        return Constants.API_SUCCESS

    def _validate_inputs(self, instance, datacenter):
        if (not datacenter.is_offline_storage) and (instance.mount_type !=
                                                    Constants.BLADE_KEY):
            rack = self.rack_table.get_rack(instance.rack_label,
                                            instance.datacenter_id)
            if rack is None:
                return f"Rack {instance.rack_label} does not exist in datacenter {datacenter.name}. Assets must be created on preexisting racks"

            rack_u_pattern = re.compile("[0-9]+")
            if rack_u_pattern.fullmatch(str(instance.rack_position)) is None:
                return "The value for Rack U must be a positive integer."

        asset_pattern = re.compile("[0-9]{6}")
        if asset_pattern.fullmatch(str(instance.asset_number)) is None:
            return "Asset numbers must be 6 digits long and only contain numbers."

        if instance.hostname is not None and instance.hostname != "":
            if len(instance.hostname) > 64:
                return "Hostnames must be 64 characters or less"

            host_pattern = re.compile("[a-zA-Z]*[A-Za-z0-9-]*[A-Za-z0-9]")
            if host_pattern.fullmatch(instance.hostname) is None:
                return "Hostnames must start with a letter, only contain letters, numbers, periods, and hyphens, and end with a letter or number."

        if instance.owner != "" and self.user_table.get_user(
                instance.owner) is None:
            return f"The owner {instance.owner} is not an existing user. Please enter the username of an existing user."

        return Constants.API_SUCCESS

    def _validate_vs_prev_actions(
        self,
        instance,
        all_cp_actions,
        cp_action,
        new_asset_number,
        new_hostname,
        pow_connections,
        instance_bottom: int,
        instance_top: int,
        datacenter,
    ):
        for prev_action in all_cp_actions:
            if prev_action.step >= cp_action.step:
                continue

            if prev_action.action != Constants.CREATE_KEY:
                self.cp_asset_set.add(prev_action.original_asset_number)
            if prev_action.action == Constants.DECOMMISSION_KEY:
                self.decom_asset_set.add(prev_action.original_asset_number)

            # Asset Number Validation
            if (prev_action.original_asset_number == new_asset_number
                    and prev_action.action != Constants.DECOMMISSION_KEY):
                return f"Asset numbers must be unique. An asset with asset number {instance.asset_number} already exists."

            if (prev_action.new_record.get(
                    Constants.ASSET_NUMBER_KEY) == new_asset_number
                    and prev_action.action == Constants.CREATE_KEY):
                return f"Asset with asset number {new_asset_number} is already being created in step {prev_action.step} of the change plan."

            # Hostname Validation
            if new_hostname is not None and new_hostname != "":
                if (prev_action.new_record.get(
                        Constants.HOSTNAME_KEY) == new_hostname
                        and prev_action.action != Constants.DECOMMISSION_KEY):
                    return f"Asset with hostname {new_hostname} already exists in step {prev_action.step} of the change plan."

            # Location Validation
            if not datacenter.is_offline_storage:
                if instance.mount_type != Constants.BLADE_KEY:
                    if (prev_action.action == Constants.CREATE_KEY
                            or prev_action.action == Constants.UPDATE_KEY):
                        model_id = self.instance_manager.get_model_id_from_name(
                            prev_action.new_record.get(Constants.MODEL_KEY))
                        model = self.model_table.get_model(model_id)
                        other_bottom = int(
                            prev_action.new_record.get(
                                Constants.RACK_POSITION_KEY))
                        other_top = (int(
                            prev_action.new_record.get(
                                Constants.RACK_POSITION_KEY)) + model.height -
                                     1)
                        if (other_bottom >= instance_bottom
                                and other_bottom <= instance_top):
                            result = f"The asset placement conflicts with asset with asset number {prev_action.new_record.get(Constants.ASSET_NUMBER_KEY)} "
                            result += f"edited in step {prev_action.step}."
                            return result
                        elif other_top >= instance_bottom and other_top <= instance_top:
                            result = f"The asset placement conflicts with asset with asset number {prev_action.new_record.get(Constants.ASSET_NUMBER_KEY)} "
                            result += f"edited in step {prev_action.step}."
                            return result
                elif (instance.mount_type == Constants.BLADE_KEY
                      and prev_action.new_record.get(
                          Constants.MOUNT_TYPE_KEY) == Constants.BLADE_KEY):
                    if instance.chassis_hostname == prev_action.new_record.get(
                            Constants.CHASSIS_HOSTNAME_KEY
                    ) and instance.chassis_slot == prev_action.new_record.get(
                            Constants.CHASSIS_SLOT_KEY):
                        return f"A blade is already being placed in chassis with hostname {instance.chassis_hostname} at position {instance.chassis_slot} in step {prev_action.step} of the change plan."

            # Power Connection Validation
            if (not datacenter.is_offline_storage) and (instance.mount_type !=
                                                        Constants.BLADE_KEY):
                if (prev_action.action != Constants.DECOMMISSION_KEY
                        and prev_action.action != Constants.COLLATERAL_KEY):
                    prev_pow_connections = prev_action.new_record.get(
                        Constants.POWER_CONNECTIONS_KEY)
                    print(prev_action.new_record)
                    prev_pow_connections = set(prev_pow_connections)
                    pow_intersection = pow_connections.intersection(
                        prev_pow_connections)
                    if len(pow_intersection) > 0:
                        return f"There is already an asset connected at PDU port {pow_intersection.pop()}. Please pick an empty PDU port."

                if (prev_action.action == Constants.UPDATE_KEY
                        or prev_action.action == Constants.DECOMMISSION_KEY):
                    old_pow_connections = prev_action.old_record.get(
                        Constants.POWER_CONNECTIONS_KEY)
                    if old_pow_connections is None:
                        self.no_pow_conflict = True
                    else:
                        old_pow_connections = set(old_pow_connections)
                        old_pow_intersection = pow_connections.intersection(
                            old_pow_connections)
                        if len(old_pow_intersection) > 0:
                            self.no_pow_conflict = True

        return Constants.API_SUCCESS

    def _common_validations(self, instance, cp_action, instance_bottom,
                            instance_top, datacenter):
        if (self.instance_table.get_instance_by_asset_number(
                instance.asset_number) is not None
                and not instance.asset_number in self.decom_asset_set
                and instance.asset_number != cp_action.original_asset_number):
            return f"Asset numbers must be unique. An asset with asset number {instance.asset_number} already exists."

        if instance.hostname != "" and instance.hostname is not None:
            duplicate_hostname = self.instance_table.get_instance_by_hostname(
                instance.hostname)

            if (duplicate_hostname is not None and
                    not duplicate_hostname.asset_number in self.cp_asset_set
                    and
                    duplicate_hostname.asset_number != instance.asset_number):
                return f"An asset with hostname {duplicate_hostname.hostname} exists at location {duplicate_hostname.rack_label} U{duplicate_hostname.rack_position}"

        if not datacenter.is_offline_storage:
            if instance.mount_type != Constants.BLADE_KEY:
                instance_list = self.instance_table.get_instances_by_rack(
                    instance.rack_label, instance.datacenter_id)
                if instance_list is not None:
                    for current_instance in instance_list:
                        if (current_instance.asset_number in self.cp_asset_set
                                or current_instance.asset_number
                                == cp_action.original_asset_number):
                            continue

                        model = self.model_table.get_model(instance.model_id)
                        current_instance_top = (
                            current_instance.rack_position + model.height - 1)
                        if (current_instance.rack_position >= instance_bottom
                                and current_instance.rack_position <=
                                instance_top):
                            return self.return_conflict(current_instance)
                        elif (current_instance_top >= instance_bottom
                              and current_instance_top <= instance_top):
                            return self.return_conflict(current_instance)
            elif instance.mount_type == Constants.BLADE_KEY:
                blade_conflict = self.instance_table.get_blade_by_chassis_and_slot(
                    instance.chassis_hostname, instance.chassis_slot)
                if (blade_conflict is not None and
                        not blade_conflict.asset_number in self.cp_asset_set):
                    return f"A blade with hostname {blade_conflict.hostname} is already located in position {instance.chassis_slot} of chassis with hostname {instance.chassis_hostname}."

        return Constants.API_SUCCESS

    def _validate_connections(self, network_connections, hostname, cp_action,
                              all_cp_actions):
        result = ""
        new_connections = {}
        for my_port in network_connections:
            mac_adress = network_connections[my_port][
                Constants.MAC_ADDRESS_KEY]
            connection_hostname = network_connections[my_port][
                "connection_hostname"]
            connection_port = network_connections[my_port]["connection_port"]

            if connection_hostname in new_connections.keys():
                if new_connections[connection_hostname] == connection_port:
                    result += "Cannot make two network connections to the same port."
            elif connection_hostname != "" and connection_port != "":
                new_connections[connection_hostname] = connection_port

            mac_pattern = re.compile(
                "[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}"
            )
            if mac_adress != "" and mac_pattern.fullmatch(
                    mac_adress.lower()) is None:
                result += f"Invalid MAC address for port {my_port}. MAC addresses must be 6 byte, colon separated hexidecimal strings (i.e. a1:b2:c3:d4:e5:f6). \n"

            if (connection_hostname == "" or connection_hostname is None) and (
                    connection_port == "" or connection_port is None):
                continue

            if not (connection_hostname != "" and connection_port != ""):
                result += "Connections require both a hostname and connection port."

            other_instance = None
            for prev_action in all_cp_actions:
                if prev_action.step >= cp_action.step:
                    continue

                if (prev_action.new_record[Constants.HOSTNAME_KEY] ==
                        connection_hostname):
                    other_instance = self.instance_manager.make_instance(
                        prev_action.new_record)
            if other_instance is None:
                other_instance = self.instance_table.get_instance_by_hostname(
                    connection_hostname)
                if other_instance is None:
                    result += f"The asset with hostname {connection_hostname} does not exist. Connections must be between assets with existing hostnames. \n"
                    continue

            if connection_port in other_instance.network_connections:
                if (other_instance.network_connections[connection_port]
                    ["connection_port"] != my_port) and (
                        other_instance.network_connections[connection_port]
                        ["connection_port"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is connected to another asset. \n"
                    continue
                if (other_instance.network_connections[connection_port]
                    ["connection_hostname"] != hostname) and (
                        other_instance.network_connections[connection_port]
                        ["connection_hostname"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is already connected to another asset."

        if result == "":
            return Constants.API_SUCCESS
        else:
            return result

    def _validate_connections_edit(self, network_connections, hostname,
                                   cp_action, all_cp_actions,
                                   prev_connections):
        result = ""
        new_connections = {}
        for my_port in network_connections:
            mac_adress = network_connections[my_port][
                Constants.MAC_ADDRESS_KEY]
            connection_hostname = network_connections[my_port][
                "connection_hostname"]
            connection_port = network_connections[my_port]["connection_port"]

            if connection_hostname in new_connections.keys():
                if new_connections[connection_hostname] == connection_port:
                    result += "Cannot make two network connections to the same port."
            elif connection_hostname != "" and connection_port != "":
                new_connections[connection_hostname] = connection_port

            mac_pattern = re.compile(
                "[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}"
            )
            if mac_adress != "" and mac_pattern.fullmatch(
                    mac_adress.lower()) is None:
                result += f"Invalid MAC address for port {my_port}. MAC addresses must be 6 byte, colon separated hexidecimal strings (i.e. a1:b2:c3:d4:e5:f6). \n"

            if (connection_hostname == "" or connection_hostname is None) and (
                    connection_port == "" or connection_port is None):
                continue

            if (connection_hostname
                    == prev_connections[my_port]["connection_hostname"]
                    and connection_port
                    == prev_connections[my_port]["connection_port"]):
                continue

            if not (connection_hostname != "" and connection_port != ""):
                result += "Connections require both a hostname and connection port."

            other_instance = None
            for prev_action in all_cp_actions:
                if prev_action.step >= cp_action.step:
                    continue

                if (prev_action.new_record[Constants.HOSTNAME_KEY] ==
                        connection_hostname):
                    other_instance = self.instance_manager.make_instance(
                        prev_action.new_record)
            if other_instance is None:
                other_instance = self.instance_table.get_instance_by_hostname(
                    connection_hostname)
                if other_instance is None:
                    result += f"The asset with hostname {connection_hostname} does not exist. Connections must be between assets with existing hostnames. \n"
                    continue

            if connection_port in other_instance.network_connections:
                if (other_instance.network_connections[connection_port]
                    ["connection_port"] != my_port) and (
                        other_instance.network_connections[connection_port]
                        ["connection_port"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is connected to another asset. \n"
                    continue
                if (other_instance.network_connections[connection_port]
                    ["connection_hostname"] != hostname) and (
                        other_instance.network_connections[connection_port]
                        ["connection_hostname"] != ""):
                    result += f"The port {connection_port} on asset with hostname {connection_hostname} is already connected to another asset."

        if result == "":
            return Constants.API_SUCCESS
        else:
            return result

    def return_conflict(self, current_instance):
        result = f"The asset placement conflicts with asset with asset number {current_instance.asset_number} "
        result += f"on rack {current_instance.rack_label} at height U{current_instance.rack_position}."
        return result
class StatsManager:
    def __init__(self):
        self.dc_table = DatacenterTable()
        self.instance_table = InstanceTable()
        self.model_table = ModelTable()
        self.rack_table = RackTable()

        self.space_by_rack = {}
        self.space_by_vendor = {}
        self.space_by_model = {}
        self.space_by_owner = {}

        self.rack_count = {}
        self.rack_height = 42

    def create_report(self, dc_name):
        self.reset_counters()

        if dc_name is None or dc_name == "":
            rack_list = self.rack_table.get_all_racks()
        else:
            dc_id = self.dc_table.get_datacenter_id_by_name(dc_name)
            rack_list = self.rack_table.get_rack_by_datacenter(dc_id)

        num_racks = len(rack_list)

        if num_racks == 0:
            raise ValueError(
                "Reports require existing racks. Please ensure that the datacenter exists and contains racks."
            )

        for rack in rack_list:
            instance_list = self.instance_table.get_instances_by_rack(
                rack.label, rack.datacenter_id)
            self.iterate_instance(instance_list, rack.label)

        total_space_used = 0
        for key in self.space_by_rack:
            total_space_used += self.space_by_rack[key]
            self.space_by_rack[key] = round(
                (self.space_by_rack[key] /
                 (self.rack_count[key] * self.rack_height)) * 100,
                2,
            )

        all_space = num_racks * self.rack_height
        percent_total_used = round((total_space_used / all_space) * 100, 2)
        percent_total_free = 100 - percent_total_used

        self.space_by_vendor = self.divide_dict_by_space(
            self.space_by_vendor, all_space)
        self.space_by_model = self.divide_dict_by_space(
            self.space_by_model, all_space)
        self.space_by_owner = self.divide_dict_by_space(
            self.space_by_owner, all_space)

        rack_positionsage_json = json.dumps(self.space_by_rack, sort_keys=True)
        vendor_usage_json = json.dumps(self.space_by_vendor, sort_keys=True)
        model_usage_json = json.dumps(self.space_by_model, sort_keys=True)
        owner_usage_json = json.dumps(self.space_by_owner, sort_keys=True)

        returnJSON = {
            "totalUsage": percent_total_used,
            "totalFree": percent_total_free,
            "spaceUsage": rack_positionsage_json,
            "vendorUsage": vendor_usage_json,
            "modelUsage": model_usage_json,
            "ownerUsage": owner_usage_json,
        }

        return returnJSON

    def iterate_instance(self, instance_list, rack_label):
        rack_space_used = 0
        for instance in instance_list:
            model = self.model_table.get_model(instance.model_id)
            rack_space_used += model.height

            if model.vendor in self.space_by_vendor:
                self.space_by_vendor[model.vendor] += model.height
            else:
                self.space_by_vendor[model.vendor] = model.height

            model_name = model.vendor + " " + model.model_number
            if model_name in self.space_by_model:
                self.space_by_model[model_name] += model.height
            else:
                self.space_by_model[model_name] = model.height

            if instance.owner is None or instance.owner == "":
                owner = "No owner listed"
            else:
                owner = instance.owner
            if owner in self.space_by_owner:
                self.space_by_owner[owner] += model.height
            else:
                self.space_by_owner[owner] = model.height

        if rack_label in self.rack_count:
            self.space_by_rack[rack_label] += rack_space_used
            self.rack_count[rack_label] += 1
        else:
            self.space_by_rack[rack_label] = rack_space_used
            self.rack_count[rack_label] = 1

    def divide_dict_by_space(self, dictionary, total_space_used):
        for key in dictionary:
            dictionary[key] = round((dictionary[key] / total_space_used) * 100,
                                    2)

        return dictionary

    def reset_counters(self):
        self.space_by_rack = {}
        self.space_by_vendor = {}
        self.space_by_model = {}
        self.space_by_owner = {}
        self.rack_count = {}

        return