mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 09:12:45 +01:00
309 lines
12 KiB
Python
309 lines
12 KiB
Python
#!/opt/saltstack/salt/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 to assist with salt-cloud VM provisioning. This is intended to work with a libvirt salt-cloud provider.
|
|
|
|
**Usage:**
|
|
python so-salt-cloud -p <profile> <vm_name> (--dhcp4 | --static4 --ip4 <ip_address> --gw4 <gateway>)
|
|
[-c <cpu_count>] [-m <memory_amount>] [-P <pci_id>] [-P <pci_id> ...] [--dns4 <dns_servers>] [--search4 <search_domain>]
|
|
|
|
**Options:**
|
|
-p, --profile The cloud profile to build the VM from.
|
|
<vm_name> The name of the VM.
|
|
--dhcp4 Configure interface for DHCP (IPv4).
|
|
--static4 Configure interface for static IPv4 settings.
|
|
--ip4 IPv4 address (e.g., 192.168.1.10/24). Required for static IPv4 configuration.
|
|
--gw4 IPv4 gateway (e.g., 192.168.1.1). Required for static IPv4 configuration.
|
|
--dns4 Comma-separated list of IPv4 DNS servers (e.g., 8.8.8.8,8.8.4.4).
|
|
--search4 DNS search domain for IPv4.
|
|
-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.
|
|
|
|
**Examples:**
|
|
|
|
1. **Static IP Configuration with Multiple PCI Devices:**
|
|
|
|
```bash
|
|
python so-salt-cloud -p core-hype1 vm1_sensor --static4 --ip4 192.168.1.10/24 --gw4 192.168.1.1 \
|
|
--dns4 192.168.1.1,192.168.1.2 --search4 example.local -c 4 -m 8192 -P 0000:00:1f.2 -P 0000:00:1f.3
|
|
```
|
|
|
|
This command provisions a VM named `vm1_sensor` using the `core-hype1` profile with the following settings:
|
|
|
|
- Static IPv4 configuration:
|
|
- IP Address: `192.168.1.10/24`
|
|
- Gateway: `192.168.1.1`
|
|
- DNS Servers: `192.168.1.1`, `192.168.1.2`
|
|
- DNS Search Domain: `example.local`
|
|
- Hardware Configuration:
|
|
- CPUs: `4`
|
|
- Memory: `8192` MiB
|
|
- PCI Device Passthrough: `0000:00:1f.2`, `0000:00:1f.3`
|
|
|
|
2. **DHCP Configuration with Default Hardware Settings:**
|
|
|
|
```bash
|
|
python so-salt-cloud -p core-hype1 vm2_master --dhcp4
|
|
```
|
|
|
|
This command provisions a VM named `vm2_master` using the `core-hype1` profile with DHCP for network configuration and default hardware settings.
|
|
|
|
3. **Static IP Configuration without Hardware Specifications:**
|
|
|
|
```bash
|
|
python so-salt-cloud -p core-hype1 vm3_search --static4 --ip4 192.168.1.20/24 --gw4 192.168.1.1
|
|
```
|
|
|
|
This command provisions a VM named `vm3_search` with a static IP configuration and default hardware settings.
|
|
|
|
4. **DHCP Configuration with Custom Hardware Specifications and Multiple PCI Devices:**
|
|
|
|
```bash
|
|
python so-salt-cloud -p core-hype1 vm4_node --dhcp4 -c 8 -m 16384 -P 0000:00:1f.4 -P 0000:00:1f.5
|
|
```
|
|
|
|
This command provisions a VM named `vm4_node` using DHCP for network configuration and custom hardware settings:
|
|
|
|
- CPUs: `8`
|
|
- Memory: `16384` MiB
|
|
- PCI Device Passthrough: `0000:00:1f.4`, `0000:00:1f.5`
|
|
|
|
**Notes:**
|
|
|
|
- When using `--static4`, both `--ip4` and `--gw4` options are required.
|
|
- The script assumes the cloud profile name follows the format `basedomain-hypervisorname`.
|
|
- Hardware parameters (`-c`, `-m`, `-P`) are optional. If not provided, default values from the profile will be used.
|
|
- The `-P` or `--pci` option can be specified multiple times to pass through multiple PCI devices to the VM.
|
|
- The `vm_name` should include the role of the VM after an underscore (e.g., `hostname_role`), as the script uses this to determine the VM's role for firewall configuration.
|
|
|
|
**Description:**
|
|
|
|
The `so-salt-cloud` script automates the provisioning of virtual machines using SaltStack's `salt-cloud` utility. It performs the following steps:
|
|
|
|
1. **Network Configuration:**
|
|
|
|
- Modifies the network settings of the base QCOW2 image before provisioning.
|
|
- Supports both DHCP and static IPv4 configurations.
|
|
- Uses the `qcow2.modify_network_config` module via SaltStack to apply these settings on the target hypervisor.
|
|
|
|
2. **VM Provisioning:**
|
|
|
|
- Calls `salt-cloud` to provision the VM using the specified profile and VM name.
|
|
- The VM is provisioned but not started immediately to allow for hardware configuration.
|
|
|
|
3. **Hardware Configuration:**
|
|
|
|
- Modifies the hardware settings of the newly defined VM.
|
|
- Supports specifying multiple PCI devices for passthrough.
|
|
- Uses the `qcow2.modify_hardware_config` module via SaltStack to adjust CPU count, memory allocation, and PCI device passthrough.
|
|
- Starts the VM after hardware modifications.
|
|
|
|
4. **Firewall Configuration:**
|
|
|
|
- Monitors the output of `salt-cloud` to extract the VM's IP address.
|
|
- Calls the `so-firewall-minion` script to apply firewall rules based on the VM's role.
|
|
|
|
**Exit Codes:**
|
|
|
|
- `0`: Success
|
|
- Non-zero: An error occurred during execution.
|
|
|
|
**Logging:**
|
|
|
|
- Logs are written to `/opt/so/log/salt/so-salt-cloud.log`.
|
|
- Both file and console logging are enabled for real-time monitoring.
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import subprocess
|
|
import re
|
|
import sys
|
|
import threading
|
|
import salt.client
|
|
import logging
|
|
|
|
# Initialize Salt local client
|
|
local = salt.client.LocalClient()
|
|
|
|
# Set up logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
file_handler = logging.FileHandler('/opt/so/log/salt/so-salt-cloud.log')
|
|
console_handler = logging.StreamHandler()
|
|
|
|
formatter = logging.Formatter('%(asctime)s %(message)s')
|
|
file_handler.setFormatter(formatter)
|
|
console_handler.setFormatter(formatter)
|
|
|
|
logger.addHandler(file_handler)
|
|
logger.addHandler(console_handler)
|
|
|
|
def call_so_firewall_minion(ip, role):
|
|
try:
|
|
# Start so-firewall-minion as a subprocess
|
|
process = subprocess.Popen(
|
|
['/usr/sbin/so-firewall-minion', f'--ip={ip}', f'--role={role}'],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True
|
|
)
|
|
|
|
# Read and log the output
|
|
for line in iter(process.stdout.readline, ''):
|
|
if line:
|
|
logger.info(line.rstrip('\n'))
|
|
|
|
process.stdout.close()
|
|
process.wait()
|
|
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while calling so-firewall-minion: {e}")
|
|
|
|
def call_salt_cloud(profile, vm_name):
|
|
try:
|
|
# Start the salt-cloud command as a subprocess
|
|
process = subprocess.Popen(
|
|
['salt-cloud', '-p', profile, vm_name, '-l', 'info'],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True
|
|
)
|
|
|
|
role = vm_name.split("_")[1]
|
|
|
|
ip_search_string = '[INFO ] Address ='
|
|
ip_search_pattern = re.compile(re.escape(ip_search_string))
|
|
|
|
# Continuously read the output from salt-cloud
|
|
while True:
|
|
# Read stdout line by line
|
|
line = process.stdout.readline()
|
|
if line:
|
|
logger.info(line.rstrip('\n'))
|
|
|
|
if ip_search_pattern.search(line):
|
|
parts = line.split("Address =")
|
|
if len(parts) > 1:
|
|
ip_address = parts[1].strip()
|
|
logger.info(f"Extracted IP address: {ip_address}")
|
|
# Create and start a thread to run so-firewall-minion
|
|
thread = threading.Thread(target=call_so_firewall_minion, args=(ip_address, role.upper()))
|
|
thread.start()
|
|
else:
|
|
logger.error("No IP address found.")
|
|
else:
|
|
# Check if salt-cloud has terminated
|
|
if process.poll() is not None:
|
|
break
|
|
|
|
process.stdout.close()
|
|
process.wait()
|
|
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while calling salt-cloud: {e}")
|
|
|
|
def run_qcow2_modify_hardware_config(profile, vm_name, cpu=None, memory=None, pci_list=None, start=False):
|
|
hv_name = profile.split('-')[1]
|
|
target = hv_name + "_*"
|
|
|
|
try:
|
|
args_list = [
|
|
'vm_name=' + vm_name,
|
|
'cpu=' + str(cpu) if cpu else '',
|
|
'memory=' + str(memory) if memory else '',
|
|
'start=' + str(start)
|
|
]
|
|
|
|
# Add PCI devices if provided
|
|
if pci_list:
|
|
# Join the list of PCI IDs into a comma-separated string
|
|
pci_devices = ','.join(pci_list)
|
|
args_list.append('pci=' + pci_devices)
|
|
|
|
r = local.cmd(target, 'qcow2.modify_hardware_config', args_list)
|
|
logger.info(f'qcow2.modify_hardware_config: {r}')
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while running qcow2.modify_hardware_config: {e}")
|
|
|
|
def run_qcow2_modify_network_config(profile, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
|
hv_name = profile.split('-')[1]
|
|
target = hv_name + "_*"
|
|
image = '/var/lib/libvirt/images/sool9/sool9.qcow2'
|
|
interface = 'eth0'
|
|
|
|
try:
|
|
r = local.cmd(target, 'qcow2.modify_network_config', [
|
|
'image=' + image,
|
|
'interface=' + interface,
|
|
'mode=' + mode,
|
|
'ip4=' + ip if ip else '',
|
|
'gw4=' + gateway if gateway else '',
|
|
'dns4=' + dns if dns else '',
|
|
'search4=' + search_domain if search_domain else ''
|
|
])
|
|
logger.info(f'qcow2.modify_network_config: {r}')
|
|
except Exception as e:
|
|
logger.error(f"An error occurred while running qcow2.modify_network_config: {e}")
|
|
|
|
def parse_arguments():
|
|
parser = argparse.ArgumentParser(description="Call salt-cloud and pass the profile and VM name to it.")
|
|
parser.add_argument('-p', '--profile', type=str, required=True, help="The cloud profile to build the VM from.")
|
|
parser.add_argument('vm_name', type=str, help="The name of the VM.")
|
|
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
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.")
|
|
|
|
parser.add_argument("--ip4", help="IPv4 address (e.g., 192.168.1.10/24). Required for static IPv4 configuration.")
|
|
parser.add_argument("--gw4", help="IPv4 gateway (e.g., 192.168.1.1). Required for static IPv4 configuration.")
|
|
parser.add_argument("--dns4", help="Comma-separated list of IPv4 DNS servers (e.g., 8.8.8.8,8.8.4.4).")
|
|
parser.add_argument("--search4", help="DNS search domain for IPv4.")
|
|
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.')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.static4:
|
|
if not args.ip4 or not args.gw4:
|
|
parser.error("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
|
return args
|
|
|
|
def main():
|
|
try:
|
|
args = parse_arguments()
|
|
|
|
if args.dhcp4:
|
|
mode = "dhcp4"
|
|
elif args.static4:
|
|
mode = "static4"
|
|
else:
|
|
mode = "dhcp4" # Default to DHCP if not specified
|
|
|
|
# Step 1: Modify network configuration
|
|
run_qcow2_modify_network_config(args.profile, mode, args.ip4, args.gw4, args.dns4, args.search4)
|
|
|
|
# Step 2: Provision the VM (without starting it)
|
|
call_salt_cloud(args.profile, args.vm_name)
|
|
|
|
# Step 3: Modify hardware configuration
|
|
run_qcow2_modify_hardware_config(args.profile, args.vm_name, cpu=args.cpu, memory=args.memory, pci_list=args.pci, start=True)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.error("so-salt-cloud: Operation cancelled by user.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
logger.error(f"so-salt-cloud: An error occurred: {e}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|