mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 17:22:49 +01:00
366 lines
14 KiB
Python
366 lines
14 KiB
Python
#!/usr/bin/python3
|
|
|
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
|
# Elastic License 2.0.
|
|
#
|
|
# Note: Per the Elastic License 2.0, the second limitation states:
|
|
#
|
|
# "You may not move, change, disable, or circumvent the license key functionality
|
|
# in the software, and you may not remove or obscure any functionality in the
|
|
# software that is protected by the license key."
|
|
|
|
{% if 'vrt' in salt['pillar.get']('features', []) %}
|
|
|
|
"""
|
|
Script for managing hardware configurations of KVM virtual machines. This script provides
|
|
functionality to modify CPU, memory, and PCI device settings without manual XML editing
|
|
or direct libvirt interaction.
|
|
|
|
The script offers three main configuration capabilities:
|
|
1. CPU Management: Adjust virtual CPU count
|
|
2. Memory Management: Modify memory allocation
|
|
3. PCI Passthrough: Configure PCI device passthrough for direct hardware access
|
|
|
|
This script is designed to work with Security Onion's virtualization infrastructure and is typically
|
|
used during VM provisioning and hardware reconfiguration tasks.
|
|
|
|
**Usage:**
|
|
so-kvm-modify-hardware -v <vm_name> [-c <cpu_count>] [-m <memory_amount>] [-p <pci_id>] [-p <pci_id> ...] [-s]
|
|
|
|
**Options:**
|
|
-v, --vm Name of the virtual machine to modify.
|
|
-c, --cpu Number of virtual CPUs to assign.
|
|
-m, --memory Amount of memory to assign in MiB.
|
|
-p, --pci PCI hardware ID(s) to passthrough to the VM (e.g., 0000:00:1f.2). Can be specified multiple times.
|
|
Format: domain:bus:device.function
|
|
-s, --start Start the VM after modification.
|
|
|
|
**Examples:**
|
|
|
|
1. **Modify CPU and Memory with Multiple PCI Devices:**
|
|
|
|
```bash
|
|
so-kvm-modify-hardware -v vm1_sensor -c 4 -m 8192 -p 0000:c7:00.0 -p 0000:c8:00.0 -s
|
|
```
|
|
|
|
This command modifies a VM with the following settings:
|
|
- VM Name: `vm1_sensor`
|
|
- Hardware Configuration:
|
|
- CPUs: `4`
|
|
- Memory: `8192` MiB
|
|
- PCI Device Passthrough: `0000:c7:00.0`, `0000:c8:00.0`
|
|
- The VM is started after modification due to the `-s` flag
|
|
|
|
2. **Add PCI Device Without Other Changes:**
|
|
|
|
```bash
|
|
so-kvm-modify-hardware -v vm2_master -p 0000:c7:00.0
|
|
```
|
|
|
|
This command adds a single PCI device passthrough to the VM:
|
|
- VM Name: `vm2_master`
|
|
- PCI Device: `0000:c7:00.0`
|
|
- Existing CPU and memory settings are preserved
|
|
|
|
3. **Update Resource Allocation:**
|
|
|
|
```bash
|
|
so-kvm-modify-hardware -v vm3_search -c 2 -m 4096
|
|
```
|
|
|
|
This command updates only compute resources:
|
|
- VM Name: `vm3_search`
|
|
- CPUs: `2`
|
|
- Memory: `4096` MiB
|
|
- VM remains stopped after modification
|
|
|
|
4. **Add Multiple PCI Devices:**
|
|
|
|
```bash
|
|
so-kvm-modify-hardware -v vm4_node -p 0000:c7:00.0 -p 0000:c4:00.0 -p 0000:c4:00.1 -s
|
|
```
|
|
|
|
This command adds multiple PCI devices and starts the VM:
|
|
- VM Name: `vm4_node`
|
|
- PCI Devices: `0000:c7:00.0`, `0000:c4:00.0`, `0000:c4:00.1`
|
|
- VM is started after modification
|
|
|
|
**Notes:**
|
|
|
|
- The script automatically stops the VM if it's running before making modifications.
|
|
- At least one modification option (-c, -m, or -p) should be provided.
|
|
- The PCI hardware IDs must be in the format `domain:bus:device.function` (e.g., `0000:c7:00.0`).
|
|
- Multiple PCI devices can be added by using the `-p` option multiple times.
|
|
- Without the `-s` flag, the VM remains stopped after modification.
|
|
- Existing hardware configurations are preserved if not explicitly modified.
|
|
|
|
**Description:**
|
|
|
|
The `so-kvm-modify-hardware` script modifies hardware parameters of KVM virtual machines using the following process:
|
|
|
|
1. **VM State Management:**
|
|
- Connects to the local libvirt daemon
|
|
- Stops the VM if it's currently running
|
|
- Retrieves current VM configuration
|
|
|
|
2. **Hardware Configuration:**
|
|
- Modifies CPU count if specified
|
|
- Updates memory allocation if specified
|
|
- Adds PCI device passthrough configurations if specified
|
|
- All changes are made through libvirt XML configuration
|
|
|
|
3. **VM Redefinition:**
|
|
- Applies the new configuration by redefining the VM
|
|
- Optionally starts the VM if requested
|
|
- Ensures clean shutdown and startup during modifications
|
|
|
|
4. **Error Handling:**
|
|
- Validates all input parameters
|
|
- Ensures proper XML structure
|
|
- Provides detailed error messages for troubleshooting
|
|
|
|
**Exit Codes:**
|
|
|
|
- `0`: Success
|
|
- `1`: An error occurred during execution
|
|
|
|
**Logging:**
|
|
|
|
- Logs are written to `/opt/so/log/hypervisor/so-kvm-modify-hardware.log`
|
|
- Both file and console logging are enabled for real-time monitoring
|
|
- Log entries include timestamps and severity levels
|
|
- Detailed error messages are logged for troubleshooting
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import libvirt
|
|
import logging
|
|
import socket
|
|
import xml.etree.ElementTree as ET
|
|
from io import StringIO
|
|
from so_vm_utils import start_vm, stop_vm
|
|
from so_logging_utils import setup_logging
|
|
import subprocess
|
|
|
|
# Get hypervisor name from local hostname
|
|
HYPERVISOR = socket.gethostname()
|
|
|
|
# Custom log handler to capture output
|
|
class StringIOHandler(logging.Handler):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.strio = StringIO()
|
|
|
|
def emit(self, record):
|
|
msg = self.format(record)
|
|
self.strio.write(msg + '\n')
|
|
|
|
def get_value(self):
|
|
return self.strio.getvalue()
|
|
|
|
def parse_arguments():
|
|
parser = argparse.ArgumentParser(description='Modify hardware parameters of a KVM virtual machine.')
|
|
parser.add_argument('-v', '--vm', required=True, help='Name of the virtual machine to modify.')
|
|
parser.add_argument('-c', '--cpu', type=int, help='Number of virtual CPUs to assign.')
|
|
parser.add_argument('-m', '--memory', type=int, help='Amount of memory to assign in MiB.')
|
|
parser.add_argument('-p', '--pci', action='append', help='PCI hardware ID(s) to passthrough to the VM (e.g., 0000:00:1f.2). Can be specified multiple times.')
|
|
parser.add_argument('-s', '--start', action='store_true', help='Start the VM after modification.')
|
|
args = parser.parse_args()
|
|
return args
|
|
|
|
def modify_vm(dom, cpu_count, memory_amount, pci_ids, logger):
|
|
try:
|
|
# Get the XML description of the VM
|
|
xml_desc = dom.XMLDesc()
|
|
root = ET.fromstring(xml_desc)
|
|
|
|
# Modify CPU count
|
|
if cpu_count is not None:
|
|
vcpu_elem = root.find('./vcpu')
|
|
if vcpu_elem is not None:
|
|
vcpu_elem.text = str(cpu_count)
|
|
logger.info(f"Set CPU count to {cpu_count}.")
|
|
else:
|
|
logger.error("Could not find <vcpu> element in XML.")
|
|
sys.exit(1)
|
|
|
|
# Modify memory amount
|
|
if memory_amount is not None:
|
|
memory_elem = root.find('./memory')
|
|
current_memory_elem = root.find('./currentMemory')
|
|
if memory_elem is not None and current_memory_elem is not None:
|
|
memory_elem.text = str(memory_amount * 1024) # Convert MiB to KiB
|
|
current_memory_elem.text = str(memory_amount * 1024)
|
|
logger.info(f"Set memory to {memory_amount} MiB.")
|
|
else:
|
|
logger.error("Could not find <memory> elements in XML.")
|
|
sys.exit(1)
|
|
|
|
# Add PCI device passthrough(s)
|
|
if pci_ids:
|
|
devices_elem = root.find('./devices')
|
|
if devices_elem is not None:
|
|
for pci_id in pci_ids:
|
|
hostdev_elem = ET.SubElement(devices_elem, 'hostdev', attrib={
|
|
'mode': 'subsystem',
|
|
'type': 'pci',
|
|
'managed': 'yes'
|
|
})
|
|
source_elem = ET.SubElement(hostdev_elem, 'source')
|
|
# Split PCI ID into components (domain:bus:slot.function)
|
|
parts = pci_id.split(':')
|
|
if len(parts) != 3:
|
|
logger.error(f"Invalid PCI ID format: {pci_id}. Expected format: domain:bus:slot.function")
|
|
sys.exit(1)
|
|
domain_id = parts[0]
|
|
bus = parts[1]
|
|
slot_func = parts[2].split('.')
|
|
if len(slot_func) != 2:
|
|
logger.error(f"Invalid PCI ID format: {pci_id}. Expected format: domain:bus:slot.function")
|
|
sys.exit(1)
|
|
slot = slot_func[0]
|
|
function = slot_func[1]
|
|
address_attrs = {
|
|
'domain': f'0x{domain_id}',
|
|
'bus': f'0x{bus}',
|
|
'slot': f'0x{slot}',
|
|
'function': f'0x{function}'
|
|
}
|
|
ET.SubElement(source_elem, 'address', attrib=address_attrs)
|
|
logger.info(f"Added PCI device passthrough for {pci_id}.")
|
|
else:
|
|
logger.error("Could not find <devices> element in XML.")
|
|
sys.exit(1)
|
|
|
|
# Convert XML back to string
|
|
new_xml_desc = ET.tostring(root, encoding='unicode')
|
|
return new_xml_desc
|
|
except Exception as e:
|
|
logger.error(f"Failed to modify VM XML: {e}")
|
|
sys.exit(1)
|
|
|
|
def redefine_vm(conn, new_xml_desc, logger):
|
|
try:
|
|
conn.defineXML(new_xml_desc)
|
|
logger.info("VM redefined with new hardware parameters.")
|
|
except libvirt.libvirtError as e:
|
|
logger.error(f"Failed to redefine VM: {e}")
|
|
sys.exit(1)
|
|
|
|
def main():
|
|
# Set up logging using the so_logging_utils library
|
|
string_handler = StringIOHandler()
|
|
string_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
|
logger = setup_logging(
|
|
logger_name='so-kvm-modify-hardware',
|
|
log_file_path='/opt/so/log/hypervisor/so-kvm-modify-hardware.log',
|
|
log_level=logging.INFO,
|
|
format_str='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger.addHandler(string_handler)
|
|
|
|
try:
|
|
args = parse_arguments()
|
|
|
|
vm_name = args.vm
|
|
cpu_count = args.cpu
|
|
memory_amount = args.memory
|
|
pci_ids = args.pci # This will be a list or None
|
|
start_vm_flag = args.start
|
|
|
|
# Connect to libvirt
|
|
try:
|
|
conn = libvirt.open(None)
|
|
except libvirt.libvirtError as e:
|
|
logger.error(f"Failed to open connection to libvirt: {e}")
|
|
try:
|
|
subprocess.run([
|
|
'so-salt-emit-vm-deployment-status-event',
|
|
'-v', vm_name,
|
|
'-H', HYPERVISOR,
|
|
'-s', 'Hardware Configuration Failed'
|
|
], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Failed to emit failure status event: {e}")
|
|
sys.exit(1)
|
|
|
|
# Stop VM if running
|
|
dom = stop_vm(conn, vm_name, logger)
|
|
|
|
# Modify VM XML
|
|
new_xml_desc = modify_vm(dom, cpu_count, memory_amount, pci_ids, logger)
|
|
|
|
# Redefine VM
|
|
redefine_vm(conn, new_xml_desc, logger)
|
|
|
|
# Start VM if -s or --start argument is provided
|
|
if start_vm_flag:
|
|
dom = conn.lookupByName(vm_name)
|
|
start_vm(dom, logger)
|
|
logger.info(f"VM '{vm_name}' started successfully.")
|
|
else:
|
|
logger.info("VM start flag not provided; VM will remain stopped.")
|
|
|
|
# Close connection
|
|
conn.close()
|
|
|
|
# Send success status event
|
|
try:
|
|
subprocess.run([
|
|
'so-salt-emit-vm-deployment-status-event',
|
|
'-v', vm_name,
|
|
'-H', HYPERVISOR,
|
|
'-s', 'Hardware Configuration'
|
|
], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Failed to emit success status event: {e}")
|
|
|
|
except KeyboardInterrupt:
|
|
error_msg = "Operation cancelled by user"
|
|
logger.error(error_msg)
|
|
try:
|
|
subprocess.run([
|
|
'so-salt-emit-vm-deployment-status-event',
|
|
'-v', vm_name,
|
|
'-H', HYPERVISOR,
|
|
'-s', 'Hardware Configuration Failed'
|
|
], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Failed to emit failure status event: {e}")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
if "Failed to open connection to libvirt" in error_msg:
|
|
error_msg = f"Failed to connect to libvirt: {error_msg}"
|
|
elif "Failed to redefine VM" in error_msg:
|
|
error_msg = f"Failed to apply hardware changes: {error_msg}"
|
|
elif "Failed to modify VM XML" in error_msg:
|
|
error_msg = f"Failed to update hardware configuration: {error_msg}"
|
|
else:
|
|
error_msg = f"An error occurred: {error_msg}"
|
|
logger.error(error_msg)
|
|
try:
|
|
subprocess.run([
|
|
'so-salt-emit-vm-deployment-status-event',
|
|
'-v', vm_name,
|
|
'-h', HYPERVISOR,
|
|
'-s', 'Hardware Configuration Failed'
|
|
], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Failed to emit failure status event: {e}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
{%- else -%}
|
|
|
|
echo "Hypervisor nodes are a feature supported only for customers with a valid license. \
|
|
Contact Security Onion Solutions, LLC via our website at https://securityonionsolutions.com \
|
|
for more information about purchasing a license to enable this feature."
|
|
|
|
{% endif -%}
|