handle cpu, copper and sfp as options

This commit is contained in:
Josh Patterson
2025-02-26 17:58:09 -05:00
parent 52839e2a7d
commit 4e954c24f7
3 changed files with 102 additions and 43 deletions

View File

@@ -36,6 +36,7 @@ Configuration Files:
- Located at <base_path>/<hypervisorHostname>VMs - Located at <base_path>/<hypervisorHostname>VMs
- Contains array of VM configurations - Contains array of VM configurations
- Each VM config specifies hardware and network settings - Each VM config specifies hardware and network settings
- Hardware indices (disk, copper, sfp) must be specified as JSON arrays: "disk":["1","2"]
defaults.yaml: Hardware capabilities configuration defaults.yaml: Hardware capabilities configuration
- Located at /opt/so/saltstack/default/salt/hypervisor/defaults.yaml - Located at /opt/so/saltstack/default/salt/hypervisor/defaults.yaml
@@ -210,7 +211,6 @@ def read_yaml_file(file_path: str) -> dict:
except Exception as e: except Exception as e:
log.error("Failed to read YAML file %s: %s", file_path, str(e)) log.error("Failed to read YAML file %s: %s", file_path, str(e))
raise raise
def convert_pci_id(pci_id: str) -> str: def convert_pci_id(pci_id: str) -> str:
""" """
Convert PCI ID from pci_0000_c7_00_0 format to 0000:c7:00.0 format. Convert PCI ID from pci_0000_c7_00_0 format to 0000:c7:00.0 format.
@@ -241,6 +241,35 @@ def convert_pci_id(pci_id: str) -> str:
log.error("Failed to convert PCI ID %s: %s", pci_id, str(e)) log.error("Failed to convert PCI ID %s: %s", pci_id, str(e))
raise raise
def parse_hardware_indices(hw_value: Any) -> List[int]:
"""
Parse hardware indices from JSON array format.
Args:
hw_value: Hardware value which should be a list
Returns:
List of integer indices
"""
indices = []
if hw_value is None:
return indices
# If it's a list (expected format)
if isinstance(hw_value, list):
try:
indices = [int(x) for x in hw_value]
log.debug("Parsed hardware indices from list format: %s", indices)
except (ValueError, TypeError) as e:
log.error("Failed to parse hardware indices from list format: %s", str(e))
raise ValueError(f"Invalid hardware indices format in list: {hw_value}")
else:
log.warning("Unexpected type for hardware indices: %s", type(hw_value))
raise ValueError(f"Hardware indices must be in array format, got: {type(hw_value)}")
return indices
def get_hypervisor_model(hypervisor: str) -> str: def get_hypervisor_model(hypervisor: str) -> str:
"""Get sosmodel from hypervisor grains.""" """Get sosmodel from hypervisor grains."""
try: try:
@@ -318,7 +347,7 @@ def validate_hardware_request(model_config: dict, requested_hw: dict) -> Tuple[b
for hw_type in ['disk', 'copper', 'sfp']: for hw_type in ['disk', 'copper', 'sfp']:
if hw_type in requested_hw and requested_hw[hw_type]: if hw_type in requested_hw and requested_hw[hw_type]:
try: try:
indices = [int(x) for x in str(requested_hw[hw_type]).split('\n')] indices = parse_hardware_indices(requested_hw[hw_type])
log.debug("Checking if %s indices %s exist in model", hw_type, indices) log.debug("Checking if %s indices %s exist in model", hw_type, indices)
if hw_type not in model_config['hardware']: if hw_type not in model_config['hardware']:
@@ -333,8 +362,8 @@ def validate_hardware_request(model_config: dict, requested_hw: dict) -> Tuple[b
if invalid_indices: if invalid_indices:
log.error("%s indices %s do not exist in model", hw_type, invalid_indices) log.error("%s indices %s do not exist in model", hw_type, invalid_indices)
errors[hw_type] = f"Invalid {hw_type} indices: {invalid_indices}" errors[hw_type] = f"Invalid {hw_type} indices: {invalid_indices}"
except ValueError: except ValueError as e:
log.error("Invalid %s indices format: %s", hw_type, requested_hw[hw_type]) log.error("Invalid %s indices format: %s", hw_type, str(e))
errors[hw_type] = f"Invalid {hw_type} indices format" errors[hw_type] = f"Invalid {hw_type} indices format"
except KeyError: except KeyError:
log.error("No %s configuration found in model", hw_type) log.error("No %s configuration found in model", hw_type)
@@ -407,10 +436,13 @@ def check_hardware_availability(hypervisor_path: str, vm_name: str, requested_hw
# Track unique resources # Track unique resources
for hw_type in ['disk', 'copper', 'sfp']: for hw_type in ['disk', 'copper', 'sfp']:
if hw_type in config and config[hw_type]: if hw_type in config and config[hw_type]:
indices = [int(x) for x in str(config[hw_type]).split('\n')] try:
for idx in indices: indices = parse_hardware_indices(config[hw_type])
used_resources[hw_type][idx] = basename.replace('_sensor', '') # Store VM name without role for idx in indices:
log.debug("VM %s is using %s indices: %s", basename, hw_type, indices) used_resources[hw_type][idx] = basename.replace('_sensor', '') # Store VM name without role
log.debug("VM %s is using %s indices: %s", basename, hw_type, indices)
except ValueError as e:
log.error("Error parsing %s indices for VM %s: %s", hw_type, basename, str(e))
log.debug("Total hardware currently in use - CPU: %d, Memory: %dGB", total_cpu, total_memory) log.debug("Total hardware currently in use - CPU: %d, Memory: %dGB", total_cpu, total_memory)
log.debug("Hardware indices currently in use: %s", used_resources) log.debug("Hardware indices currently in use: %s", used_resources)
@@ -434,23 +466,27 @@ def check_hardware_availability(hypervisor_path: str, vm_name: str, requested_hw
# Check for hardware conflicts # Check for hardware conflicts
for hw_type in ['disk', 'copper', 'sfp']: for hw_type in ['disk', 'copper', 'sfp']:
if hw_type in requested_hw and requested_hw[hw_type]: if hw_type in requested_hw and requested_hw[hw_type]:
requested_indices = [int(x) for x in str(requested_hw[hw_type]).split('\n')] try:
log.debug("Checking for %s conflicts - Requesting indices: %s, Currently in use: %s", requested_indices = parse_hardware_indices(requested_hw[hw_type])
hw_type, requested_indices, used_resources[hw_type]) log.debug("Checking for %s conflicts - Requesting indices: %s, Currently in use: %s",
conflicts = {} # {index: vm_name} hw_type, requested_indices, used_resources[hw_type])
for idx in requested_indices: conflicts = {} # {index: vm_name}
if idx in used_resources[hw_type]: for idx in requested_indices:
conflicts[idx] = used_resources[hw_type][idx] if idx in used_resources[hw_type]:
conflicts[idx] = used_resources[hw_type][idx]
if conflicts: if conflicts:
# Create one sentence per conflict # Create one sentence per conflict
conflict_details = [] conflict_details = []
hw_name = hw_type.upper() if hw_type == 'sfp' else hw_type.capitalize() hw_name = hw_type.upper() if hw_type == 'sfp' else hw_type.capitalize()
for idx, vm in conflicts.items(): for idx, vm in conflicts.items():
conflict_details.append(f"{hw_name} index {idx} in use by {vm}") conflict_details.append(f"{hw_name} index {idx} in use by {vm}")
log.debug("Found conflicting %s indices: %s", hw_type, conflict_details) log.debug("Found conflicting %s indices: %s", hw_type, conflict_details)
errors[hw_type] = ". ".join(conflict_details) + "." errors[hw_type] = ". ".join(conflict_details) + "."
except ValueError as e:
log.error("Error parsing %s indices for conflict check: %s", hw_type, str(e))
errors[hw_type] = f"Invalid {hw_type} indices format"
if errors: if errors:
log.debug("Hardware validation failed with errors: %s", errors) log.debug("Hardware validation failed with errors: %s", errors)
@@ -636,12 +672,23 @@ def process_vm_creation(hypervisor_path: str, vm_config: dict) -> None:
# Add PCI devices # Add PCI devices
for hw_type in ['disk', 'copper', 'sfp']: for hw_type in ['disk', 'copper', 'sfp']:
if hw_type in vm_config and vm_config[hw_type]: if hw_type in vm_config and vm_config[hw_type]:
indices = [int(x) for x in str(vm_config[hw_type]).split('\n')] try:
for idx in indices: indices = parse_hardware_indices(vm_config[hw_type])
hw_config = {int(k): v for k, v in model_config['hardware'][hw_type].items()} for idx in indices:
pci_id = hw_config[idx] hw_config = {int(k): v for k, v in model_config['hardware'][hw_type].items()}
converted_pci_id = convert_pci_id(pci_id) pci_id = hw_config[idx]
cmd.extend(['-P', converted_pci_id]) converted_pci_id = convert_pci_id(pci_id)
cmd.extend(['-P', converted_pci_id])
except ValueError as e:
error_msg = f"Failed to parse {hw_type} indices: {str(e)}"
log.error(error_msg)
mark_vm_failed(os.path.join(hypervisor_path, vm_name), 3, error_msg)
raise ValueError(error_msg)
except KeyError as e:
error_msg = f"Invalid {hw_type} index: {str(e)}"
log.error(error_msg)
mark_vm_failed(os.path.join(hypervisor_path, vm_name), 3, error_msg)
raise KeyError(error_msg)
# Execute command # Execute command
result = subprocess.run(cmd, capture_output=True, text=True, check=True) result = subprocess.run(cmd, capture_output=True, text=True, check=True)

View File

@@ -63,17 +63,17 @@ hypervisor:
readonly: true readonly: true
forcedType: int forcedType: int
- field: disk - field: disk
label: "Disk(s) for passthrough. Line-delimited list. Free: FREE | Total: TOTAL" label: "Disk(s) for passthrough. Free: FREE | Total: TOTAL"
readonly: true readonly: true
options: []
forcedType: '[]int' forcedType: '[]int'
multiline: true
- field: copper - field: copper
label: "Copper port(s) for passthrough. Line-delimited list. Free: FREE | Total: TOTAL" label: "Copper port(s) for passthrough. Free: FREE | Total: TOTAL"
readonly: true readonly: true
options: []
forcedType: '[]int' forcedType: '[]int'
multiline: true
- field: sfp - field: sfp
label: "SFP port(s) for passthrough. Line-delimited list. Free: FREE | Total: TOTAL" label: "SFP port(s) for passthrough. Free: FREE | Total: TOTAL"
readonly: true readonly: true
options: []
forcedType: '[]int' forcedType: '[]int'
multiline: true

View File

@@ -36,7 +36,7 @@ Status values: {% for step in PROCESS_STEPS %}{{ step }}{% if not loop.last %},
{%- if is_destroyed %} {%- if is_destroyed %}
| {{ hostname }} | {{ vm_status }} | - | - | - | - | - | {{ vm_data.get('status', {}).get('timestamp', 'Never') | replace('T', ' ') | regex_replace('\\.[0-9]+', '') }} | | {{ hostname }} | {{ vm_status }} | - | - | - | - | - | {{ vm_data.get('status', {}).get('timestamp', 'Never') | replace('T', ' ') | regex_replace('\\.[0-9]+', '') }} |
{%- else %} {%- else %}
| {{ hostname }} | {{ vm_status }} | {{ vm_data.get('config', {}).get('cpu', 'N/A') }} | {{ vm_data.get('config', {}).get('memory', 'N/A') }} | {{ vm_data.get('config', {}).get('disk', '-') | replace('\n', ',') if vm_data.get('config', {}).get('disk') else '-' }} | {{ vm_data.get('config', {}).get('copper', '-') | replace('\n', ',') if vm_data.get('config', {}).get('copper') else '-' }} | {{ vm_data.get('config', {}).get('sfp', '-') | replace('\n', ',') if vm_data.get('config', {}).get('sfp') else '-' }} | {{ vm_data.get('status', {}).get('timestamp', 'Never') | replace('T', ' ') | regex_replace('\\.[0-9]+', '') }} | | {{ hostname }} | {{ vm_status }} | {{ vm_data.get('config', {}).get('cpu', 'N/A') }} | {{ vm_data.get('config', {}).get('memory', 'N/A') }} | {{ vm_data.get('config', {}).get('disk', []) | join(',') if vm_data.get('config', {}).get('disk') else '-' }} | {{ vm_data.get('config', {}).get('copper', []) | join(',') if vm_data.get('config', {}).get('copper') else '-' }} | {{ vm_data.get('config', {}).get('sfp', []) | join(',') if vm_data.get('config', {}).get('sfp') else '-' }} | {{ vm_data.get('status', {}).get('timestamp', 'Never') | replace('T', ' ') | regex_replace('\\.[0-9]+', '') }} |
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
{%- else %} {%- else %}
@@ -92,9 +92,9 @@ No Virtual Machines Found
{%- set vm_status = vm.get('status', {}).get('status', '') -%} {%- set vm_status = vm.get('status', {}).get('status', '') -%}
{%- if vm_status != 'Destroyed Instance' -%} {%- if vm_status != 'Destroyed Instance' -%}
{%- set config = vm.get('config', {}) -%} {%- set config = vm.get('config', {}) -%}
{%- do used_disk.extend((config.get('disk', '') | string).split('\n') | map('trim') | list) -%} {%- do used_disk.extend(config.get('disk', [])) -%}
{%- do used_copper.extend((config.get('copper', '') | string).split('\n') | map('trim') | list) -%} {%- do used_copper.extend(config.get('copper', [])) -%}
{%- do used_sfp.extend((config.get('sfp', '') | string).split('\n') | map('trim') | list) -%} {%- do used_sfp.extend(config.get('sfp', [])) -%}
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
@@ -154,11 +154,23 @@ No Virtual Machines Found
'regexFailureMessage': 'Enter a value not exceeding ' ~ mem_free | string ~ ' GB' 'regexFailureMessage': 'Enter a value not exceeding ' ~ mem_free | string ~ ' GB'
}) -%} }) -%}
{%- elif field.field == 'disk' -%} {%- elif field.field == 'disk' -%}
{%- do updated_field.update({'label': field.label | replace('FREE', disk_free) | replace('TOTAL', disk_total | replace('\n', ','))}) -%} {%- set disk_free_list = disk_free.split(',') if disk_free else [] -%}
{%- do updated_field.update({
'label': field.label | replace('FREE', disk_free) | replace('TOTAL', disk_total | replace('\n', ',')),
'options': disk_free_list
}) -%}
{%- elif field.field == 'copper' -%} {%- elif field.field == 'copper' -%}
{%- do updated_field.update({'label': field.label | replace('FREE', copper_free) | replace('TOTAL', copper_total | replace('\n', ','))}) -%} {%- set copper_free_list = copper_free.split(',') if copper_free else [] -%}
{%- do updated_field.update({
'label': field.label | replace('FREE', copper_free) | replace('TOTAL', copper_total | replace('\n', ',')),
'options': copper_free_list
}) -%}
{%- elif field.field == 'sfp' -%} {%- elif field.field == 'sfp' -%}
{%- do updated_field.update({'label': field.label | replace('FREE', sfp_free) | replace('TOTAL', sfp_total | replace('\n', ','))}) -%} {%- set sfp_free_list = sfp_free.split(',') if sfp_free else [] -%}
{%- do updated_field.update({
'label': field.label | replace('FREE', sfp_free) | replace('TOTAL', sfp_total | replace('\n', ',')),
'options': sfp_free_list
}) -%}
{%- endif -%} {%- endif -%}
{%- do updated_elements.append(updated_field) -%} {%- do updated_elements.append(updated_field) -%}
{%- endfor -%} {%- endfor -%}