mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 17:22:49 +01:00
create volume
This commit is contained in:
@@ -7,12 +7,14 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Salt module for managing QCOW2 image configurations and VM hardware settings. This module provides functions
|
Salt module for managing QCOW2 image configurations and VM hardware settings. This module provides functions
|
||||||
for modifying network configurations within QCOW2 images and adjusting virtual machine hardware settings.
|
for modifying network configurations within QCOW2 images, adjusting virtual machine hardware settings, and
|
||||||
It serves as a Salt interface to the so-qcow2-modify-network and so-kvm-modify-hardware scripts.
|
creating virtual storage volumes. It serves as a Salt interface to the so-qcow2-modify-network,
|
||||||
|
so-kvm-modify-hardware, and so-kvm-create-volume scripts.
|
||||||
|
|
||||||
The module offers two main capabilities:
|
The module offers three main capabilities:
|
||||||
1. Network Configuration: Modify network settings (DHCP/static IP) within QCOW2 images
|
1. Network Configuration: Modify network settings (DHCP/static IP) within QCOW2 images
|
||||||
2. Hardware Configuration: Adjust VM hardware settings (CPU, memory, PCI passthrough)
|
2. Hardware Configuration: Adjust VM hardware settings (CPU, memory, PCI passthrough)
|
||||||
|
3. Volume Management: Create and attach virtual storage volumes for NSM data
|
||||||
|
|
||||||
This module is intended to work with Security Onion's virtualization infrastructure and is typically
|
This module is intended to work with Security Onion's virtualization infrastructure and is typically
|
||||||
used in conjunction with salt-cloud for VM provisioning and management.
|
used in conjunction with salt-cloud for VM provisioning and management.
|
||||||
@@ -244,3 +246,90 @@ def modify_hardware_config(vm_name, cpu=None, memory=None, pci=None, start=False
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('qcow2 module: An error occurred while executing the script: {}'.format(e))
|
log.error('qcow2 module: An error occurred while executing the script: {}'.format(e))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def create_volume_config(vm_name, size_gb, start=False):
|
||||||
|
'''
|
||||||
|
Usage:
|
||||||
|
salt '*' qcow2.create_volume_config vm_name=<name> size_gb=<size> [start=<bool>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
vm_name
|
||||||
|
Name of the virtual machine to attach the volume to
|
||||||
|
size_gb
|
||||||
|
Volume size in GB (positive integer)
|
||||||
|
This determines the capacity of the virtual storage volume
|
||||||
|
start
|
||||||
|
Boolean flag to start the VM after volume creation
|
||||||
|
Optional - defaults to False
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
1. **Create 500GB Volume:**
|
||||||
|
```bash
|
||||||
|
salt '*' qcow2.create_volume_config vm_name='sensor1_sensor' size_gb=500
|
||||||
|
```
|
||||||
|
This creates a 500GB virtual volume for NSM storage
|
||||||
|
|
||||||
|
2. **Create 1TB Volume and Start VM:**
|
||||||
|
```bash
|
||||||
|
salt '*' qcow2.create_volume_config vm_name='sensor1_sensor' size_gb=1000 start=True
|
||||||
|
```
|
||||||
|
This creates a 1TB volume and starts the VM after attachment
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- VM must be stopped before volume creation
|
||||||
|
- Volume is created as a qcow2 image and attached to the VM
|
||||||
|
- This is an alternative to disk passthrough via modify_hardware_config
|
||||||
|
- Volume is automatically attached to the VM's libvirt configuration
|
||||||
|
- Requires so-kvm-create-volume script to be installed
|
||||||
|
- Volume files are stored in the hypervisor's VM storage directory
|
||||||
|
|
||||||
|
Description:
|
||||||
|
This function creates and attaches a virtual storage volume to a KVM virtual machine
|
||||||
|
using the so-kvm-create-volume script. It creates a qcow2 disk image of the specified
|
||||||
|
size and attaches it to the VM for NSM (Network Security Monitoring) storage purposes.
|
||||||
|
This provides an alternative to physical disk passthrough, allowing flexible storage
|
||||||
|
allocation without requiring dedicated hardware. The VM can optionally be started
|
||||||
|
after the volume is successfully created and attached.
|
||||||
|
|
||||||
|
Exit Codes:
|
||||||
|
0: Success
|
||||||
|
1: Invalid parameters
|
||||||
|
2: VM state error (running when should be stopped)
|
||||||
|
3: Volume creation error
|
||||||
|
4: System command error
|
||||||
|
255: Unexpected error
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
- All operations are logged to the salt minion log
|
||||||
|
- Log entries are prefixed with 'qcow2 module:'
|
||||||
|
- Volume creation and attachment operations are logged
|
||||||
|
- Errors include detailed messages and stack traces
|
||||||
|
- Final status of volume creation is logged
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Validate size_gb parameter
|
||||||
|
if not isinstance(size_gb, int) or size_gb <= 0:
|
||||||
|
raise ValueError('size_gb must be a positive integer.')
|
||||||
|
|
||||||
|
cmd = ['/usr/sbin/so-kvm-create-volume', '-v', vm_name, '-s', str(size_gb)]
|
||||||
|
|
||||||
|
if start:
|
||||||
|
cmd.append('-S')
|
||||||
|
|
||||||
|
log.info('qcow2 module: Executing command: {}'.format(' '.join(shlex.quote(arg) for arg in cmd)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||||
|
ret = {
|
||||||
|
'retcode': result.returncode,
|
||||||
|
'stdout': result.stdout,
|
||||||
|
'stderr': result.stderr
|
||||||
|
}
|
||||||
|
if result.returncode != 0:
|
||||||
|
log.error('qcow2 module: Script execution failed with return code {}: {}'.format(result.returncode, result.stderr))
|
||||||
|
else:
|
||||||
|
log.info('qcow2 module: Script executed successfully.')
|
||||||
|
return ret
|
||||||
|
except Exception as e:
|
||||||
|
log.error('qcow2 module: An error occurred while executing the script: {}'.format(e))
|
||||||
|
raise
|
||||||
|
|||||||
533
salt/hypervisor/tools/sbin_jinja/so-kvm-create-volume
Normal file
533
salt/hypervisor/tools/sbin_jinja/so-kvm-create-volume
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
#!/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 creating and attaching virtual volumes to KVM virtual machines for NSM storage.
|
||||||
|
This script provides functionality to create pre-allocated raw disk images and attach them
|
||||||
|
to VMs as virtio-blk devices for high-performance network security monitoring data storage.
|
||||||
|
|
||||||
|
The script handles the complete volume lifecycle:
|
||||||
|
1. Volume Creation: Creates pre-allocated raw disk images using qemu-img
|
||||||
|
2. Volume Attachment: Attaches volumes to VMs as virtio-blk devices
|
||||||
|
3. VM Management: Stops/starts VMs as needed during the process
|
||||||
|
|
||||||
|
This script is designed to work with Security Onion's virtualization infrastructure and is typically
|
||||||
|
used during VM provisioning to add dedicated NSM storage volumes.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
so-kvm-create-volume -v <vm_name> -s <size_gb> [-S]
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
-v, --vm Name of the virtual machine to attach the volume to (required).
|
||||||
|
-s, --size Size of the volume in GB (required, must be a positive integer).
|
||||||
|
-S, --start Start the VM after volume creation and attachment (optional).
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
1. **Create and Attach 500GB Volume:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
so-kvm-create-volume -v vm1_sensor -s 500
|
||||||
|
```
|
||||||
|
|
||||||
|
This command creates and attaches a volume with the following settings:
|
||||||
|
- VM Name: `vm1_sensor`
|
||||||
|
- Volume Size: `500` GB
|
||||||
|
- Volume Path: `/nsm/libvirt/volumes/vm1_sensor-nsm.img`
|
||||||
|
- Device: `/dev/vdb` (virtio-blk)
|
||||||
|
- VM remains stopped after attachment
|
||||||
|
|
||||||
|
2. **Create Volume and Start VM:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
so-kvm-create-volume -v vm2_sensor -s 1000 -S
|
||||||
|
```
|
||||||
|
|
||||||
|
This command creates a volume and starts the VM:
|
||||||
|
- VM Name: `vm2_sensor`
|
||||||
|
- Volume Size: `1000` GB (1 TB)
|
||||||
|
- VM is started after volume attachment due to the `-S` flag
|
||||||
|
|
||||||
|
3. **Create Large Volume for Heavy Traffic:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
so-kvm-create-volume -v vm3_sensor -s 2000 -S
|
||||||
|
```
|
||||||
|
|
||||||
|
This command creates a large volume for high-traffic environments:
|
||||||
|
- VM Name: `vm3_sensor`
|
||||||
|
- Volume Size: `2000` GB (2 TB)
|
||||||
|
- VM is started after attachment
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- The script automatically stops the VM if it's running before creating and attaching the volume.
|
||||||
|
- Volumes are created with full pre-allocation for optimal performance.
|
||||||
|
- Volume files are stored in `/nsm/libvirt/volumes/` with naming pattern `<vm_name>-nsm.img`.
|
||||||
|
- Volumes are attached as `/dev/vdb` using virtio-blk for high performance.
|
||||||
|
- The script checks available disk space before creating the volume.
|
||||||
|
- Ownership is set to `socore:socore` with permissions `644`.
|
||||||
|
- Without the `-S` flag, the VM remains stopped after volume attachment.
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
The `so-kvm-create-volume` script creates and attaches NSM storage volumes using the following process:
|
||||||
|
|
||||||
|
1. **Pre-flight Checks:**
|
||||||
|
- Validates input parameters (VM name, size)
|
||||||
|
- Checks available disk space in `/nsm/libvirt/volumes/`
|
||||||
|
- Ensures sufficient space for the requested volume size
|
||||||
|
|
||||||
|
2. **VM State Management:**
|
||||||
|
- Connects to the local libvirt daemon
|
||||||
|
- Stops the VM if it's currently running
|
||||||
|
- Retrieves current VM configuration
|
||||||
|
|
||||||
|
3. **Volume Creation:**
|
||||||
|
- Creates volume directory if it doesn't exist
|
||||||
|
- Uses `qemu-img create` with full pre-allocation
|
||||||
|
- Sets proper ownership (socore:socore) and permissions (644)
|
||||||
|
- Validates volume creation success
|
||||||
|
|
||||||
|
4. **Volume Attachment:**
|
||||||
|
- Modifies VM's libvirt XML configuration
|
||||||
|
- Adds disk element with virtio-blk driver
|
||||||
|
- Configures cache='none' and io='native' for performance
|
||||||
|
- Attaches volume as `/dev/vdb`
|
||||||
|
|
||||||
|
5. **VM Redefinition:**
|
||||||
|
- Applies the new configuration by redefining the VM
|
||||||
|
- Optionally starts the VM if requested
|
||||||
|
- Emits deployment status events for monitoring
|
||||||
|
|
||||||
|
6. **Error Handling:**
|
||||||
|
- Validates all input parameters
|
||||||
|
- Checks disk space before creation
|
||||||
|
- Handles volume creation failures
|
||||||
|
- Handles volume attachment failures
|
||||||
|
- 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-create-volume.log`
|
||||||
|
- Both file and console logging are enabled for real-time monitoring
|
||||||
|
- Log entries include timestamps and severity levels
|
||||||
|
- Log prefixes: VOLUME:, VM:, HARDWARE:, SPACE:
|
||||||
|
- Detailed error messages are logged for troubleshooting
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import libvirt
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get hypervisor name from local hostname
|
||||||
|
HYPERVISOR = socket.gethostname()
|
||||||
|
|
||||||
|
# Volume storage directory
|
||||||
|
VOLUME_DIR = '/nsm/libvirt/volumes'
|
||||||
|
|
||||||
|
# Custom exception classes
|
||||||
|
class InsufficientSpaceError(Exception):
|
||||||
|
"""Raised when there is insufficient disk space for volume creation."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class VolumeCreationError(Exception):
|
||||||
|
"""Raised when volume creation fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class VolumeAttachmentError(Exception):
|
||||||
|
"""Raised when volume attachment fails."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
"""Parse command-line arguments."""
|
||||||
|
parser = argparse.ArgumentParser(description='Create and attach a virtual volume to a KVM virtual machine for NSM storage.')
|
||||||
|
parser.add_argument('-v', '--vm', required=True, help='Name of the virtual machine to attach the volume to.')
|
||||||
|
parser.add_argument('-s', '--size', type=int, required=True, help='Size of the volume in GB (must be a positive integer).')
|
||||||
|
parser.add_argument('-S', '--start', action='store_true', help='Start the VM after volume creation and attachment.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate size is positive
|
||||||
|
if args.size <= 0:
|
||||||
|
parser.error("Volume size must be a positive integer.")
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def check_disk_space(size_gb, logger):
|
||||||
|
"""
|
||||||
|
Check if there is sufficient disk space available for volume creation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
size_gb: Size of the volume in GB
|
||||||
|
logger: Logger instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InsufficientSpaceError: If there is not enough disk space
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stat = os.statvfs(VOLUME_DIR)
|
||||||
|
# Available space in bytes
|
||||||
|
available_bytes = stat.f_bavail * stat.f_frsize
|
||||||
|
# Required space in bytes (add 10% buffer)
|
||||||
|
required_bytes = size_gb * 1024 * 1024 * 1024 * 1.1
|
||||||
|
|
||||||
|
available_gb = available_bytes / (1024 * 1024 * 1024)
|
||||||
|
required_gb = required_bytes / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
logger.info(f"SPACE: Available: {available_gb:.2f} GB, Required: {required_gb:.2f} GB")
|
||||||
|
|
||||||
|
if available_bytes < required_bytes:
|
||||||
|
raise InsufficientSpaceError(
|
||||||
|
f"Insufficient disk space. Available: {available_gb:.2f} GB, Required: {required_gb:.2f} GB"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"SPACE: Sufficient disk space available for {size_gb} GB volume")
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"SPACE: Failed to check disk space: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_volume_file(vm_name, size_gb, logger):
|
||||||
|
"""
|
||||||
|
Create a pre-allocated raw disk image for the VM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vm_name: Name of the VM
|
||||||
|
size_gb: Size of the volume in GB
|
||||||
|
logger: Logger instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the created volume file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
VolumeCreationError: If volume creation fails
|
||||||
|
"""
|
||||||
|
# Create volume directory if it doesn't exist
|
||||||
|
try:
|
||||||
|
if not os.path.exists(VOLUME_DIR):
|
||||||
|
os.makedirs(VOLUME_DIR, mode=0o755)
|
||||||
|
logger.info(f"VOLUME: Created volume directory: {VOLUME_DIR}")
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"VOLUME: Failed to create volume directory: {e}")
|
||||||
|
raise VolumeCreationError(f"Failed to create volume directory: {e}")
|
||||||
|
|
||||||
|
# Define volume path
|
||||||
|
volume_path = os.path.join(VOLUME_DIR, f"{vm_name}-nsm.img")
|
||||||
|
|
||||||
|
# Check if volume already exists
|
||||||
|
if os.path.exists(volume_path):
|
||||||
|
logger.error(f"VOLUME: Volume already exists: {volume_path}")
|
||||||
|
raise VolumeCreationError(f"Volume already exists: {volume_path}")
|
||||||
|
|
||||||
|
logger.info(f"VOLUME: Creating {size_gb} GB volume at {volume_path}")
|
||||||
|
|
||||||
|
# Create volume using qemu-img with full pre-allocation
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
'qemu-img', 'create',
|
||||||
|
'-f', 'raw',
|
||||||
|
'-o', 'preallocation=full',
|
||||||
|
volume_path,
|
||||||
|
f"{size_gb}G"
|
||||||
|
]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"VOLUME: Volume created successfully")
|
||||||
|
if result.stdout:
|
||||||
|
logger.debug(f"VOLUME: qemu-img output: {result.stdout.strip()}")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"VOLUME: Failed to create volume: {e}")
|
||||||
|
if e.stderr:
|
||||||
|
logger.error(f"VOLUME: qemu-img error: {e.stderr.strip()}")
|
||||||
|
raise VolumeCreationError(f"Failed to create volume: {e}")
|
||||||
|
|
||||||
|
# Set ownership to socore:socore
|
||||||
|
try:
|
||||||
|
socore_uid = pwd.getpwnam('socore').pw_uid
|
||||||
|
socore_gid = grp.getgrnam('socore').gr_gid
|
||||||
|
os.chown(volume_path, socore_uid, socore_gid)
|
||||||
|
logger.info(f"VOLUME: Set ownership to socore:socore")
|
||||||
|
except (KeyError, OSError) as e:
|
||||||
|
logger.error(f"VOLUME: Failed to set ownership: {e}")
|
||||||
|
raise VolumeCreationError(f"Failed to set ownership: {e}")
|
||||||
|
|
||||||
|
# Set permissions to 644
|
||||||
|
try:
|
||||||
|
os.chmod(volume_path, 0o644)
|
||||||
|
logger.info(f"VOLUME: Set permissions to 644")
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"VOLUME: Failed to set permissions: {e}")
|
||||||
|
raise VolumeCreationError(f"Failed to set permissions: {e}")
|
||||||
|
|
||||||
|
# Verify volume was created
|
||||||
|
if not os.path.exists(volume_path):
|
||||||
|
logger.error(f"VOLUME: Volume file not found after creation: {volume_path}")
|
||||||
|
raise VolumeCreationError(f"Volume file not found after creation: {volume_path}")
|
||||||
|
|
||||||
|
volume_size = os.path.getsize(volume_path)
|
||||||
|
logger.info(f"VOLUME: Volume created: {volume_path} ({volume_size} bytes)")
|
||||||
|
|
||||||
|
return volume_path
|
||||||
|
|
||||||
|
def attach_volume_to_vm(conn, vm_name, volume_path, logger):
|
||||||
|
"""
|
||||||
|
Attach the volume to the VM's libvirt XML configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conn: Libvirt connection
|
||||||
|
vm_name: Name of the VM
|
||||||
|
volume_path: Path to the volume file
|
||||||
|
logger: Logger instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
VolumeAttachmentError: If volume attachment fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the VM domain
|
||||||
|
dom = conn.lookupByName(vm_name)
|
||||||
|
|
||||||
|
# Get the XML description of the VM
|
||||||
|
xml_desc = dom.XMLDesc()
|
||||||
|
root = ET.fromstring(xml_desc)
|
||||||
|
|
||||||
|
# Find the devices element
|
||||||
|
devices_elem = root.find('./devices')
|
||||||
|
if devices_elem is None:
|
||||||
|
logger.error("VM: Could not find <devices> element in XML")
|
||||||
|
raise VolumeAttachmentError("Could not find <devices> element in VM XML")
|
||||||
|
|
||||||
|
# Check if vdb already exists
|
||||||
|
for disk in devices_elem.findall('./disk'):
|
||||||
|
target = disk.find('./target')
|
||||||
|
if target is not None and target.get('dev') == 'vdb':
|
||||||
|
logger.error("VM: Device vdb already exists in VM configuration")
|
||||||
|
raise VolumeAttachmentError("Device vdb already exists in VM configuration")
|
||||||
|
|
||||||
|
logger.info(f"VM: Attaching volume to {vm_name} as /dev/vdb")
|
||||||
|
|
||||||
|
# Create disk element
|
||||||
|
disk_elem = ET.SubElement(devices_elem, 'disk', attrib={
|
||||||
|
'type': 'file',
|
||||||
|
'device': 'disk'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add driver element
|
||||||
|
ET.SubElement(disk_elem, 'driver', attrib={
|
||||||
|
'name': 'qemu',
|
||||||
|
'type': 'raw',
|
||||||
|
'cache': 'none',
|
||||||
|
'io': 'native'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add source element
|
||||||
|
ET.SubElement(disk_elem, 'source', attrib={
|
||||||
|
'file': volume_path
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add target element
|
||||||
|
ET.SubElement(disk_elem, 'target', attrib={
|
||||||
|
'dev': 'vdb',
|
||||||
|
'bus': 'virtio'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add address element
|
||||||
|
ET.SubElement(disk_elem, 'address', attrib={
|
||||||
|
'type': 'pci',
|
||||||
|
'domain': '0x0000',
|
||||||
|
'bus': '0x00',
|
||||||
|
'slot': '0x07',
|
||||||
|
'function': '0x0'
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"HARDWARE: Added disk configuration for vdb")
|
||||||
|
|
||||||
|
# Convert XML back to string
|
||||||
|
new_xml_desc = ET.tostring(root, encoding='unicode')
|
||||||
|
|
||||||
|
# Redefine the VM with the new XML
|
||||||
|
conn.defineXML(new_xml_desc)
|
||||||
|
logger.info(f"VM: VM redefined with volume attached")
|
||||||
|
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
logger.error(f"VM: Failed to attach volume: {e}")
|
||||||
|
raise VolumeAttachmentError(f"Failed to attach volume: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"VM: Failed to attach volume: {e}")
|
||||||
|
raise VolumeAttachmentError(f"Failed to attach volume: {e}")
|
||||||
|
|
||||||
|
def emit_status_event(vm_name, status):
|
||||||
|
"""
|
||||||
|
Emit a deployment status event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vm_name: Name of the VM
|
||||||
|
status: Status message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
'so-salt-emit-vm-deployment-status-event',
|
||||||
|
'-v', vm_name,
|
||||||
|
'-H', HYPERVISOR,
|
||||||
|
'-s', status
|
||||||
|
], check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# Don't fail the entire operation if status event fails
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to orchestrate volume creation and attachment."""
|
||||||
|
# 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-create-volume',
|
||||||
|
log_file_path='/opt/so/log/hypervisor/so-kvm-create-volume.log',
|
||||||
|
log_level=logging.INFO,
|
||||||
|
format_str='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger.addHandler(string_handler)
|
||||||
|
|
||||||
|
vm_name = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse arguments
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
vm_name = args.vm
|
||||||
|
size_gb = args.size
|
||||||
|
start_vm_flag = args.start
|
||||||
|
|
||||||
|
logger.info(f"VOLUME: Starting volume creation for VM '{vm_name}' with size {size_gb} GB")
|
||||||
|
|
||||||
|
# Emit start status event
|
||||||
|
emit_status_event(vm_name, 'Volume Creation')
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
check_disk_space(size_gb, logger)
|
||||||
|
|
||||||
|
# Connect to libvirt
|
||||||
|
try:
|
||||||
|
conn = libvirt.open(None)
|
||||||
|
logger.info("VM: Connected to libvirt")
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
logger.error(f"VM: Failed to open connection to libvirt: {e}")
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Stop VM if running
|
||||||
|
dom = stop_vm(conn, vm_name, logger)
|
||||||
|
|
||||||
|
# Create volume file
|
||||||
|
volume_path = create_volume_file(vm_name, size_gb, logger)
|
||||||
|
|
||||||
|
# Attach volume to VM
|
||||||
|
attach_volume_to_vm(conn, vm_name, volume_path, 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 '{vm_name}' started successfully")
|
||||||
|
else:
|
||||||
|
logger.info("VM: Start flag not provided; VM will remain stopped")
|
||||||
|
|
||||||
|
# Close connection
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Emit success status event
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration')
|
||||||
|
|
||||||
|
logger.info(f"VOLUME: Volume creation and attachment completed successfully for VM '{vm_name}'")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
error_msg = "Operation cancelled by user"
|
||||||
|
logger.error(error_msg)
|
||||||
|
if vm_name:
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except InsufficientSpaceError as e:
|
||||||
|
error_msg = f"SPACE: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
if vm_name:
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except VolumeCreationError as e:
|
||||||
|
error_msg = f"VOLUME: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
if vm_name:
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except VolumeAttachmentError as e:
|
||||||
|
error_msg = f"VM: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
if vm_name:
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"An error occurred: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
if vm_name:
|
||||||
|
emit_status_event(vm_name, 'Volume Configuration Failed')
|
||||||
|
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 -%}
|
||||||
@@ -533,6 +533,64 @@ def run_qcow2_modify_hardware_config(profile, vm_name, cpu=None, memory=None, pc
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred while running qcow2.modify_hardware_config: {e}")
|
logger.error(f"An error occurred while running qcow2.modify_hardware_config: {e}")
|
||||||
|
|
||||||
|
def run_qcow2_create_volume_config(profile, vm_name, size_gb, cpu=None, memory=None, start=False):
|
||||||
|
"""Create a volume for the VM and optionally configure CPU/memory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile (str): The cloud profile name
|
||||||
|
vm_name (str): The name of the VM
|
||||||
|
size_gb (int): Size of the volume in GB
|
||||||
|
cpu (int, optional): Number of CPUs to assign
|
||||||
|
memory (int, optional): Amount of memory in MiB
|
||||||
|
start (bool): Whether to start the VM after configuration
|
||||||
|
"""
|
||||||
|
hv_name = profile.split('-')[1]
|
||||||
|
target = hv_name + "_*"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Create the volume
|
||||||
|
logger.info(f"Creating {size_gb}GB volume for VM {vm_name}")
|
||||||
|
volume_result = local.cmd(
|
||||||
|
target,
|
||||||
|
'qcow2.create_volume_config',
|
||||||
|
kwarg={
|
||||||
|
'vm_name': vm_name,
|
||||||
|
'size_gb': size_gb,
|
||||||
|
'start': False # Don't start yet if we need to configure CPU/memory
|
||||||
|
}
|
||||||
|
)
|
||||||
|
format_qcow2_output('Volume creation', volume_result)
|
||||||
|
|
||||||
|
# Step 2: Configure CPU and memory if specified
|
||||||
|
if cpu or memory:
|
||||||
|
logger.info(f"Configuring hardware for VM {vm_name}: CPU={cpu}, Memory={memory}MiB")
|
||||||
|
hw_result = local.cmd(
|
||||||
|
target,
|
||||||
|
'qcow2.modify_hardware_config',
|
||||||
|
kwarg={
|
||||||
|
'vm_name': vm_name,
|
||||||
|
'cpu': cpu,
|
||||||
|
'memory': memory,
|
||||||
|
'start': start
|
||||||
|
}
|
||||||
|
)
|
||||||
|
format_qcow2_output('Hardware configuration', hw_result)
|
||||||
|
elif start:
|
||||||
|
# If no CPU/memory config needed but we need to start the VM
|
||||||
|
logger.info(f"Starting VM {vm_name}")
|
||||||
|
start_result = local.cmd(
|
||||||
|
target,
|
||||||
|
'qcow2.modify_hardware_config',
|
||||||
|
kwarg={
|
||||||
|
'vm_name': vm_name,
|
||||||
|
'start': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
format_qcow2_output('VM startup', start_result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred while creating volume and configuring hardware: {e}")
|
||||||
|
|
||||||
def run_qcow2_modify_network_config(profile, vm_name, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
def run_qcow2_modify_network_config(profile, vm_name, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
||||||
hv_name = profile.split('-')[1]
|
hv_name = profile.split('-')[1]
|
||||||
target = hv_name + "_*"
|
target = hv_name + "_*"
|
||||||
@@ -586,6 +644,7 @@ def parse_arguments():
|
|||||||
network_group.add_argument('-c', '--cpu', type=int, help='Number of virtual CPUs to assign.')
|
network_group.add_argument('-c', '--cpu', type=int, help='Number of virtual CPUs to assign.')
|
||||||
network_group.add_argument('-m', '--memory', type=int, help='Amount of memory to assign in MiB.')
|
network_group.add_argument('-m', '--memory', type=int, help='Amount of memory to assign in MiB.')
|
||||||
network_group.add_argument('-P', '--pci', action='append', help='PCI hardware ID(s) to passthrough to the VM (e.g., 0000:c7:00.0). Can be specified multiple times.')
|
network_group.add_argument('-P', '--pci', action='append', help='PCI hardware ID(s) to passthrough to the VM (e.g., 0000:c7:00.0). Can be specified multiple times.')
|
||||||
|
network_group.add_argument('--nsm-size', type=int, help='Size in GB for NSM volume creation. If both --pci and --nsm-size are specified, --pci takes precedence.')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -621,6 +680,8 @@ def main():
|
|||||||
hw_config.append(f"{args.memory}MB RAM")
|
hw_config.append(f"{args.memory}MB RAM")
|
||||||
if args.pci:
|
if args.pci:
|
||||||
hw_config.append(f"PCI devices: {', '.join(args.pci)}")
|
hw_config.append(f"PCI devices: {', '.join(args.pci)}")
|
||||||
|
if args.nsm_size:
|
||||||
|
hw_config.append(f"NSM volume: {args.nsm_size}GB")
|
||||||
hw_string = f" and hardware config: {', '.join(hw_config)}" if hw_config else ""
|
hw_string = f" and hardware config: {', '.join(hw_config)}" if hw_config else ""
|
||||||
|
|
||||||
logger.info(f"Received request to create VM '{args.vm_name}' using profile '{args.profile}' {network_config}{hw_string}")
|
logger.info(f"Received request to create VM '{args.vm_name}' using profile '{args.profile}' {network_config}{hw_string}")
|
||||||
@@ -643,8 +704,39 @@ def main():
|
|||||||
# Step 2: Provision the VM (without starting it)
|
# Step 2: Provision the VM (without starting it)
|
||||||
call_salt_cloud(args.profile, args.vm_name)
|
call_salt_cloud(args.profile, args.vm_name)
|
||||||
|
|
||||||
# Step 3: Modify hardware configuration
|
# Step 3: Determine storage configuration approach
|
||||||
run_qcow2_modify_hardware_config(args.profile, args.vm_name, cpu=args.cpu, memory=args.memory, pci_list=args.pci, start=True)
|
# Priority: disk passthrough (--pci) > volume creation (--nsm-size)
|
||||||
|
use_disk_passthrough = False
|
||||||
|
use_volume_creation = False
|
||||||
|
|
||||||
|
if args.pci:
|
||||||
|
use_disk_passthrough = True
|
||||||
|
logger.info("Using disk passthrough (--pci parameter specified)")
|
||||||
|
if args.nsm_size:
|
||||||
|
logger.warning(f"Both --pci and --nsm-size specified. Using --pci (disk passthrough) and ignoring --nsm-size={args.nsm_size}GB")
|
||||||
|
elif args.nsm_size:
|
||||||
|
use_volume_creation = True
|
||||||
|
# Validate nsm_size
|
||||||
|
if args.nsm_size <= 0:
|
||||||
|
logger.error(f"Invalid nsm_size value: {args.nsm_size}. Must be a positive integer.")
|
||||||
|
sys.exit(1)
|
||||||
|
logger.info(f"Using volume creation with size {args.nsm_size}GB (--nsm-size parameter specified)")
|
||||||
|
|
||||||
|
# Step 4: Configure hardware based on storage approach
|
||||||
|
if use_disk_passthrough:
|
||||||
|
# Use existing disk passthrough logic via modify_hardware_config
|
||||||
|
run_qcow2_modify_hardware_config(args.profile, args.vm_name, cpu=args.cpu, memory=args.memory, pci_list=args.pci, start=True)
|
||||||
|
elif use_volume_creation:
|
||||||
|
# Use new volume creation logic
|
||||||
|
run_qcow2_create_volume_config(args.profile, args.vm_name, size_gb=args.nsm_size, cpu=args.cpu, memory=args.memory, start=True)
|
||||||
|
else:
|
||||||
|
# No storage configuration, just configure CPU/memory if specified
|
||||||
|
if args.cpu or args.memory:
|
||||||
|
run_qcow2_modify_hardware_config(args.profile, args.vm_name, cpu=args.cpu, memory=args.memory, pci_list=None, start=True)
|
||||||
|
else:
|
||||||
|
# No hardware configuration needed, just start the VM
|
||||||
|
logger.info(f"No hardware configuration specified, starting VM {args.vm_name}")
|
||||||
|
run_qcow2_modify_hardware_config(args.profile, args.vm_name, cpu=None, memory=None, pci_list=None, start=True)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.error("so-salt-cloud: Operation cancelled by user.")
|
logger.error("so-salt-cloud: Operation cancelled by user.")
|
||||||
|
|||||||
@@ -633,6 +633,35 @@ def process_vm_creation(hypervisor_path: str, vm_config: dict) -> None:
|
|||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"Failed to emit success status event: {e}")
|
logger.error(f"Failed to emit success status event: {e}")
|
||||||
|
|
||||||
|
# Validate nsm_size if present
|
||||||
|
if 'nsm_size' in vm_config:
|
||||||
|
try:
|
||||||
|
size = int(vm_config['nsm_size'])
|
||||||
|
if size <= 0:
|
||||||
|
log.error("VM: %s - nsm_size must be a positive integer, got: %d", vm_name, size)
|
||||||
|
mark_invalid_hardware(hypervisor_path, vm_name, vm_config,
|
||||||
|
{'nsm_size': 'Invalid nsm_size: must be positive integer'})
|
||||||
|
return
|
||||||
|
if size > 10000: # 10TB reasonable maximum
|
||||||
|
log.error("VM: %s - nsm_size %dGB exceeds reasonable maximum (10000GB)", vm_name, size)
|
||||||
|
mark_invalid_hardware(hypervisor_path, vm_name, vm_config,
|
||||||
|
{'nsm_size': f'Invalid nsm_size: {size}GB exceeds maximum (10000GB)'})
|
||||||
|
return
|
||||||
|
log.debug("VM: %s - nsm_size validated: %dGB", vm_name, size)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
log.error("VM: %s - nsm_size must be a valid integer, got: %s", vm_name, vm_config.get('nsm_size'))
|
||||||
|
mark_invalid_hardware(hypervisor_path, vm_name, vm_config,
|
||||||
|
{'nsm_size': 'Invalid nsm_size: must be valid integer'})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for conflicting storage configurations
|
||||||
|
has_disk = 'disk' in vm_config and vm_config['disk']
|
||||||
|
has_nsm_size = 'nsm_size' in vm_config and vm_config['nsm_size']
|
||||||
|
|
||||||
|
if has_disk and has_nsm_size:
|
||||||
|
log.warning("VM: %s - Both disk and nsm_size specified. disk takes precedence, nsm_size will be ignored.",
|
||||||
|
vm_name)
|
||||||
|
|
||||||
# Initial hardware validation against model
|
# Initial hardware validation against model
|
||||||
is_valid, errors = validate_hardware_request(model_config, vm_config)
|
is_valid, errors = validate_hardware_request(model_config, vm_config)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
@@ -668,6 +697,11 @@ def process_vm_creation(hypervisor_path: str, vm_config: dict) -> None:
|
|||||||
if 'memory' in vm_config:
|
if 'memory' in vm_config:
|
||||||
memory_mib = int(vm_config['memory']) * 1024
|
memory_mib = int(vm_config['memory']) * 1024
|
||||||
cmd.extend(['-m', str(memory_mib)])
|
cmd.extend(['-m', str(memory_mib)])
|
||||||
|
|
||||||
|
# Add nsm_size if specified and disk is not specified
|
||||||
|
if 'nsm_size' in vm_config and vm_config['nsm_size'] and not ('disk' in vm_config and vm_config['disk']):
|
||||||
|
cmd.extend(['--nsm-size', str(vm_config['nsm_size'])])
|
||||||
|
log.debug("VM: %s - Adding nsm_size parameter: %s", vm_name, vm_config['nsm_size'])
|
||||||
|
|
||||||
# Add PCI devices
|
# Add PCI devices
|
||||||
for hw_type in ['disk', 'copper', 'sfp']:
|
for hw_type in ['disk', 'copper', 'sfp']:
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ hypervisor:
|
|||||||
required: true
|
required: true
|
||||||
readonly: true
|
readonly: true
|
||||||
forcedType: int
|
forcedType: int
|
||||||
|
- field: nsm_size
|
||||||
|
label: "Size of /nsm, in GB. Only used if there is not a passthrough disk."
|
||||||
|
forcedType: int
|
||||||
|
readonly: true
|
||||||
- field: disk
|
- field: disk
|
||||||
label: "Disk(s) for passthrough. Free: FREE | Total: TOTAL"
|
label: "Disk(s) for passthrough to /nsm. Free: FREE | Total: TOTAL"
|
||||||
readonly: true
|
readonly: true
|
||||||
options: []
|
options: []
|
||||||
forcedType: '[]int'
|
forcedType: '[]int'
|
||||||
|
|||||||
Reference in New Issue
Block a user