add extra_script option and qemu-guest-agen sock wrapper

This commit is contained in:
Gytis Stoškevičius 2025-04-04 13:53:19 +00:00
parent e77b22aaa1
commit d04cc507f0
6 changed files with 263 additions and 9 deletions

View file

@ -465,6 +465,11 @@
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>25</Order>
<CommandLine>reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f</CommandLine>
<Description>Disable ip autoconfiguration for network interfaces</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>26</Order>
<CommandLine>cmd /C "type nul > \\host.lan\Data\prepared"</CommandLine>
<Description>Let host known that all configuration is done</Description>
</SynchronousCommand>

62
scripts/enable_sshd.ps1 Normal file
View file

@ -0,0 +1,62 @@
# Define variables
$OpenSSH_URL = "https://github.com/PowerShell/Win32-OpenSSH/releases/latest/download/OpenSSH-Win64.zip"
$OpenSSH_Install_Path = "C:\Program Files\OpenSSH"
$OpenSSH_Zip = "$env:TEMP\OpenSSH-Win64.zip"
# Function to check if running as Administrator
function Test-Admin {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal($currentUser)
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-Admin)) {
Write-Host "Please run this script as Administrator!" -ForegroundColor Red
exit 1
}
# Ensure the install path exists
if (!(Test-Path $OpenSSH_Install_Path)) {
New-Item -ItemType Directory -Path $OpenSSH_Install_Path -Force
}
# Download OpenSSH if not already present
Write-Host "Downloading OpenSSH..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $OpenSSH_URL -OutFile $OpenSSH_Zip
# Extract OpenSSH
Write-Host "Extracting OpenSSH..." -ForegroundColor Cyan
Expand-Archive -Path $OpenSSH_Zip -DestinationPath $OpenSSH_Install_Path -Force
# Check if install-sshd.ps1 exists
if (!(Test-Path "$OpenSSH_Install_Path\OpenSSH-Win64\install-sshd.ps1")) {
Write-Host "❌ Error: install-sshd.ps1 not found in $OpenSSH_Install_Path. Extraction failed!" -ForegroundColor Red
exit 1
}
# Navigate to OpenSSH directory
Push-Location -Path $OpenSSH_Install_Path\OpenSSH-Win64
# Run install script
Write-Host "Installing OpenSSH service..." -ForegroundColor Green
powershell.exe -ExecutionPolicy Bypass -File install-sshd.ps1
# Set SSHD service to start automatically
Write-Host "Setting SSHD to start automatically..." -ForegroundColor Green
if (Get-Service sshd -ErrorAction SilentlyContinue) {
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
} else {
Write-Host "⚠ OpenSSH service was not installed correctly. Try running install-sshd.ps1 manually." -ForegroundColor Red
exit 1
}
# Verify installation
$sshdStatus = Get-Service -Name sshd -ErrorAction SilentlyContinue
if ($sshdStatus.Status -eq 'Running') {
Write-Host "✅ OpenSSH installation successful! You can now connect via SSH." -ForegroundColor Green
} else {
Write-Host "⚠ OpenSSH installation failed. Try restarting your computer and rerun the script." -ForegroundColor Red
}
Pop-Location

View file

@ -3,5 +3,6 @@ pushd "C:/OEM"
powershell -ExecutionPolicy Bypass -File "dependencies_windows.ps1"
powershell -ExecutionPolicy Bypass -File "optimize.ps1"
powershell -ExecutionPolicy Bypass -File "disable_updates.ps1"
powershell -ExecutionPolicy Bypass -File "enable_sshd.ps1"
popd

View file

@ -36,16 +36,38 @@ terminal
(
sleep 30
boot
configure_guest_network_interface
info "Windows started succesfully, you can now connect using RDP"
if [[ "${NETWORK,,}" != "bridge"* ]]; then
info "or visit http://localhost:8006/ to view the screen..."
if ! configure_guest_network_interface; then
error "Failed to configure guest network interfaces"
exit 666
fi
if [[ -n "${EXTRA_SCRIPT:-}" ]]; then
info "Executing extra script: $EXTRA_SCRIPT"
if ! "$EXTRA_SCRIPT"; then
error "Extra script failed"
exit 555
fi
fi
info "Windows started successfully, you can now connect using RDP or visit http://localhost:8006/ to view the screen..."
touch "$STORAGE/ready"
) &
bg_pid=$!
tail -fn +0 "$QEMU_LOG" 2>/dev/null &
cat "$QEMU_TERM" 2>/dev/null | tee "$QEMU_PTY" &
wait $! || :
term_pd=$!
wait $bg_pid
exit_code=$?
if [[ $exit_code -ne 0 ]]; then
error "A critical process failed, exiting container..."
exit $exit_code
fi
wait $term_pd || :
sleep 1 &
wait $!

View file

