mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2025-12-06 17:22:49 +01:00
vm power operations
This commit is contained in:
330
salt/salt/engines/master/virtual_power_manager.py
Normal file
330
salt/salt/engines/master/virtual_power_manager.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# Note: Per the Elastic License 2.0, the second limitation states:
|
||||||
|
#
|
||||||
|
# "You may not move, change, disable, or circumvent the license key functionality
|
||||||
|
# in the software, and you may not remove or obscure any functionality in the
|
||||||
|
# software that is protected by the license key."
|
||||||
|
|
||||||
|
"""
|
||||||
|
Salt Engine for Virtual Machine Power Management
|
||||||
|
|
||||||
|
This engine manages power control actions for virtual machines in Security Onion's
|
||||||
|
virtualization infrastructure. It monitors VM configurations for power control requests
|
||||||
|
and executes the appropriate virt module actions.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
engines:
|
||||||
|
- virtual_power_manager:
|
||||||
|
interval: 60
|
||||||
|
base_path: /opt/so/saltstack/local/salt/hypervisor/hosts
|
||||||
|
|
||||||
|
Options:
|
||||||
|
interval: Time in seconds between engine runs (managed by salt-master, default: 60)
|
||||||
|
base_path: Base directory containing hypervisor configurations (default: /opt/so/saltstack/local/salt/hypervisor/hosts)
|
||||||
|
|
||||||
|
Configuration Files:
|
||||||
|
<hypervisorHostname>VMs: JSON file containing VM configurations
|
||||||
|
- Located at <base_path>/<hypervisorHostname>VMs
|
||||||
|
- Contains array of VM configurations
|
||||||
|
- Power control requests are specified with the "powercontrol" key
|
||||||
|
- Valid values for "powercontrol": "Reboot", "Reset", "Shutdown", "Start", "Stop"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
1. Basic Configuration:
|
||||||
|
engines:
|
||||||
|
- virtual_power_manager: {}
|
||||||
|
|
||||||
|
Uses default settings to process power control requests every 60 seconds.
|
||||||
|
|
||||||
|
2. Custom Interval:
|
||||||
|
engines:
|
||||||
|
- virtual_power_manager:
|
||||||
|
interval: 120
|
||||||
|
|
||||||
|
Processes power control requests every 120 seconds.
|
||||||
|
|
||||||
|
Power Control Actions:
|
||||||
|
- Reboot: Gracefully reboot the VM (virt.reboot)
|
||||||
|
- Reset: Force reset the VM (virt.reset)
|
||||||
|
- Shutdown: Gracefully shut down the VM (virt.shutdown)
|
||||||
|
- Start: Start the VM (virt.start)
|
||||||
|
- Stop: Force stop the VM (virt.stop)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- File locking is used to prevent race conditions when multiple processes access the VMs file
|
||||||
|
- The "powercontrol" key is removed from the VM configuration after successful execution
|
||||||
|
- Comprehensive logging for troubleshooting
|
||||||
|
- No continuous loop (salt-master handles scheduling)
|
||||||
|
- File locking is only applied when a powercontrol key is detected, not on every run
|
||||||
|
|
||||||
|
Description:
|
||||||
|
The engine operates in the following phases:
|
||||||
|
|
||||||
|
1. Configuration Processing
|
||||||
|
- Reads VMs file for each hypervisor without locking
|
||||||
|
- Identifies VMs with "powercontrol" key
|
||||||
|
- If powercontrol key is found, acquires lock and reads file again
|
||||||
|
|
||||||
|
2. Power Control Execution
|
||||||
|
- Maps "powercontrol" value to virt module function
|
||||||
|
- Executes appropriate virt module command
|
||||||
|
- Removes "powercontrol" key after successful execution
|
||||||
|
|
||||||
|
3. File Locking
|
||||||
|
- Acquires lock only when a powercontrol key is detected
|
||||||
|
- Releases lock after modifications
|
||||||
|
- Handles lock acquisition failures
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
Log files are written to /opt/so/log/salt/master
|
||||||
|
Comprehensive logging includes:
|
||||||
|
- Power control action details
|
||||||
|
- Command execution results
|
||||||
|
- Error conditions with full context
|
||||||
|
- File locking operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import fcntl
|
||||||
|
import salt.client
|
||||||
|
from typing import Dict, List, Optional, Any, Tuple
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DEFAULT_INTERVAL = 60
|
||||||
|
DEFAULT_BASE_PATH = '/opt/so/saltstack/local/salt/hypervisor/hosts'
|
||||||
|
VALID_POWER_ACTIONS = {'Reboot', 'Reset', 'Shutdown', 'Start', 'Stop'}
|
||||||
|
|
||||||
|
class FileLock:
|
||||||
|
"""
|
||||||
|
Context manager for file locking.
|
||||||
|
|
||||||
|
This class provides a context manager for file locking using fcntl.
|
||||||
|
It acquires an exclusive lock on the file when entering the context
|
||||||
|
and releases the lock when exiting.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
with FileLock(file_path):
|
||||||
|
# Read and modify file
|
||||||
|
# Lock is automatically released when exiting the context
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file_path: str):
|
||||||
|
self.file_path = file_path
|
||||||
|
self.lock_path = f"{file_path}.lock"
|
||||||
|
self.lock_file = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
# Open the lock file
|
||||||
|
self.lock_file = open(self.lock_path, 'w')
|
||||||
|
|
||||||
|
# Acquire exclusive lock
|
||||||
|
fcntl.flock(self.lock_file, fcntl.LOCK_EX)
|
||||||
|
log.debug("Acquired lock on %s", self.file_path)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Failed to acquire lock on %s: %s", self.file_path, str(e))
|
||||||
|
if self.lock_file:
|
||||||
|
self.lock_file.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
try:
|
||||||
|
# Release lock
|
||||||
|
if self.lock_file:
|
||||||
|
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
||||||
|
self.lock_file.close()
|
||||||
|
log.debug("Released lock on %s", self.file_path)
|
||||||
|
|
||||||
|
# Remove lock file
|
||||||
|
if os.path.exists(self.lock_path):
|
||||||
|
os.remove(self.lock_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Error releasing lock on %s: %s", self.file_path, str(e))
|
||||||
|
|
||||||
|
def read_json_file(file_path: str) -> Any:
|
||||||
|
"""
|
||||||
|
Read and parse a JSON file.
|
||||||
|
Returns an empty array if the file is empty.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
if not content:
|
||||||
|
return []
|
||||||
|
return json.loads(content)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Failed to read JSON file %s: %s", file_path, str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def write_json_file(file_path: str, data: Any) -> None:
|
||||||
|
"""Write data to a JSON file."""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Failed to write JSON file %s: %s", file_path, str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def has_power_control_requests(nodes_config: List[Dict]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if any VM in the configuration has a powercontrol key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes_config: List of VM configurations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if at least one VM has a powercontrol key, False otherwise
|
||||||
|
"""
|
||||||
|
return any('powercontrol' in vm_config for vm_config in nodes_config)
|
||||||
|
|
||||||
|
def process_power_control(hypervisor: str, vm_config: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Process a power control request for a VM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hypervisor: Name of the hypervisor
|
||||||
|
vm_config: VM configuration dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the power control action was successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get VM name and power control action
|
||||||
|
vm_name = f"{vm_config['hostname']}_{vm_config['role']}"
|
||||||
|
power_action = vm_config['powercontrol']
|
||||||
|
|
||||||
|
# Validate power action
|
||||||
|
if power_action not in VALID_POWER_ACTIONS:
|
||||||
|
log.error("Invalid power control action: %s", power_action)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Map power action to virt module function
|
||||||
|
virt_function = power_action.lower()
|
||||||
|
|
||||||
|
# Execute power control action
|
||||||
|
log.info("Executing %s on VM %s", power_action, vm_name)
|
||||||
|
client = salt.client.LocalClient()
|
||||||
|
result = client.cmd(
|
||||||
|
f"{hypervisor}_*",
|
||||||
|
f"virt.{virt_function}",
|
||||||
|
[vm_name],
|
||||||
|
expr_form="glob"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if result and any(success for success in result.values()):
|
||||||
|
log.info("Successfully executed %s on VM %s", power_action, vm_name)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.error("Failed to execute %s on VM %s: %s", power_action, vm_name, result)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Error processing power control for VM %s: %s", vm_config.get('hostname', 'unknown'), str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_hypervisor_power_requests(hypervisor_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Process power control requests for a single hypervisor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hypervisor_path: Path to the hypervisor directory
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get hypervisor name from path
|
||||||
|
hypervisor = os.path.basename(hypervisor_path)
|
||||||
|
|
||||||
|
# Read VMs file
|
||||||
|
vms_file = os.path.join(os.path.dirname(hypervisor_path), f"{hypervisor}VMs")
|
||||||
|
if not os.path.exists(vms_file):
|
||||||
|
log.debug("No VMs file found at %s", vms_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# First, read the file without locking to check if any VM has a powercontrol key
|
||||||
|
nodes_config = read_json_file(vms_file)
|
||||||
|
if not nodes_config:
|
||||||
|
log.debug("Empty VMs configuration in %s", vms_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if any VM has a powercontrol key
|
||||||
|
if not has_power_control_requests(nodes_config):
|
||||||
|
log.debug("No power control requests found in %s", vms_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we found powercontrol keys, lock the file and process the requests
|
||||||
|
with FileLock(vms_file):
|
||||||
|
# Read the VMs file again with the lock to ensure we have the latest data
|
||||||
|
nodes_config = read_json_file(vms_file)
|
||||||
|
if not nodes_config:
|
||||||
|
log.debug("Empty VMs configuration in %s (after lock)", vms_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Track if any changes were made
|
||||||
|
changes_made = False
|
||||||
|
|
||||||
|
# Process each VM configuration
|
||||||
|
for i, vm_config in enumerate(nodes_config):
|
||||||
|
if 'powercontrol' in vm_config:
|
||||||
|
# Process power control request
|
||||||
|
log.info("Found power control request for VM %s_%s: %s",
|
||||||
|
vm_config.get('hostname', 'unknown'),
|
||||||
|
vm_config.get('role', 'unknown'),
|
||||||
|
vm_config['powercontrol'])
|
||||||
|
|
||||||
|
success = process_power_control(hypervisor, vm_config)
|
||||||
|
if success:
|
||||||
|
# Remove powercontrol key
|
||||||
|
log.info("Power control action successful, removing powercontrol key")
|
||||||
|
del nodes_config[i]['powercontrol']
|
||||||
|
changes_made = True
|
||||||
|
|
||||||
|
# Write updated configuration if changes were made
|
||||||
|
if changes_made:
|
||||||
|
log.info("Writing updated VM configuration to %s", vms_file)
|
||||||
|
write_json_file(vms_file, nodes_config)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Failed to process hypervisor %s: %s", hypervisor_path, str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def start(interval: int = DEFAULT_INTERVAL,
|
||||||
|
base_path: str = DEFAULT_BASE_PATH) -> None:
|
||||||
|
"""
|
||||||
|
Process virtual machine power control requests.
|
||||||
|
|
||||||
|
This function processes power control requests for virtual machines
|
||||||
|
by monitoring the <hypervisor>VMs files for the "powercontrol" key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval: Time in seconds between engine runs (managed by salt-master)
|
||||||
|
base_path: Base path containing hypervisor configurations
|
||||||
|
"""
|
||||||
|
log.info("Starting virtual power manager engine")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process each hypervisor directory
|
||||||
|
for hypervisor_path in glob.glob(os.path.join(base_path, '*')):
|
||||||
|
if os.path.isdir(hypervisor_path):
|
||||||
|
process_hypervisor_power_requests(hypervisor_path)
|
||||||
|
|
||||||
|
log.info("Virtual power manager completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Error in virtual power manager: %s", str(e))
|
||||||
7
salt/salt/files/hvn_engine.conf
Normal file
7
salt/salt/files/hvn_engine.conf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
engines:
|
||||||
|
- virtual_node_manager:
|
||||||
|
interval: 10
|
||||||
|
base_path: /opt/so/saltstack/local/salt/hypervisor/hosts
|
||||||
|
- virtual_power_manager:
|
||||||
|
interval: 10
|
||||||
|
base_path: /opt/so/saltstack/local/salt/hypervisor/hosts
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
engines:
|
|
||||||
- virtual_node_manager:
|
|
||||||
interval: 30
|
|
||||||
base_path: /opt/so/saltstack/local/salt/hypervisor/hosts
|
|
||||||
@@ -49,6 +49,13 @@ pillarWatch_engine:
|
|||||||
- source: salt://salt/engines/master/pillarWatch.py
|
- source: salt://salt/engines/master/pillarWatch.py
|
||||||
|
|
||||||
{% if 'hvn' in salt['pillar.get']('features', []) %}
|
{% if 'hvn' in salt['pillar.get']('features', []) %}
|
||||||
|
hvn_engine_config:
|
||||||
|
file.managed:
|
||||||
|
- name: /etc/salt/master.d/hvn_engine.conf
|
||||||
|
- source: salt://salt/files/hvn_engine.conf
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_master_service
|
||||||
|
|
||||||
virtual_node_manager_engine:
|
virtual_node_manager_engine:
|
||||||
file.managed:
|
file.managed:
|
||||||
- name: /etc/salt/engines/virtual_node_manager.py
|
- name: /etc/salt/engines/virtual_node_manager.py
|
||||||
@@ -56,11 +63,12 @@ virtual_node_manager_engine:
|
|||||||
- watch_in:
|
- watch_in:
|
||||||
- service: salt_master_service
|
- service: salt_master_service
|
||||||
|
|
||||||
virtual_node_manager_engine_config:
|
virtual_power_manager_engine:
|
||||||
file.managed:
|
file.managed:
|
||||||
- name: /etc/salt/master.d/virtual_node_manager_engine.conf
|
- name: /etc/salt/engines/virtual_power_manager.py
|
||||||
- source: salt://salt/files/virtual_node_manager_engine.conf
|
- source: salt://salt/engines/master/virtual_power_manager.py
|
||||||
|
- watch_in:
|
||||||
|
- service: salt_master_service
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
engines_config:
|
engines_config:
|
||||||
@@ -88,7 +96,6 @@ reactor:
|
|||||||
- 'setup/so-minion':
|
- 'setup/so-minion':
|
||||||
- /opt/so/saltstack/default/salt/reactor/sominion_setup.sls
|
- /opt/so/saltstack/default/salt/reactor/sominion_setup.sls
|
||||||
- 'salt/cloud/*/destroyed':
|
- 'salt/cloud/*/destroyed':
|
||||||
- /opt/so/saltstack/default/salt/reactor/virtReleaseHardware.sls
|
|
||||||
- /opt/so/saltstack/default/salt/reactor/deleteKey.sls
|
- /opt/so/saltstack/default/salt/reactor/deleteKey.sls
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,11 @@ hypervisor:
|
|||||||
readonly: true
|
readonly: true
|
||||||
options: []
|
options: []
|
||||||
forcedType: '[]int'
|
forcedType: '[]int'
|
||||||
|
- field: powercontrol
|
||||||
|
label: "Execute VM power operations"
|
||||||
|
options:
|
||||||
|
- Start
|
||||||
|
- Reboot
|
||||||
|
- Shutdown
|
||||||
|
- Reset
|
||||||
|
- Stop
|
||||||
|
|||||||
Reference in New Issue
Block a user