mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 17:22:49 +01:00
add tools to set dhcp/static ip inside the qcow2 image
This commit is contained in:
73
salt/_modules/qcow2.py
Normal file
73
salt/_modules/qcow2.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!py
|
||||||
|
|
||||||
|
# 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 logging
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__virtualname__ = 'qcow2'
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return __virtualname__
|
||||||
|
|
||||||
|
def modify_network_config(image, interface, mode, ip4=None, gw4=None, dns4=None, search4=None):
|
||||||
|
'''
|
||||||
|
Wrapper function to call so-qcow2-modify-network
|
||||||
|
|
||||||
|
:param image: Path to the QCOW2 image.
|
||||||
|
:param interface: Network interface to modify (e.g., 'eth0').
|
||||||
|
:param mode: 'dhcp4' or 'static4'.
|
||||||
|
:param ip4: IPv4 address with CIDR notation (e.g., '192.168.1.100/24'). Required for static configuration.
|
||||||
|
:param gw4: IPv4 gateway (e.g., '192.168.1.1'). Required for static configuration.
|
||||||
|
:param dns4: Comma-separated list of IPv4 DNS servers (e.g., '8.8.8.8,8.8.4.4').
|
||||||
|
:param search4: DNS search domain for IPv4.
|
||||||
|
|
||||||
|
:return: A dictionary with the result of the script execution.
|
||||||
|
|
||||||
|
CLI Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
salt '*' qcow2.modify_network_config image='/path/to/image.qcow2' interface='eth0' mode='static4' ip4='192.168.1.100/24' gw4='192.168.1.1' dns4='8.8.8.8,8.8.4.4' search4='example.com'
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
cmd = ['/usr/sbin/so-qcow2-modify-network.py', '-I', image, '-i', interface]
|
||||||
|
|
||||||
|
if mode.lower() == 'dhcp4':
|
||||||
|
cmd.append('--dhcp4')
|
||||||
|
elif mode.lower() == 'static4':
|
||||||
|
cmd.append('--static4')
|
||||||
|
if not ip4 or not gw4:
|
||||||
|
raise ValueError('Both ip4 and gw4 are required for static configuration.')
|
||||||
|
cmd.extend(['--ip4', ip4, '--gw4', gw4])
|
||||||
|
if dns4:
|
||||||
|
cmd.extend(['--dns4', dns4])
|
||||||
|
if search4:
|
||||||
|
cmd.extend(['--search4', search4])
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid mode '{}'. Expected 'dhcp4' or 'static4'.".format(mode))
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
hypervisor_sbin:
|
||||||
|
file.recurse:
|
||||||
|
- name: /usr/sbin
|
||||||
|
- source: salt://hypervisor/tools/sbin
|
||||||
|
- file_mode: 744
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#!/usr/bin/python3
|
#!/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.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import guestfs
|
import guestfs
|
||||||
import re
|
import re
|
||||||
@@ -16,55 +21,39 @@ logger = logging.getLogger(__name__)
|
|||||||
NETWORK_CONFIG_DIR = "/etc/NetworkManager/system-connections"
|
NETWORK_CONFIG_DIR = "/etc/NetworkManager/system-connections"
|
||||||
|
|
||||||
def validate_ip_address(ip_str, description="IP address"):
|
def validate_ip_address(ip_str, description="IP address"):
|
||||||
"""
|
|
||||||
Validates that the given string is a properly formatted IPv4 address or IPv4 interface.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Try to parse as IPv4 interface (e.g., "192.168.1.10/24")
|
|
||||||
ipaddress.IPv4Interface(ip_str)
|
ipaddress.IPv4Interface(ip_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
# Try to parse as IPv4 address (e.g., "192.168.1.10")
|
|
||||||
ipaddress.IPv4Address(ip_str)
|
ipaddress.IPv4Address(ip_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f"Invalid {description}: {ip_str}")
|
raise ValueError(f"Invalid {description}: {ip_str}")
|
||||||
|
|
||||||
def validate_dns_addresses(dns_str):
|
def validate_dns_addresses(dns_str):
|
||||||
"""
|
|
||||||
Validates a comma-separated list of DNS server IP addresses.
|
|
||||||
"""
|
|
||||||
dns_list = dns_str.split(',')
|
dns_list = dns_str.split(',')
|
||||||
for dns in dns_list:
|
for dns in dns_list:
|
||||||
dns = dns.strip()
|
dns = dns.strip()
|
||||||
validate_ip_address(dns, description="DNS server address")
|
validate_ip_address(dns, description="DNS server address")
|
||||||
|
|
||||||
def validate_interface_name(interface_name):
|
def validate_interface_name(interface_name):
|
||||||
"""
|
|
||||||
Validates that the network interface name contains only valid characters.
|
|
||||||
"""
|
|
||||||
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 update_ipv4_section(content, mode, ip=None, gateway=None, dns=None, search_domain=None):
|
||||||
"""
|
|
||||||
Updates the IPv4 section of the network configuration file with either DHCP or static settings.
|
|
||||||
"""
|
|
||||||
config = configparser.ConfigParser(strict=False)
|
config = configparser.ConfigParser(strict=False)
|
||||||
config.optionxform = str # Preserve case sensitivity
|
config.optionxform = str
|
||||||
config.read_string(content)
|
config.read_string(content)
|
||||||
|
|
||||||
if 'ipv4' not in config.sections():
|
if 'ipv4' not in config.sections():
|
||||||
# Handle missing [ipv4] section gracefully
|
|
||||||
config.add_section('ipv4')
|
config.add_section('ipv4')
|
||||||
|
|
||||||
if mode == "dhcp":
|
if mode == "dhcp4":
|
||||||
config.set('ipv4', 'method', 'auto')
|
config.set('ipv4', 'method', 'auto')
|
||||||
# Remove static addresses, DNS settings, and search domains
|
|
||||||
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 == "static":
|
elif mode == "static4":
|
||||||
config.set('ipv4', 'method', 'manual')
|
config.set('ipv4', 'method', 'manual')
|
||||||
if ip and gateway:
|
if ip and gateway:
|
||||||
config.set('ipv4', 'address1', f"{ip},{gateway}")
|
config.set('ipv4', 'address1', f"{ip},{gateway}")
|
||||||
@@ -79,9 +68,8 @@ def update_ipv4_section(content, mode, ip=None, gateway=None, dns=None, search_d
|
|||||||
else:
|
else:
|
||||||
config.remove_option('ipv4', 'dns-search')
|
config.remove_option('ipv4', 'dns-search')
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid mode '{mode}'. Expected 'dhcp' or 'static'.")
|
raise ValueError(f"Invalid mode '{mode}'. Expected 'dhcp4' or 'static4'.")
|
||||||
|
|
||||||
# Write the updated content back to a string
|
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
config.write(output, space_around_delimiters=False)
|
config.write(output, space_around_delimiters=False)
|
||||||
updated_content = output.getvalue()
|
updated_content = output.getvalue()
|
||||||
@@ -89,29 +77,21 @@ def update_ipv4_section(content, mode, ip=None, gateway=None, dns=None, search_d
|
|||||||
|
|
||||||
return updated_content
|
return updated_content
|
||||||
|
|
||||||
|
# modify the network config file for the interface inside the qcow2 image
|
||||||
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 the network configuration file for the given interface inside the QCOW2 image.
|
|
||||||
"""
|
|
||||||
# Check for write permissions to the image file
|
|
||||||
if not os.access(image_path, os.W_OK):
|
if not os.access(image_path, os.W_OK):
|
||||||
raise PermissionError(f"Write permission denied for image file: {image_path}")
|
raise PermissionError(f"Write permission denied for image file: {image_path}")
|
||||||
|
|
||||||
# Initialize the guestfs instance and add the image
|
|
||||||
g = guestfs.GuestFS(python_return_dict=True)
|
g = guestfs.GuestFS(python_return_dict=True)
|
||||||
try:
|
try:
|
||||||
g.set_network(False) # Disable network access if not needed
|
g.set_network(False)
|
||||||
|
g.selinux = False
|
||||||
# Disable SELinux relabeling
|
|
||||||
g.selinux = False # Correct way to disable SELinux relabeling
|
|
||||||
|
|
||||||
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:
|
||||||
raise RuntimeError(f"Failed to initialize GuestFS or launch appliance: {e}")
|
raise RuntimeError(f"Failed to initialize GuestFS or launch appliance: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Detect and mount the operating system
|
|
||||||
os_list = g.inspect_os()
|
os_list = g.inspect_os()
|
||||||
if not os_list:
|
if not os_list:
|
||||||
raise RuntimeError(f"Unable to find any OS in {image_path}.")
|
raise RuntimeError(f"Unable to find any OS in {image_path}.")
|
||||||
@@ -122,14 +102,11 @@ def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dn
|
|||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise RuntimeError(f"Failed to mount the filesystem: {e}")
|
raise RuntimeError(f"Failed to mount the filesystem: {e}")
|
||||||
|
|
||||||
# Check if NetworkManager configuration directory exists
|
|
||||||
if not g.is_dir(NETWORK_CONFIG_DIR):
|
if not g.is_dir(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}.")
|
||||||
|
|
||||||
# Path to the network configuration file for the given interface
|
|
||||||
config_file_path = f"{NETWORK_CONFIG_DIR}/{interface}.nmconnection"
|
config_file_path = f"{NETWORK_CONFIG_DIR}/{interface}.nmconnection"
|
||||||
|
|
||||||
# Read the current configuration file
|
|
||||||
try:
|
try:
|
||||||
file_content = g.read_file(config_file_path)
|
file_content = g.read_file(config_file_path)
|
||||||
current_content = file_content.decode('utf-8')
|
current_content = file_content.decode('utf-8')
|
||||||
@@ -138,16 +115,14 @@ def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dn
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise ValueError(f"Failed to decode the configuration file for {interface}.")
|
raise ValueError(f"Failed to decode the configuration file for {interface}.")
|
||||||
|
|
||||||
# Update the content based on the provided arguments
|
|
||||||
updated_content = update_ipv4_section(current_content, mode, ip, gateway, dns, search_domain)
|
updated_content = update_ipv4_section(current_content, mode, ip, gateway, dns, search_domain)
|
||||||
|
|
||||||
# Write the updated content back to the configuration file
|
|
||||||
try:
|
try:
|
||||||
g.write(config_file_path, updated_content.encode('utf-8'))
|
g.write(config_file_path, updated_content.encode('utf-8'))
|
||||||
except RuntimeError as e:
|
except RuntimeError as 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"so-qcow2-modify-network: Updated {interface} network configuration in {image_path} using {mode.upper()} mode.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
@@ -156,14 +131,11 @@ def modify_network_config(image_path, interface, mode, ip=None, gateway=None, dn
|
|||||||
g.close()
|
g.close()
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""
|
|
||||||
Parses command-line arguments for the script.
|
|
||||||
"""
|
|
||||||
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., eth0).")
|
||||||
group = parser.add_mutually_exclusive_group(required=True)
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
group.add_argument("--dhcp", 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.")
|
||||||
parser.add_argument("--ip4", help="IPv4 address (e.g., 192.168.1.10/24). Required for static IPv4 configuration.")
|
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("--gw4", help="IPv4 gateway (e.g., 192.168.1.1). Required for static IPv4 configuration.")
|
||||||
@@ -172,44 +144,37 @@ def parse_arguments():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Validate arguments
|
|
||||||
if args.static4:
|
if args.static4:
|
||||||
if not args.ip4 or not args.gw4:
|
if not args.ip4 or not args.gw4:
|
||||||
parser.error("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
parser.error("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
|
||||||
Main entry point for the script.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
validate_interface_name(args.interface)
|
validate_interface_name(args.interface)
|
||||||
|
|
||||||
# Validate mode
|
if args.dhcp4:
|
||||||
if args.dhcp:
|
mode = "dhcp4"
|
||||||
mode = "dhcp"
|
|
||||||
elif args.static4:
|
elif args.static4:
|
||||||
mode = "static"
|
mode = "static4"
|
||||||
if not args.ip4 or not args.gw4:
|
if not args.ip4 or not args.gw4:
|
||||||
raise ValueError("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
raise ValueError("Both --ip4 and --gw4 are required for static IPv4 configuration.")
|
||||||
# Validate 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:
|
||||||
raise ValueError("Either --dhcp or --static4 must be specified.")
|
raise ValueError("Either --dhcp4 or --static4 must be specified.")
|
||||||
|
|
||||||
# Modify the network configuration inside the image
|
|
||||||
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)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.error("Operation cancelled by user.")
|
logger.error("so-qcow2-modify-network: Operation cancelled by user.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An error occurred: {e}")
|
logger.error(f"so-qcow2-modify-network: An error occurred: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -39,6 +39,6 @@ libvirt_python_wheel:
|
|||||||
|
|
||||||
libvirt_python_module:
|
libvirt_python_module:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: /opt/saltstack/salt/bin/python3.10 -m pip install --no-index --find-links=/opt/so/conf/libvirt/source-packages/libvirt-python libvirt-python
|
- name: /opt/saltstack/salt/bin/python3 -m pip install --no-index --find-links=/opt/so/conf/libvirt/source-packages/libvirt-python libvirt-python
|
||||||
- onchanges:
|
- onchanges:
|
||||||
- file: libvirt_python_wheel
|
- file: libvirt_python_wheel
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python3
|
#!/opt/saltstack/salt/bin/python3
|
||||||
|
|
||||||
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
# 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
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
@@ -8,7 +8,26 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import threading
|
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):
|
def call_so_firewall_minion(ip, role):
|
||||||
try:
|
try:
|
||||||
@@ -20,8 +39,16 @@ def call_so_firewall_minion(ip, role):
|
|||||||
text=True
|
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:
|
except Exception as e:
|
||||||
print(f"An error occurred while calling the command: {e}")
|
logger.error(f"An error occurred while calling so-firewall-minion: {e}")
|
||||||
|
|
||||||
def call_salt_cloud(profile, vm_name):
|
def call_salt_cloud(profile, vm_name):
|
||||||
try:
|
try:
|
||||||
@@ -38,38 +65,94 @@ def call_salt_cloud(profile, vm_name):
|
|||||||
ip_search_string = '[INFO ] Address ='
|
ip_search_string = '[INFO ] Address ='
|
||||||
ip_search_pattern = re.compile(re.escape(ip_search_string))
|
ip_search_pattern = re.compile(re.escape(ip_search_string))
|
||||||
|
|
||||||
# Continuously read the output
|
# continuously read the output from salt-cloud
|
||||||
while True:
|
while True:
|
||||||
# Read stdout line by line
|
# Read stdout line by line
|
||||||
line = process.stdout.readline()
|
line = process.stdout.readline()
|
||||||
if line:
|
if line:
|
||||||
print(line.rstrip('\n'))
|
logger.info(line.rstrip('\n'))
|
||||||
|
|
||||||
if ip_search_pattern.search(line):
|
if ip_search_pattern.search(line):
|
||||||
parts = line.split("Address =")
|
parts = line.split("Address =")
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
ip_address = parts[1].strip()
|
ip_address = parts[1].strip()
|
||||||
print("Extracted IP address:", ip_address)
|
logger.info(f"Extracted IP address: {ip_address}")
|
||||||
# Create and start a thread to run so-firewall-minion
|
# Create and start a thread to run so-firewall-minion
|
||||||
thread = threading.Thread(target=call_so_firewall_minion, args=(ip_address, role.upper()))
|
thread = threading.Thread(target=call_so_firewall_minion, args=(ip_address, role.upper()))
|
||||||
thread.start()
|
thread.start()
|
||||||
else:
|
else:
|
||||||
print("No IP address found.")
|
logger.error("No IP address found.")
|
||||||
|
else:
|
||||||
# Check if the process has terminated
|
# check if salt-cloud has terminated
|
||||||
elif process.poll() is not None:
|
if process.poll() is not None:
|
||||||
# process finished
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
process.stdout.close()
|
||||||
|
process.wait()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred while calling the command: {e}")
|
logger.error(f"An error occurred while calling salt-cloud: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# 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 = 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('-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.")
|
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()
|
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)
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user