@ -58,7 +58,7 @@ configure_guest_network_interface() {
if [ -z "$CURRENT_IP" ]; then
echo "Error: Unable to retrieve the current IP address of $HOST_INTERFACE."
exit 1
return 1
fi
echo "Current Host IP: $CURRENT_IP"
@ -84,10 +84,33 @@ configure_guest_network_interface() {
INTERFACE_NAME="Ethernet $IDX"
fi
echo -e '{"execute": "guest-exec", "arguments": {"path": "C:\\\\Windows\\\\System32\\\\netsh.exe", "capture-output": true, "arg": ["interface", "ipv4", "set", "address", "'"$INTERFACE_NAME"'", "static", "'$CURRENT_IP'", "255.255.255.0", "'$GW'"]}}' | nc -U /tmp/qga.sock -w 5
echo -e '{"execute": "guest-exec", "arguments": {"path": "C:\\\\Windows\\\\System32\\\\netsh.exe", "capture-output": true, "arg": ["interface", "ipv4", "add", "dnsservers", "'"$INTERFACE_NAME"'", "1.1.1.1", "index=1"]}}' | nc -U /tmp/qga.sock -w 5
exit_code=0
python3 /run/qga.py powershell -Command "Set-NetIPInterface -InterfaceAlias '$INTERFACE_NAME' -Dhcp Disabled" || exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "Failed to disable dhcp using qga.py" >&2
return 2
fi
if [[ -f "$STORAGE/interfaces_configured" ]]; then
python3 /run/qga.py powershell -Command "Remove-NetIPAddress -IPAddress '$CURRENT_IP' -Confirm:\$false" || true
python3 /run/qga.py powershell -Command "Remove-NetRoute -InterfaceAlias '$INTERFACE_NAME' -DestinationPrefix '0.0.0.0/0' -Confirm:\$false" || true
fi
python3 /run/qga.py powershell -Command "New-NetIPAddress -InterfaceAlias '$INTERFACE_NAME' -IPAddress '$CURRENT_IP' -PrefixLength 24 -DefaultGateway '$GW'" || exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "Failed to set ip address using qga.py" >&2
return 3
fi
python3 /run/qga.py powershell -Command "Set-DnsClientServerAddress -InterfaceAlias '$INTERFACE_NAME' -ServerAddresses 1.1.1.1" || exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "Failed to set dns server using qga.py" >&2
return 4
fi
done
touch "$STORAGE/interfaces_configured"
fi
return 0
@ -465,7 +488,10 @@ closeNetwork() {
[[ "$NETWORK" == [Nn]* ]] && return 0
exec 30<&- || true
exec 40<&- || true
for ((i = 0; i < ETH_COUNT; i++)); do
fd=$((40 + i))
eval "exec $fd<&-" || true
done
if [[ "$DHCP" == [Yy1]* ]]; then

138
src/qga.py Normal file
View file

@ -0,0 +1,138 @@
import argparse
import base64
import json
import socket
import sys
import time
QGA_SOCKET = "/tmp/qga.sock" # Adjust if needed
def send_qga_command(sock, command):
"""Send a JSON command to the QEMU Guest Agent socket and receive the response."""
try:
cmd = (json.dumps(command) + "\n").encode()
sock.sendall(cmd)
response = sock.recv(4096)
return json.loads(response.decode())
except socket.timeout:
print(f"Timeout waiting for response from {QGA_SOCKET}", file=sys.stderr)
return None
except Exception as e:
print(f"Error communicating with socket: {e}", file=sys.stderr)
return None
def decode_output(data):
"""Try to decode output as hex or Base64, or return raw."""
if not data:
return ""
try:
# Try Hex decoding first
return bytes.fromhex(data).decode("utf-8", errors="ignore")
except ValueError:
pass
try:
# If hex fails, try Base64 decoding
return base64.b64decode(data).decode("utf-8", errors="ignore")
except ValueError:
pass
# If all decoding fails, return raw
return data
def execute_command(sock, command_path, command_args):
"""Execute a command inside the guest VM with specified path and arguments."""
exec_request = {
"execute": "guest-exec",
"arguments": {
"path": command_path,
"arg": command_args,
"capture-output": True, # Capture stdout and stderr
},
}
response = send_qga_command(sock, exec_request)
if response is None:
return None
if "return" not in response or "pid" not in response["return"]:
print("Error: Failed to start execution:", response, file=sys.stderr)
return None
pid = response["return"]["pid"]
print(f"Command started with PID {pid}")
# Step 2: Wait for completion
while True:
status_request = {"execute": "guest-exec-status", "arguments": {"pid": pid}}
status_response = send_qga_command(sock, status_request)
if status_response is None:
continue
if "return" in status_response:
status = status_response["return"]
if status.get("exited", False):
break # Command finished
time.sleep(0.2) # Wait before checking again
# Step 3: Get exit code and output
exit_code = status.get("exitcode", -1)
stdout_data = decode_output(status.get("out-data", ""))
stderr_data = decode_output(status.get("err-data", ""))
return {"exit_code": exit_code, "stdout": stdout_data, "stderr": stderr_data}
def create_socket():
"""Create and return a reusable socket connection to the QEMU Guest Agent."""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(30) # 30 seconds timeout
try:
sock.connect(QGA_SOCKET)
return sock
except Exception as e:
print(f"Error creating socket: {e}", file=sys.stderr)
return None
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="Send commands to QEMU Guest Agent.")
parser.add_argument(
"command", help="Path to the command to execute inside the guest VM"
)
parser.add_argument(
"args", nargs=argparse.REMAINDER, help="Arguments to pass to the command"
)
return parser.parse_args()
if __name__ == "__main__":
# Parse command-line arguments
args = parse_args()
# Create a reusable socket
unix_sock = create_socket()
if not unix_sock:
print("Failed to create socket.", file=sys.stderr)
sys.exit(1) # Exit if we can't connect to the socket
# Execute the command
result = execute_command(unix_sock, args.command, args.args)
if result:
print(f"Exit Code: {result['exit_code']}")
if result["stdout"]:
print("STDOUT:\n", result["stdout"])
if result["stderr"]:
print("STDERR:\n", result["stderr"])
# Close the socket once all commands are executed
unix_sock.close()
# Exit with the appropriate code based on command execution result
sys.exit(result["exit_code"] if result else 2)