Files
securityonion/salt/manager/tools/sbin/so-salt-cloud

159 lines
5.8 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.
import argparse
import subprocess
import re
import sys
import threading
import salt.client
local = salt.client.LocalClient()
import 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}")
# This function requires the cloud profile to be in the form: basedomain_hypervisorhostname. The profile name will be used to target the hypervisor.
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/coreol9/coreol9.qcow2.MODIFIED'
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.")
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
run_qcow2_modify_network_config(args.profile, mode, args.ip4, args.gw4, args.dns4, args.search4)
call_salt_cloud(args.profile, args.vm_name)
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()