mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 17:22:49 +01:00
create predicatble interfaces
This commit is contained in:
@@ -38,7 +38,7 @@ def modify_network_config(image, interface, mode, ip4=None, gw4=None, dns4=None,
|
|||||||
image
|
image
|
||||||
Path to the QCOW2 image file that will be modified
|
Path to the QCOW2 image file that will be modified
|
||||||
interface
|
interface
|
||||||
Network interface name to configure (e.g., 'eth0')
|
Network interface name to configure (e.g., 'enp1s0')
|
||||||
mode
|
mode
|
||||||
Network configuration mode, either 'dhcp4' or 'static4'
|
Network configuration mode, either 'dhcp4' or 'static4'
|
||||||
ip4
|
ip4
|
||||||
@@ -57,13 +57,13 @@ def modify_network_config(image, interface, mode, ip4=None, gw4=None, dns4=None,
|
|||||||
Examples:
|
Examples:
|
||||||
1. **Configure DHCP:**
|
1. **Configure DHCP:**
|
||||||
```bash
|
```bash
|
||||||
salt '*' qcow2.modify_network_config image='/nsm/libvirt/images/sool9/sool9.qcow2' interface='eth0' mode='dhcp4'
|
salt '*' qcow2.modify_network_config image='/nsm/libvirt/images/sool9/sool9.qcow2' interface='enp1s0' mode='dhcp4'
|
||||||
```
|
```
|
||||||
This configures eth0 to use DHCP for IP assignment
|
This configures enp1s0 to use DHCP for IP assignment
|
||||||
|
|
||||||
2. **Configure Static IP:**
|
2. **Configure Static IP:**
|
||||||
```bash
|
```bash
|
||||||
salt '*' qcow2.modify_network_config image='/nsm/libvirt/images/sool9/sool9.qcow2' interface='eth0' mode='static4' ip4='192.168.1.10/24' gw4='192.168.1.1' dns4='192.168.1.1,8.8.8.8' search4='example.local'
|
salt '*' qcow2.modify_network_config image='/nsm/libvirt/images/sool9/sool9.qcow2' interface='enp1s0' mode='static4' ip4='192.168.1.10/24' gw4='192.168.1.1' dns4='192.168.1.1,8.8.8.8' search4='example.local'
|
||||||
```
|
```
|
||||||
This sets a static IP configuration with DNS servers and search domain
|
This sets a static IP configuration with DNS servers and search domain
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ The script offers two main configuration modes:
|
|||||||
1. DHCP Configuration: Enable automatic IP address assignment
|
1. DHCP Configuration: Enable automatic IP address assignment
|
||||||
2. Static IP Configuration: Set specific IP address, gateway, DNS servers, and search domains
|
2. Static IP Configuration: Set specific IP address, gateway, DNS servers, and search domains
|
||||||
|
|
||||||
|
For both configuration modes, the script automatically sets the following NetworkManager connection properties:
|
||||||
|
- connection.autoconnect: yes (ensures interface connects automatically)
|
||||||
|
- connection.autoconnect-priority: 999 (sets connection priority)
|
||||||
|
- connection.autoconnect-retries: -1 (unlimited connection retries)
|
||||||
|
- connection.multi-connect: 0 (single connection mode)
|
||||||
|
- connection.wait-device-timeout: -1 (wait indefinitely for device)
|
||||||
|
|
||||||
This script is designed to work with Security Onion's virtualization infrastructure and is typically
|
This script is designed to work with Security Onion's virtualization infrastructure and is typically
|
||||||
used during VM provisioning and network reconfiguration tasks.
|
used during VM provisioning and network reconfiguration tasks.
|
||||||
|
|
||||||
@@ -23,7 +30,7 @@ used during VM provisioning and network reconfiguration tasks.
|
|||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
-I, --image Path to the QCOW2 image.
|
-I, --image Path to the QCOW2 image.
|
||||||
-i, --interface Network interface to modify (e.g., eth0).
|
-i, --interface Network interface to modify (e.g., enp1s0).
|
||||||
--dhcp4 Configure interface for DHCP (IPv4).
|
--dhcp4 Configure interface for DHCP (IPv4).
|
||||||
--static4 Configure interface for static IPv4 settings.
|
--static4 Configure interface for static IPv4 settings.
|
||||||
--ip4 IPv4 address (e.g., 192.168.1.10/24). Required for static IPv4 configuration.
|
--ip4 IPv4 address (e.g., 192.168.1.10/24). Required for static IPv4 configuration.
|
||||||
@@ -36,7 +43,7 @@ used during VM provisioning and network reconfiguration tasks.
|
|||||||
1. **Static IP Configuration with DNS and Search Domain:**
|
1. **Static IP Configuration with DNS and Search Domain:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i eth0 --static4 \
|
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i enp1s0 --static4 \
|
||||||
--ip4 192.168.1.10/24 --gw4 192.168.1.1 --dns4 192.168.1.1,192.168.1.2 --search4 example.local
|
--ip4 192.168.1.10/24 --gw4 192.168.1.1 --dns4 192.168.1.1,192.168.1.2 --search4 example.local
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ used during VM provisioning and network reconfiguration tasks.
|
|||||||
2. **DHCP Configuration:**
|
2. **DHCP Configuration:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i eth0 --dhcp4
|
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i enp1s0 --dhcp4
|
||||||
```
|
```
|
||||||
|
|
||||||
This command configures the network interface to use DHCP for automatic IP address assignment.
|
This command configures the network interface to use DHCP for automatic IP address assignment.
|
||||||
@@ -58,7 +65,7 @@ used during VM provisioning and network reconfiguration tasks.
|
|||||||
3. **Static IP Configuration without DNS Settings:**
|
3. **Static IP Configuration without DNS Settings:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i eth0 --static4 \
|
so-qcow2-modify-network -I /nsm/libvirt/images/sool9/sool9.qcow2 -i enp1s0 --static4 \
|
||||||
--ip4 192.168.1.20/24 --gw4 192.168.1.1
|
--ip4 192.168.1.20/24 --gw4 192.168.1.1
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -122,7 +129,9 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import configparser
|
import configparser
|
||||||
|
import uuid
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import libvirt
|
||||||
from so_logging_utils import setup_logging
|
from so_logging_utils import setup_logging
|
||||||
|
|
||||||
# Set up logging using the so_logging_utils library
|
# Set up logging using the so_logging_utils library
|
||||||
@@ -154,33 +163,91 @@ def validate_interface_name(interface_name):
|
|||||||
if not re.match(r'^[a-zA-Z0-9_\-]+$', interface_name):
|
if not re.match(r'^[a-zA-Z0-9_\-]+$', interface_name):
|
||||||
raise ValueError(f"Invalid interface name: {interface_name}")
|
raise ValueError(f"Invalid interface name: {interface_name}")
|
||||||
|
|
||||||
def update_ipv4_section(content, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
def check_base_domain_status(image_path):
|
||||||
|
"""
|
||||||
|
Check if the base domain corresponding to the image path is currently running.
|
||||||
|
Base domains should not be running when modifying their configuration.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image_path (str): Path to the QCOW2 image.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the base domain is running, False otherwise.
|
||||||
|
"""
|
||||||
|
base_domain = os.path.basename(os.path.dirname(image_path))
|
||||||
|
logger.info(f"Verifying base domain status for image: {image_path}")
|
||||||
|
logger.info(f"Checking if base domain '{base_domain}' is running...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = libvirt.open('qemu:///system')
|
||||||
|
try:
|
||||||
|
dom = conn.lookupByName(base_domain)
|
||||||
|
is_running = dom.isActive()
|
||||||
|
if is_running:
|
||||||
|
logger.error(f"Base domain '{base_domain}' is running - cannot modify configuration")
|
||||||
|
return is_running
|
||||||
|
except libvirt.libvirtError:
|
||||||
|
logger.info(f"Base domain '{base_domain}' not found or not running")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
logger.error(f"Failed to connect to libvirt: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_network_config(content, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
||||||
config = configparser.ConfigParser(strict=False)
|
config = configparser.ConfigParser(strict=False)
|
||||||
config.optionxform = str
|
config.optionxform = str
|
||||||
config.read_string(content)
|
config.read_string(content)
|
||||||
|
|
||||||
|
# Ensure connection section exists and set required properties
|
||||||
|
if 'connection' not in config.sections():
|
||||||
|
logger.info("Creating new connection section in network configuration")
|
||||||
|
config.add_section('connection')
|
||||||
|
|
||||||
|
# Set mandatory connection properties
|
||||||
|
config.set('connection', 'autoconnect', 'yes')
|
||||||
|
config.set('connection', 'autoconnect-priority', '999')
|
||||||
|
config.set('connection', 'autoconnect-retries', '-1')
|
||||||
|
config.set('connection', 'multi-connect', '0')
|
||||||
|
config.set('connection', 'wait-device-timeout', '-1')
|
||||||
|
|
||||||
|
# Ensure ipv4 section exists
|
||||||
if 'ipv4' not in config.sections():
|
if 'ipv4' not in config.sections():
|
||||||
|
logger.info("Creating new IPv4 section in network configuration")
|
||||||
config.add_section('ipv4')
|
config.add_section('ipv4')
|
||||||
|
|
||||||
if mode == "dhcp4":
|
if mode == "dhcp4":
|
||||||
|
logger.info("Configuring DHCP settings:")
|
||||||
|
logger.info(" method: auto (DHCP enabled)")
|
||||||
|
logger.info(" Removing any existing static configuration")
|
||||||
config.set('ipv4', 'method', 'auto')
|
config.set('ipv4', 'method', 'auto')
|
||||||
config.remove_option('ipv4', 'address1')
|
config.remove_option('ipv4', 'address1')
|
||||||
config.remove_option('ipv4', 'addresses')
|
config.remove_option('ipv4', 'addresses')
|
||||||
config.remove_option('ipv4', 'dns')
|
config.remove_option('ipv4', 'dns')
|
||||||
config.remove_option('ipv4', 'dns-search')
|
config.remove_option('ipv4', 'dns-search')
|
||||||
elif mode == "static4":
|
elif mode == "static4":
|
||||||
|
logger.info("Configuring static IP settings:")
|
||||||
|
logger.info(" method: manual (static configuration)")
|
||||||
config.set('ipv4', 'method', 'manual')
|
config.set('ipv4', 'method', 'manual')
|
||||||
if ip and gateway:
|
if ip and gateway:
|
||||||
|
logger.info(f" Setting address: {ip}")
|
||||||
|
logger.info(f" Setting gateway: {gateway}")
|
||||||
config.set('ipv4', 'address1', f"{ip},{gateway}")
|
config.set('ipv4', 'address1', f"{ip},{gateway}")
|
||||||
else:
|
else:
|
||||||
|
logger.error("Missing required IP address or gateway for static configuration")
|
||||||
raise ValueError("Both IP address and gateway are required for static configuration.")
|
raise ValueError("Both IP address and gateway are required for static configuration.")
|
||||||
if dns:
|
if dns:
|
||||||
|
logger.info(f" Setting DNS servers: {dns}")
|
||||||
config.set('ipv4', 'dns', f"{dns};")
|
config.set('ipv4', 'dns', f"{dns};")
|
||||||
else:
|
else:
|
||||||
|
logger.info(" No DNS servers specified")
|
||||||
config.remove_option('ipv4', 'dns')
|
config.remove_option('ipv4', 'dns')
|
||||||
if search_domain:
|
if search_domain:
|
||||||
|
logger.info(f" Setting search domain: {search_domain}")
|
||||||
config.set('ipv4', 'dns-search', f"{search_domain};")
|
config.set('ipv4', 'dns-search', f"{search_domain};")
|
||||||
else:
|
else:
|
||||||
|
logger.info(" No search domain specified")
|
||||||
config.remove_option('ipv4', 'dns-search')
|
config.remove_option('ipv4', 'dns-search')
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid mode '{mode}'. Expected 'dhcp4' or 'static4'.")
|
raise ValueError(f"Invalid mode '{mode}'. Expected 'dhcp4' or 'static4'.")
|
||||||
@@ -193,50 +260,138 @@ def update_ipv4_section(content, mode, ip=None, gateway=None, dns=None, search_d
|
|||||||
return updated_content
|
return updated_content
|
||||||
|
|
||||||
def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
||||||
|
"""
|
||||||
|
Modifies network configuration in a QCOW2 image, ensuring specific connection settings are set.
|
||||||
|
|
||||||
|
Handles both eth0 and predictable network interface names (e.g., enp1s0).
|
||||||
|
If the requested interface configuration is not found but eth0.nmconnection exists,
|
||||||
|
it will be renamed and updated with the proper interface configuration.
|
||||||
|
"""
|
||||||
|
# Check if base domain is running
|
||||||
|
if check_base_domain_status(image_path):
|
||||||
|
raise RuntimeError("Cannot modify network configuration while base domain is running")
|
||||||
|
|
||||||
if not os.access(image_path, os.W_OK):
|
if not os.access(image_path, os.W_OK):
|
||||||
|
logger.error(f"Permission denied: Cannot write to image file {image_path}")
|
||||||
raise PermissionError(f"Write permission denied for image file: {image_path}")
|
raise PermissionError(f"Write permission denied for image file: {image_path}")
|
||||||
|
|
||||||
|
logger.info(f"Configuring network for VM image: {image_path}")
|
||||||
|
logger.info(f"Network configuration details for interface {interface}:")
|
||||||
|
logger.info(f" Mode: {mode.upper()}")
|
||||||
|
if mode == "static4":
|
||||||
|
logger.info(f" IP Address: {ip}")
|
||||||
|
logger.info(f" Gateway: {gateway}")
|
||||||
|
logger.info(f" DNS Servers: {dns if dns else 'Not configured'}")
|
||||||
|
logger.info(f" Search Domain: {search_domain if search_domain else 'Not configured'}")
|
||||||
|
|
||||||
g = guestfs.GuestFS(python_return_dict=True)
|
g = guestfs.GuestFS(python_return_dict=True)
|
||||||
try:
|
try:
|
||||||
|
logger.info("Initializing GuestFS and mounting image...")
|
||||||
g.set_network(False)
|
g.set_network(False)
|
||||||
g.selinux = False
|
g.selinux = False
|
||||||
g.add_drive_opts(image_path, format="qcow2")
|
g.add_drive_opts(image_path, format="qcow2")
|
||||||
g.launch()
|
g.launch()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
logger.error(f"Failed to initialize GuestFS: {e}")
|
||||||
raise RuntimeError(f"Failed to initialize GuestFS or launch appliance: {e}")
|
raise RuntimeError(f"Failed to initialize GuestFS or launch appliance: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os_list = g.inspect_os()
|
os_list = g.inspect_os()
|
||||||
if not os_list:
|
if not os_list:
|
||||||
|
logger.error(f"No operating system found in image: {image_path}")
|
||||||
raise RuntimeError(f"Unable to find any OS in {image_path}.")
|
raise RuntimeError(f"Unable to find any OS in {image_path}.")
|
||||||
|
|
||||||
root_fs = os_list[0]
|
root_fs = os_list[0]
|
||||||
try:
|
try:
|
||||||
g.mount(root_fs, "/")
|
g.mount(root_fs, "/")
|
||||||
|
logger.info("Successfully mounted VM image filesystem")
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
logger.error(f"Failed to mount filesystem: {e}")
|
||||||
raise RuntimeError(f"Failed to mount the filesystem: {e}")
|
raise RuntimeError(f"Failed to mount the filesystem: {e}")
|
||||||
|
|
||||||
if not g.is_dir(NETWORK_CONFIG_DIR):
|
if not g.is_dir(NETWORK_CONFIG_DIR):
|
||||||
|
logger.error(f"NetworkManager configuration directory not found: {NETWORK_CONFIG_DIR}")
|
||||||
raise FileNotFoundError(f"NetworkManager configuration directory not found in the image at {NETWORK_CONFIG_DIR}.")
|
raise FileNotFoundError(f"NetworkManager configuration directory not found in the image at {NETWORK_CONFIG_DIR}.")
|
||||||
|
|
||||||
config_file_path = f"{NETWORK_CONFIG_DIR}/{interface}.nmconnection"
|
requested_config_path = f"{NETWORK_CONFIG_DIR}/{interface}.nmconnection"
|
||||||
|
eth0_config_path = f"{NETWORK_CONFIG_DIR}/eth0.nmconnection"
|
||||||
|
config_file_path = None
|
||||||
|
current_content = None
|
||||||
|
|
||||||
|
# Try to read the requested interface config first
|
||||||
try:
|
try:
|
||||||
file_content = g.read_file(config_file_path)
|
file_content = g.read_file(requested_config_path)
|
||||||
current_content = file_content.decode('utf-8')
|
current_content = file_content.decode('utf-8')
|
||||||
|
config_file_path = requested_config_path
|
||||||
|
logger.info(f"Found existing network configuration for interface {interface}")
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise FileNotFoundError(f"Configuration file for {interface} not found at {config_file_path}.")
|
# If not found, try eth0 config
|
||||||
|
try:
|
||||||
|
file_content = g.read_file(eth0_config_path)
|
||||||
|
current_content = file_content.decode('utf-8')
|
||||||
|
config_file_path = eth0_config_path
|
||||||
|
logger.info("Found eth0 network configuration, will update for new interface")
|
||||||
|
except RuntimeError:
|
||||||
|
logger.error(f"No network configuration found for either {interface} or eth0")
|
||||||
|
raise FileNotFoundError(f"No network configuration found at {requested_config_path} or {eth0_config_path}")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise ValueError(f"Failed to decode the configuration file for {interface}.")
|
logger.error(f"Failed to decode network configuration file")
|
||||||
|
raise ValueError(f"Failed to decode the configuration file")
|
||||||
|
|
||||||
updated_content = update_ipv4_section(current_content, mode, ip, gateway, dns, search_domain)
|
# If using eth0 config, update interface-specific fields
|
||||||
|
if config_file_path == eth0_config_path:
|
||||||
|
config = configparser.ConfigParser(strict=False)
|
||||||
|
config.optionxform = str
|
||||||
|
config.read_string(current_content)
|
||||||
|
|
||||||
|
if 'connection' not in config.sections():
|
||||||
|
config.add_section('connection')
|
||||||
|
|
||||||
|
# Update interface-specific fields
|
||||||
|
config.set('connection', 'id', interface)
|
||||||
|
config.set('connection', 'interface-name', interface)
|
||||||
|
config.set('connection', 'uuid', str(uuid.uuid4()))
|
||||||
|
|
||||||
|
# Write updated content back to string
|
||||||
|
output = StringIO()
|
||||||
|
config.write(output, space_around_delimiters=False)
|
||||||
|
current_content = output.getvalue()
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
# Update config file path to new interface name
|
||||||
|
config_file_path = requested_config_path
|
||||||
|
|
||||||
|
logger.info("Applying network configuration changes...")
|
||||||
|
updated_content = update_network_config(current_content, mode, ip, gateway, dns, search_domain)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
g.write(config_file_path, updated_content.encode('utf-8'))
|
g.write(config_file_path, updated_content.encode('utf-8'))
|
||||||
|
# Set proper permissions (600) on the network configuration file
|
||||||
|
g.chmod(0o600, config_file_path)
|
||||||
|
logger.info("Successfully wrote updated network configuration with proper permissions (600)")
|
||||||
|
|
||||||
|
# If we renamed eth0 to the new interface, remove the old eth0 config
|
||||||
|
if config_file_path == requested_config_path and eth0_config_path != requested_config_path:
|
||||||
|
try:
|
||||||
|
g.rm(eth0_config_path)
|
||||||
|
logger.info("Removed old eth0 configuration file")
|
||||||
|
except RuntimeError:
|
||||||
|
logger.warning("Could not remove old eth0 configuration file - it may have already been removed")
|
||||||
|
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
logger.error(f"Failed to write network configuration: {e}")
|
||||||
raise IOError(f"Failed to write updated configuration to {config_file_path}: {e}")
|
raise IOError(f"Failed to write updated configuration to {config_file_path}: {e}")
|
||||||
|
|
||||||
logger.info(f"Updated {interface} network configuration in {image_path} using {mode.upper()} mode.")
|
logger.info(f"Successfully updated network configuration:")
|
||||||
|
logger.info(f" Image: {image_path}")
|
||||||
|
logger.info(f" Interface: {interface}")
|
||||||
|
logger.info(f" Mode: {mode.upper()}")
|
||||||
|
if mode == "static4":
|
||||||
|
logger.info(f" Settings applied:")
|
||||||
|
logger.info(f" IP Address: {ip}")
|
||||||
|
logger.info(f" Gateway: {gateway}")
|
||||||
|
logger.info(f" DNS Servers: {dns if dns else 'Not configured'}")
|
||||||
|
logger.info(f" Search Domain: {search_domain if search_domain else 'Not configured'}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
@@ -247,7 +402,7 @@ def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dn
|
|||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
parser = argparse.ArgumentParser(description="Modify IPv4 settings in a QCOW2 image for a specified network interface.")
|
parser = argparse.ArgumentParser(description="Modify IPv4 settings in a QCOW2 image for a specified network interface.")
|
||||||
parser.add_argument("-I", "--image", required=True, help="Path to the QCOW2 image.")
|
parser.add_argument("-I", "--image", required=True, help="Path to the QCOW2 image.")
|
||||||
parser.add_argument("-i", "--interface", required=True, help="Network interface to modify (e.g., eth0).")
|
parser.add_argument("-i", "--interface", required=True, help="Network interface to modify (e.g., enp1s0).")
|
||||||
group = parser.add_mutually_exclusive_group(required=True)
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
group.add_argument("--dhcp4", action="store_true", help="Configure interface for DHCP (IPv4).")
|
group.add_argument("--dhcp4", action="store_true", help="Configure interface for DHCP (IPv4).")
|
||||||
group.add_argument("--static4", action="store_true", help="Configure interface for static IPv4 settings.")
|
group.add_argument("--static4", action="store_true", help="Configure interface for static IPv4 settings.")
|
||||||
@@ -265,29 +420,41 @@ def parse_arguments():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
|
logger.info("Starting network configuration update...")
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
|
logger.info("Validating interface name...")
|
||||||
validate_interface_name(args.interface)
|
validate_interface_name(args.interface)
|
||||||
|
|
||||||
if args.dhcp4:
|
if args.dhcp4:
|
||||||
mode = "dhcp4"
|
mode = "dhcp4"
|
||||||
|
logger.info("Using DHCP configuration mode")
|
||||||
elif args.static4:
|
elif args.static4:
|
||||||
mode = "static4"
|
mode = "static4"
|
||||||
|
logger.info("Using static IP configuration mode")
|
||||||
if not args.ip4 or not args.gw4:
|
if not args.ip4 or not args.gw4:
|
||||||
|
logger.error("Missing required parameters for static configuration")
|
||||||
raise ValueError("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
raise ValueError("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
||||||
|
|
||||||
|
logger.info("Validating IP addresses...")
|
||||||
validate_ip_address(args.ip4, description="IPv4 address")
|
validate_ip_address(args.ip4, description="IPv4 address")
|
||||||
validate_ip_address(args.gw4, description="IPv4 gateway")
|
validate_ip_address(args.gw4, description="IPv4 gateway")
|
||||||
if args.dns4:
|
if args.dns4:
|
||||||
validate_dns_addresses(args.dns4)
|
validate_dns_addresses(args.dns4)
|
||||||
else:
|
else:
|
||||||
|
logger.error("No configuration mode specified")
|
||||||
raise ValueError("Either --dhcp4 or --static4 must be specified.")
|
raise ValueError("Either --dhcp4 or --static4 must be specified.")
|
||||||
|
|
||||||
modify_network_config(args.image, args.interface, mode, args.ip4, args.gw4, args.dns4, args.search4)
|
modify_network_config(args.image, args.interface, mode, args.ip4, args.gw4, args.dns4, args.search4)
|
||||||
|
logger.info("Network configuration update completed successfully")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.error("Operation cancelled by user.")
|
logger.error("Operation cancelled by user.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if "base domain is running" in str(e):
|
||||||
|
logger.error("Cannot proceed: Base domain must not be running when modifying network configuration")
|
||||||
|
else:
|
||||||
logger.error(f"An error occurred: {e}")
|
logger.error(f"An error occurred: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
378
salt/hypervisor/tools/sbin/so-qcow2-network-predictable
Normal file
378
salt/hypervisor/tools/sbin/so-qcow2-network-predictable
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Script for configuring network interface predictability in Security Onion VMs.
|
||||||
|
This script modifies the necessary files to ensure consistent network interface naming.
|
||||||
|
|
||||||
|
The script performs the following operations:
|
||||||
|
1. Modifies the BLS entry to set net.ifnames=1
|
||||||
|
2. Removes any existing persistent network rules
|
||||||
|
3. Updates GRUB configuration
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
so-qcow2-network-predictable -n <domain_name> [-I <qcow2_image_path>]
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
-n, --name Domain name of the VM to configure
|
||||||
|
-I, --image (Optional) Path to the QCOW2 image. If not provided,
|
||||||
|
defaults to /nsm/libvirt/images/<domain_name>/<domain_name>.qcow2
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
1. **Configure using domain name:**
|
||||||
|
```bash
|
||||||
|
so-qcow2-network-predictable -n sool9
|
||||||
|
```
|
||||||
|
This command will:
|
||||||
|
- Use default image path: /nsm/libvirt/images/sool9/sool9.qcow2
|
||||||
|
- Configure network interface predictability
|
||||||
|
|
||||||
|
2. **Configure using custom image path:**
|
||||||
|
```bash
|
||||||
|
so-qcow2-network-predictable -n sool9 -I /path/to/custom/image.qcow2
|
||||||
|
```
|
||||||
|
This command will:
|
||||||
|
- Use the specified image path
|
||||||
|
- Configure network interface predictability
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- The VM must not be running when executing this script
|
||||||
|
- Requires root privileges
|
||||||
|
- Will automatically find and modify the appropriate BLS entry
|
||||||
|
- Removes /etc/udev/rules.d/70-persistent-net.rules if it exists
|
||||||
|
- Updates GRUB configuration after changes
|
||||||
|
|
||||||
|
**Exit Codes:**
|
||||||
|
- 0: Success
|
||||||
|
- 1: General error (invalid arguments, file operations, etc.)
|
||||||
|
- 2: VM is running
|
||||||
|
- 3: Required files not found
|
||||||
|
- 4: Permission denied
|
||||||
|
|
||||||
|
**Logging:**
|
||||||
|
- Logs are written to /opt/so/log/hypervisor/so-qcow2-network-predictable.log
|
||||||
|
- Both file and console logging are enabled
|
||||||
|
- Log entries include:
|
||||||
|
- Timestamps
|
||||||
|
- Operation details
|
||||||
|
- Error messages
|
||||||
|
- Configuration changes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import guestfs
|
||||||
|
import glob
|
||||||
|
import libvirt
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from so_logging_utils import setup_logging
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logger = setup_logging(
|
||||||
|
logger_name='so-qcow2-network-predictable',
|
||||||
|
log_file_path='/opt/so/log/hypervisor/so-qcow2-network-predictable.log',
|
||||||
|
log_level=logging.INFO,
|
||||||
|
format_str='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_domain_status(domain_name):
|
||||||
|
"""
|
||||||
|
Check if the specified domain exists and is not running.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the libvirt domain to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if domain exists and is not running, False otherwise
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If domain is running or connection to libvirt fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = libvirt.open('qemu:///system')
|
||||||
|
try:
|
||||||
|
dom = conn.lookupByName(domain_name)
|
||||||
|
is_running = dom.isActive()
|
||||||
|
if is_running:
|
||||||
|
logger.error(f"Domain '{domain_name}' is running - cannot modify configuration")
|
||||||
|
raise RuntimeError(f"Domain '{domain_name}' must not be running")
|
||||||
|
logger.info(f"Domain '{domain_name}' exists and is not running")
|
||||||
|
return True
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
if "no domain with matching name" in str(e):
|
||||||
|
logger.error(f"Domain '{domain_name}' not found")
|
||||||
|
raise RuntimeError(f"Domain '{domain_name}' not found")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
logger.error(f"Failed to connect to libvirt: {e}")
|
||||||
|
raise RuntimeError(f"Failed to connect to libvirt: {e}")
|
||||||
|
|
||||||
|
def modify_bls_entry(g):
|
||||||
|
"""
|
||||||
|
Find and modify the BLS entry to set net.ifnames=1.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
g: Mounted guestfs handle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False if no changes needed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If BLS entry cannot be found or modified
|
||||||
|
"""
|
||||||
|
bls_dir = "/boot/loader/entries"
|
||||||
|
logger.info(f"Checking BLS directory: {bls_dir}")
|
||||||
|
if g.is_dir(bls_dir):
|
||||||
|
logger.info("BLS directory exists")
|
||||||
|
else:
|
||||||
|
logger.info("Listing /boot contents:")
|
||||||
|
try:
|
||||||
|
boot_contents = g.ls("/boot")
|
||||||
|
logger.info(f"/boot contains: {boot_contents}")
|
||||||
|
if g.is_dir("/boot/loader"):
|
||||||
|
logger.info("Listing /boot/loader contents:")
|
||||||
|
loader_contents = g.ls("/boot/loader")
|
||||||
|
logger.info(f"/boot/loader contains: {loader_contents}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing /boot contents: {e}")
|
||||||
|
raise RuntimeError(f"BLS directory not found: {bls_dir}")
|
||||||
|
|
||||||
|
# Find BLS entry file
|
||||||
|
entries = g.glob_expand(f"{bls_dir}/*.conf")
|
||||||
|
logger.info(f"Found BLS entries: {entries}")
|
||||||
|
if not entries:
|
||||||
|
logger.error("No BLS entry files found")
|
||||||
|
raise RuntimeError("No BLS entry files found")
|
||||||
|
|
||||||
|
# Use the first entry found
|
||||||
|
bls_file = entries[0]
|
||||||
|
logger.info(f"Found BLS entry file: {bls_file}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Reading BLS file contents from: {bls_file}")
|
||||||
|
content = g.read_file(bls_file).decode('utf-8')
|
||||||
|
logger.info("Current BLS file content:")
|
||||||
|
logger.info("---BEGIN BLS CONTENT---")
|
||||||
|
logger.info(content)
|
||||||
|
logger.info("---END BLS CONTENT---")
|
||||||
|
|
||||||
|
lines = content.splitlines()
|
||||||
|
modified = False
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('options '):
|
||||||
|
logger.info(f"Found options line: {line}")
|
||||||
|
# Check if net.ifnames parameter exists
|
||||||
|
if 'net.ifnames=' in line:
|
||||||
|
# Replace existing parameter
|
||||||
|
new_line = re.sub(r'net\.ifnames=[01]', 'net.ifnames=1', line)
|
||||||
|
if new_line != line:
|
||||||
|
lines[i] = new_line
|
||||||
|
modified = True
|
||||||
|
logger.info(f"Updated existing net.ifnames parameter to 1. New line: {new_line}")
|
||||||
|
else:
|
||||||
|
# Add parameter
|
||||||
|
lines[i] = f"{line} net.ifnames=1"
|
||||||
|
modified = True
|
||||||
|
logger.info(f"Added net.ifnames=1 parameter. New line: {lines[i]}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
new_content = '\n'.join(lines) + '\n'
|
||||||
|
logger.info("New BLS file content:")
|
||||||
|
logger.info("---BEGIN NEW BLS CONTENT---")
|
||||||
|
logger.info(new_content)
|
||||||
|
logger.info("---END NEW BLS CONTENT---")
|
||||||
|
g.write(bls_file, new_content.encode('utf-8'))
|
||||||
|
logger.info("Successfully updated BLS entry")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info("No changes needed for BLS entry")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to modify BLS entry: {e}")
|
||||||
|
raise RuntimeError(f"Failed to modify BLS entry: {e}")
|
||||||
|
|
||||||
|
def remove_persistent_net_rules(g):
|
||||||
|
"""
|
||||||
|
Remove the persistent network rules file if it exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
g: Mounted guestfs handle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if file was removed, False if it didn't exist
|
||||||
|
"""
|
||||||
|
rules_file = "/etc/udev/rules.d/70-persistent-net.rules"
|
||||||
|
logger.info(f"Checking for persistent network rules file: {rules_file}")
|
||||||
|
try:
|
||||||
|
if g.is_file(rules_file):
|
||||||
|
logger.info("Found persistent network rules file, removing...")
|
||||||
|
g.rm(rules_file)
|
||||||
|
logger.info(f"Successfully removed persistent network rules file: {rules_file}")
|
||||||
|
return True
|
||||||
|
logger.info("No persistent network rules file found")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove persistent network rules: {e}")
|
||||||
|
raise RuntimeError(f"Failed to remove persistent network rules: {e}")
|
||||||
|
|
||||||
|
def update_grub_config(g):
|
||||||
|
"""
|
||||||
|
Update GRUB configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
g: Mounted guestfs handle
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If GRUB update fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Updating GRUB configuration...")
|
||||||
|
output = g.command(['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'])
|
||||||
|
logger.info("GRUB update output:")
|
||||||
|
logger.info(output)
|
||||||
|
logger.info("Successfully updated GRUB configuration")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update GRUB configuration: {e}")
|
||||||
|
raise RuntimeError(f"Failed to update GRUB configuration: {e}")
|
||||||
|
|
||||||
|
def configure_network_predictability(domain_name, image_path=None):
|
||||||
|
"""
|
||||||
|
Configure network interface predictability for a VM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to configure
|
||||||
|
image_path (str, optional): Path to the QCOW2 image
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If configuration fails
|
||||||
|
"""
|
||||||
|
# Check domain status
|
||||||
|
check_domain_status(domain_name)
|
||||||
|
|
||||||
|
# Use default image path if none provided
|
||||||
|
if not image_path:
|
||||||
|
image_path = f"/nsm/libvirt/images/{domain_name}/{domain_name}.qcow2"
|
||||||
|
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
logger.error(f"Image file not found: {image_path}")
|
||||||
|
raise RuntimeError(f"Image file not found: {image_path}")
|
||||||
|
|
||||||
|
if not os.access(image_path, os.R_OK | os.W_OK):
|
||||||
|
logger.error(f"Permission denied: Cannot access image file {image_path}")
|
||||||
|
raise RuntimeError(f"Permission denied: Cannot access image file {image_path}")
|
||||||
|
|
||||||
|
logger.info(f"Configuring network predictability for domain: {domain_name}")
|
||||||
|
logger.info(f"Using image: {image_path}")
|
||||||
|
|
||||||
|
g = guestfs.GuestFS(python_return_dict=True)
|
||||||
|
try:
|
||||||
|
logger.info("Initializing guestfs...")
|
||||||
|
g.set_network(False)
|
||||||
|
g.selinux = False
|
||||||
|
g.add_drive_opts(image_path, format="qcow2")
|
||||||
|
g.launch()
|
||||||
|
|
||||||
|
logger.info("Inspecting operating system...")
|
||||||
|
roots = g.inspect_os()
|
||||||
|
if not roots:
|
||||||
|
raise RuntimeError("No operating system found in image")
|
||||||
|
|
||||||
|
root = roots[0]
|
||||||
|
logger.info(f"Found root filesystem: {root}")
|
||||||
|
logger.info(f"Operating system type: {g.inspect_get_type(root)}")
|
||||||
|
logger.info(f"Operating system distro: {g.inspect_get_distro(root)}")
|
||||||
|
logger.info(f"Operating system major version: {g.inspect_get_major_version(root)}")
|
||||||
|
logger.info(f"Operating system minor version: {g.inspect_get_minor_version(root)}")
|
||||||
|
|
||||||
|
logger.info("Getting mount points...")
|
||||||
|
mountpoints = g.inspect_get_mountpoints(root)
|
||||||
|
logger.info(f"Found mount points: {mountpoints}")
|
||||||
|
logger.info("Converting mount points to sortable list...")
|
||||||
|
# Convert dictionary to list of tuples
|
||||||
|
mountpoints = list(mountpoints.items())
|
||||||
|
logger.info(f"Converted mount points: {mountpoints}")
|
||||||
|
logger.info("Sorting mount points by path length for proper mount order...")
|
||||||
|
mountpoints.sort(key=lambda m: len(m[0]))
|
||||||
|
logger.info(f"Mount order will be: {[mp[0] for mp in mountpoints]}")
|
||||||
|
|
||||||
|
for mp_path, mp_device in mountpoints:
|
||||||
|
try:
|
||||||
|
logger.info(f"Attempting to mount {mp_device} at {mp_path}")
|
||||||
|
g.mount(mp_device, mp_path)
|
||||||
|
logger.info(f"Successfully mounted {mp_device} at {mp_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not mount {mp_device} at {mp_path}: {str(e)}")
|
||||||
|
# Continue with other mounts
|
||||||
|
|
||||||
|
# Perform configuration steps
|
||||||
|
bls_modified = modify_bls_entry(g)
|
||||||
|
rules_removed = remove_persistent_net_rules(g)
|
||||||
|
|
||||||
|
if bls_modified or rules_removed:
|
||||||
|
update_grub_config(g)
|
||||||
|
logger.info("Network predictability configuration completed successfully")
|
||||||
|
else:
|
||||||
|
logger.info("No changes were necessary")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Failed to configure network predictability: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
logger.info("Unmounting all filesystems...")
|
||||||
|
g.umount_all()
|
||||||
|
logger.info("Successfully unmounted all filesystems")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error unmounting filesystems: {e}")
|
||||||
|
g.close()
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""Parse command line arguments."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Configure network interface predictability for Security Onion VMs"
|
||||||
|
)
|
||||||
|
parser.add_argument("-n", "--name", required=True,
|
||||||
|
help="Domain name of the VM to configure")
|
||||||
|
parser.add_argument("-I", "--image",
|
||||||
|
help="Path to the QCOW2 image (optional)")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for the script."""
|
||||||
|
try:
|
||||||
|
args = parse_arguments()
|
||||||
|
configure_network_predictability(args.name, args.image)
|
||||||
|
sys.exit(0)
|
||||||
|
except RuntimeError as e:
|
||||||
|
if "must not be running" in str(e):
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(2)
|
||||||
|
elif "not found" in str(e):
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(3)
|
||||||
|
elif "Permission denied" in str(e):
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(4)
|
||||||
|
else:
|
||||||
|
logger.error(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.error("Operation cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
206
salt/hypervisor/tools/sbin/so-wait-cloud-init
Normal file
206
salt/hypervisor/tools/sbin/so-wait-cloud-init
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Script for waiting for cloud-init to complete on a Security Onion VM.
|
||||||
|
Monitors VM state to ensure proper cloud-init initialization and shutdown.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
so-wait-cloud-init -n <domain_name>
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
-n, --name Domain name of the VM to monitor
|
||||||
|
|
||||||
|
**Exit Codes:**
|
||||||
|
- 0: Success (cloud-init completed and VM shutdown)
|
||||||
|
- 1: General error
|
||||||
|
- 2: VM never started
|
||||||
|
- 3: VM stopped too quickly
|
||||||
|
- 4: VM failed to shutdown
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
This script monitors a VM's state to ensure proper cloud-init initialization and completion:
|
||||||
|
1. Waits for VM to start running
|
||||||
|
2. Verifies VM remains running (not an immediate crash)
|
||||||
|
3. Waits for VM to shutdown (indicating cloud-init completion)
|
||||||
|
4. Verifies VM remains shutdown
|
||||||
|
|
||||||
|
The script is typically used in the libvirt.images state after creating a new VM
|
||||||
|
to ensure cloud-init completes its initialization before proceeding with further
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
**Logging:**
|
||||||
|
- Logs are written to /opt/so/log/hypervisor/so-wait-cloud-init.log
|
||||||
|
- Both file and console logging are enabled
|
||||||
|
- Log entries include:
|
||||||
|
- Timestamps
|
||||||
|
- State changes
|
||||||
|
- Error conditions
|
||||||
|
- Verification steps
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from so_logging_utils import setup_logging
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logger = setup_logging(
|
||||||
|
logger_name='so-wait-cloud-init',
|
||||||
|
log_file_path='/opt/so/log/hypervisor/so-wait-cloud-init.log',
|
||||||
|
log_level=logging.INFO,
|
||||||
|
format_str='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_vm_running(domain_name):
|
||||||
|
"""
|
||||||
|
Check if VM is in running state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if VM is running, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['virsh', 'list', '--state-running', '--name'],
|
||||||
|
capture_output=True, text=True, check=True)
|
||||||
|
return domain_name in result.stdout.splitlines()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed to check VM state: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_for_vm_start(domain_name, timeout=300):
|
||||||
|
"""
|
||||||
|
Wait for VM to start running.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to monitor
|
||||||
|
timeout (int): Maximum time to wait in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if VM started, False if timeout occurred
|
||||||
|
"""
|
||||||
|
logger.info(f"Waiting for VM {domain_name} to start...")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
if check_vm_running(domain_name):
|
||||||
|
logger.info("VM is running")
|
||||||
|
return True
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
logger.error(f"Timeout waiting for VM {domain_name} to start")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def verify_vm_running(domain_name):
|
||||||
|
"""
|
||||||
|
Verify VM remains running after initial start.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to verify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if VM is still running after verification period
|
||||||
|
"""
|
||||||
|
logger.info("Verifying VM remains running...")
|
||||||
|
time.sleep(5) # Wait to ensure VM is stable
|
||||||
|
|
||||||
|
if not check_vm_running(domain_name):
|
||||||
|
logger.error("VM stopped too quickly after starting")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("VM verified running")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def wait_for_vm_shutdown(domain_name, timeout=600):
|
||||||
|
"""
|
||||||
|
Wait for VM to shutdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to monitor
|
||||||
|
timeout (int): Maximum time to wait in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if VM shutdown, False if timeout occurred
|
||||||
|
"""
|
||||||
|
logger.info("Waiting for cloud-init to complete and VM to shutdown...")
|
||||||
|
start_time = time.time()
|
||||||
|
check_count = 0
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
if not check_vm_running(domain_name):
|
||||||
|
logger.info("VM has shutdown")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Log status every minute (after 12 checks at 5 second intervals)
|
||||||
|
check_count += 1
|
||||||
|
if check_count % 12 == 0:
|
||||||
|
elapsed = int(time.time() - start_time)
|
||||||
|
logger.info(f"Still waiting for cloud-init... ({elapsed} seconds elapsed)")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
logger.error(f"Timeout waiting for VM {domain_name} to shutdown")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def verify_vm_shutdown(domain_name):
|
||||||
|
"""
|
||||||
|
Verify VM remains shutdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name (str): Name of the domain to verify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if VM remains shutdown after verification period
|
||||||
|
"""
|
||||||
|
logger.info("Verifying VM remains shutdown...")
|
||||||
|
time.sleep(5) # Wait to ensure VM state is stable
|
||||||
|
|
||||||
|
if check_vm_running(domain_name):
|
||||||
|
logger.error("VM is still running after shutdown check")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("VM verified shutdown")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Wait for cloud-init to complete on a Security Onion VM"
|
||||||
|
)
|
||||||
|
parser.add_argument("-n", "--name", required=True,
|
||||||
|
help="Domain name of the VM to monitor")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Wait for VM to start
|
||||||
|
if not wait_for_vm_start(args.name):
|
||||||
|
sys.exit(2) # VM never started
|
||||||
|
|
||||||
|
# Verify VM remains running
|
||||||
|
if not verify_vm_running(args.name):
|
||||||
|
sys.exit(3) # VM stopped too quickly
|
||||||
|
|
||||||
|
# Wait for VM to shutdown
|
||||||
|
if not wait_for_vm_shutdown(args.name):
|
||||||
|
sys.exit(4) # VM failed to shutdown
|
||||||
|
|
||||||
|
# Verify VM remains shutdown
|
||||||
|
if not verify_vm_shutdown(args.name):
|
||||||
|
sys.exit(4) # VM failed to stay shutdown
|
||||||
|
|
||||||
|
logger.info("Cloud-init completed successfully")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -117,7 +117,7 @@ undefine_vm_sool9:
|
|||||||
- onlyif:
|
- onlyif:
|
||||||
- virsh dominfo sool9
|
- virsh dominfo sool9
|
||||||
|
|
||||||
# Create and start the VM using virt-install
|
# Create and start the VM, letting cloud-init run
|
||||||
create_vm_sool9:
|
create_vm_sool9:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: |
|
- name: |
|
||||||
@@ -138,6 +138,21 @@ create_vm_sool9:
|
|||||||
- file: manage_userdata_sool9
|
- file: manage_userdata_sool9
|
||||||
- file: manage_cidata_sool9
|
- file: manage_cidata_sool9
|
||||||
|
|
||||||
|
# Wait for cloud-init to complete and VM to shutdown
|
||||||
|
wait_for_cloud_init_sool9:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-wait-cloud-init -n sool9
|
||||||
|
- require:
|
||||||
|
- cmd: create_vm_sool9
|
||||||
|
- timeout: 600
|
||||||
|
|
||||||
|
# Configure network predictability after cloud-init
|
||||||
|
configure_network_predictable_sool9:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-qcow2-network-predictable -n sool9
|
||||||
|
- require:
|
||||||
|
- cmd: wait_for_cloud_init_sool9
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{sls}}_no_license_detected:
|
{{sls}}_no_license_detected:
|
||||||
test.fail_without_changes:
|
test.fail_without_changes:
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def run_qcow2_modify_network_config(profile, mode, ip=None, gateway=None, dns=No
|
|||||||
hv_name = profile.split('-')[1]
|
hv_name = profile.split('-')[1]
|
||||||
target = hv_name + "_*"
|
target = hv_name + "_*"
|
||||||
image = '/nsm/libvirt/images/sool9/sool9.qcow2'
|
image = '/nsm/libvirt/images/sool9/sool9.qcow2'
|
||||||
interface = 'eth0'
|
interface = 'enp1s0'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = local.cmd(target, 'qcow2.modify_network_config', [
|
r = local.cmd(target, 'qcow2.modify_network_config', [
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ sool9-{{host}}:
|
|||||||
inline_script:
|
inline_script:
|
||||||
- |
|
- |
|
||||||
sudo salt-call state.apply salt.mine_functions \
|
sudo salt-call state.apply salt.mine_functions \
|
||||||
pillar='{"host": {"mainint": "eth0"}}'
|
pillar='{"host": {"mainint": "enp1s0"}}'
|
||||||
- 'sudo salt-call mine.update'
|
- 'sudo salt-call mine.update'
|
||||||
- 'sudo salt-call state.apply setup.virt'
|
- 'sudo salt-call state.apply setup.virt'
|
||||||
# grains to add to the minion
|
# grains to add to the minion
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
MAINIP:
|
MAINIP:
|
||||||
MNIC: eth0
|
MNIC: enp1s0
|
||||||
NODE_DESCRIPTION: 'vm'
|
NODE_DESCRIPTION: 'vm'
|
||||||
ES_HEAP_SIZE:
|
ES_HEAP_SIZE:
|
||||||
PATCHSCHEDULENAME:
|
PATCHSCHEDULENAME:
|
||||||
|
|||||||
Reference in New Issue
Block a user