#!/bin/bash
set -euo pipefail
VIRTCCA_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../src/virtcca_deploy/" && pwd)"
GUEST_AGENT_DIR="$VIRTCCA_DEPLOY_DIR/guest_agent"
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "$0")"
SCRIPT_PID=$$
START_TIME=$(date +%s)
START_TIME_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="/tmp/inject_guest_agent_${SCRIPT_PID}.log"
TEMP_DIR=""
STEP_COUNT=0
TOTAL_STEPS=5
ERROR_COUNT=0
WARN_COUNT=0
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
IMAGE_PATH="/etc/virtcca_deploy/base.qcow2"
INSTALL_DIR="/opt/virtcca"
SSL_ENABLED="true"
SSL_CERT_SRC=""
SSL_KEY_SRC=""
SSL_CA_CERT_SRC=""
SSL_CERT_PATH=""
SSL_KEY_PATH=""
SSL_CA_CERT_PATH=""
SSL_CERT_VM_DIR="/etc/virtcca_deploy/cert"
DRY_RUN="false"
KEEP_TEMP="false"
log_msg() {
local level="$1"
local module="$2"
local message="$3"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local log_line="[${timestamp}] [${level}] [${module}] [PID:${SCRIPT_PID}] ${message}"
case "$level" in
INFO) echo -e "${GREEN}${log_line}${NC}" ;;
WARN) echo -e "${YELLOW}${log_line}${NC}"; ((WARN_COUNT++)) ;;
ERROR) echo -e "${RED}${log_line}${NC}"; ((ERROR_COUNT++)) ;;
DEBUG) echo -e "${CYAN}${log_line}${NC}" ;;
STEP) echo -e "${BLUE}--- ${log_line} ---${NC}" ;;
esac
echo "$log_line" >> "$LOG_FILE"
}
log_info() { log_msg "INFO" "$1" "$2"; }
log_warn() { log_msg "WARN" "$1" "$2"; }
log_error() { log_msg "ERROR" "$1" "$2"; }
log_debug() { log_msg "DEBUG" "$1" "$2"; }
log_step() { log_msg "STEP" "$1" "$2"; }
die() {
log_error "${1:-"Unknown error"}" "${2:-""}"
exit 1
}
declare -A STEP_TIMES
start_timer() {
STEP_TIMES["$1"]=$(date +%s)
}
stop_timer() {
local step="$1"
local end_time
end_time=$(date +%s)
local start=${STEP_TIMES["$step"]:-$end_time}
echo $((end_time - start))
}
show_usage() {
echo -e "${BLUE}Guest Agent VM Image Injection Tool${NC}"
echo ""
echo "Usage: $0 -i <image_path> [options]"
echo ""
echo "Options:"
echo " -i <path> 虚拟机镜像路径 (qcow2 格式)"
echo " -d <dir> 虚机内安装目录 (默认: /opt/virtcca/guest_agent)"
echo " --ssl 启用 SSL 加密通信"
echo " --ssl-cert-src <p> 宿主机 SSL 证书文件路径 (将被复制到虚机)"
echo " --ssl-key-src <p> 宿主机 SSL 私钥文件路径 (将被复制到虚机)"
echo " --ssl-ca-src <p> 宿主机 SSL CA 证书文件路径 (可选, 将被复制到虚机)"
echo " --ssl-cert-vm-dir <d> 虚机内证书存放目录 (默认: /etc/virtcca_deploy/cert)"
echo " --ssl-cert <p> SSL 证书路径 (虚机内路径, 如不指定则使用 --ssl-cert-src)"
echo " --ssl-key <p> SSL 私钥路径 (虚机内路径, 如不指定则使用 --ssl-key-src)"
echo " --ssl-ca <p> SSL CA 证书路径 (虚机内路径, 可选)"
echo " --dry-run 仅检查环境并准备文件,不执行注入"
echo " -h 显示帮助信息"
echo ""
echo "Example:"
echo " $0 -i /etc/virtcca_deploy/base.qcow2"
echo " $0 -i /etc/virtcca_deploy/base.qcow2 --ssl --ssl-cert-src /path/to/agent.crt --ssl-key-src /path/to/agent.key"
echo " $0 -i /etc/virtcca_deploy/base.qcow2 --ssl --ssl-cert-src /path/to/agent.crt --ssl-key-src /path/to/agent.key --ssl-ca-src /path/to/ca.crt"
}
parse_args() {
log_step "PARAM" "Parsing command line arguments"
while [[ $# -gt 0 ]]; do
case $1 in
-i) IMAGE_PATH="$2"; shift 2 ;;
-d) INSTALL_DIR="$2"; shift 2 ;;
--ssl) SSL_ENABLED="true"; shift ;;
--ssl-cert-src) SSL_CERT_SRC="$2"; shift 2 ;;
--ssl-key-src) SSL_KEY_SRC="$2"; shift 2 ;;
--ssl-ca-src) SSL_CA_CERT_SRC="$2"; shift 2 ;;
--ssl-cert-vm-dir) SSL_CERT_VM_DIR="$2"; shift 2 ;;
--ssl-cert) SSL_CERT_PATH="$2"; shift 2 ;;
--ssl-key) SSL_KEY_PATH="$2"; shift 2 ;;
--ssl-ca) SSL_CA_CERT_PATH="$2"; shift 2 ;;
--dry-run) DRY_RUN="true"; shift ;;
-h) show_usage; exit 0 ;;
*) log_error "PARAM" "Unknown option: $1"; show_usage; exit 1 ;;
esac
done
if [ -z "$IMAGE_PATH" ]; then
log_error "PARAM" "Image path is required (-i)"
show_usage
exit 1
fi
if [ ! -f "$IMAGE_PATH" ]; then
log_error "PARAM" "Image file not found: $IMAGE_PATH"
exit 1
fi
if [ -n "$SSL_CERT_SRC" ]; then
SSL_CERT_PATH="$SSL_CERT_VM_DIR/$(basename "$SSL_CERT_SRC")"
fi
if [ -n "$SSL_KEY_SRC" ]; then
SSL_KEY_PATH="$SSL_CERT_VM_DIR/$(basename "$SSL_KEY_SRC")"
fi
if [ -n "$SSL_CA_CERT_SRC" ]; then
SSL_CA_CERT_PATH="$SSL_CERT_VM_DIR/$(basename "$SSL_CA_CERT_SRC")"
fi
if [ -n "$SSL_CERT_SRC" ] && [ ! -f "$SSL_CERT_SRC" ]; then
log_error "PARAM" "SSL cert source file not found: $SSL_CERT_SRC"
exit 1
fi
if [ -n "$SSL_KEY_SRC" ] && [ ! -f "$SSL_KEY_SRC" ]; then
log_error "PARAM" "SSL key source file not found: $SSL_KEY_SRC"
exit 1
fi
if [ -n "$SSL_CA_CERT_SRC" ] && [ ! -f "$SSL_CA_CERT_SRC" ]; then
log_error "PARAM" "SSL CA cert source file not found: $SSL_CA_CERT_SRC"
exit 1
fi
log_info "PARAM" "IMAGE_PATH=$IMAGE_PATH"
log_info "PARAM" "INSTALL_DIR=$INSTALL_DIR"
log_info "PARAM" "SSL_ENABLED=$SSL_ENABLED"
if [ "$SSL_ENABLED" = "true" ]; then
if [ -n "$SSL_CERT_SRC" ]; then
log_info "PARAM" "SSL_CERT_SRC=$SSL_CERT_SRC -> VM:$SSL_CERT_PATH"
log_info "PARAM" "SSL_KEY_SRC=$SSL_KEY_SRC -> VM:$SSL_KEY_PATH"
log_info "PARAM" "SSL_CA_CERT_SRC=${SSL_CA_CERT_SRC:-<not set>} -> VM:${SSL_CA_CERT_PATH:-<not set>}"
log_info "PARAM" "SSL_CERT_VM_DIR=$SSL_CERT_VM_DIR"
else
log_info "PARAM" "SSL_CERT_PATH=$SSL_CERT_PATH"
log_info "PARAM" "SSL_KEY_PATH=$SSL_KEY_PATH"
log_info "PARAM" "SSL_CA_CERT_PATH=${SSL_CA_CERT_PATH:-<not set>}"
fi
fi
log_info "PARAM" "DRY_RUN=$DRY_RUN"
}
step1_check_environment() {
log_step "CHECK" "Step 1/5: Checking environment"
start_timer "check"
local check_pass=true
if command -v virt-customize &>/dev/null; then
local version
version=$(virt-customize --version 2>/dev/null || echo "unknown")
log_info "CHECK" "virt-customize found: $version [OK]"
else
log_error "CHECK" "virt-customize not found. Install: yum install libguestfs-tools"
check_pass=false
fi
if command -v qemu-img &>/dev/null; then
log_info "CHECK" "qemu-img found [OK]"
else
log_warn "CHECK" "qemu-img not found, image validation skipped"
fi
if command -v qemu-img &>/dev/null; then
local fmt
fmt=$(qemu-img info --output=json "$IMAGE_PATH" | jq -r '.format')
log_info "CHECK" "Image format: $fmt"
if [[ "$fmt" != "qcow2" && "$fmt" != "raw" ]]; then
log_warn "CHECK" "Image format '$fmt' may not be fully supported"
fi
fi
if [ -r "$IMAGE_PATH" ] && [ -w "$IMAGE_PATH" ]; then
local size
size=$(du -h "$IMAGE_PATH" | cut -f1)
log_info "CHECK" "Image permissions: readable+writable [OK]"
log_info "CHECK" "Image size: $size"
else
log_error "CHECK" "Image file not readable/writable: $IMAGE_PATH"
check_pass=false
fi
if [ -d "$VIRTCCA_DEPLOY_DIR" ] && [ -d "$GUEST_AGENT_DIR" ]; then
log_info "CHECK" "virtcca_deploy dir: $VIRTCCA_DEPLOY_DIR [OK]"
else
log_error "CHECK" "virtcca_deploy not found"
check_pass=false
fi
if [ -f "$GUEST_AGENT_DIR/requirements.txt" ]; then
local dep_count
dep_count=$(wc -l < "$GUEST_AGENT_DIR/requirements.txt")
log_info "CHECK" "requirements.txt found: $dep_count dependencies [OK]"
else
log_warn "CHECK" "requirements.txt not found"
fi
if [ -f "$GUEST_AGENT_DIR/virtcca-guest-agent.service" ]; then
log_info "CHECK" "systemd service file found [OK]"
else
log_warn "CHECK" "virtcca-guest-agent.service not found, auto-start may not work"
fi
local elapsed
elapsed=$(stop_timer "check")
log_info "CHECK" "Environment check completed in ${elapsed}s"
if [ "$check_pass" = false ]; then
die "Environment check failed, aborting" "CHECK"
fi
}
step2_prepare_temp() {
log_step "TEMP" "Step 2/5: Preparing temporary directory"
start_timer "temp"
TEMP_DIR=$(mktemp -d /tmp/ga_inject_XXXXXX)
log_info "TEMP" "Created temp directory: $TEMP_DIR"
cp -r "$VIRTCCA_DEPLOY_DIR" "$TEMP_DIR/virtcca_deploy"
local file_count
file_count=$(find "$TEMP_DIR/virtcca_deploy" -type f | wc -l)
log_info "TEMP" "Copied $file_count files from virtcca_deploy to temp directory"
local elapsed
elapsed=$(stop_timer "temp")
log_info "TEMP" "Temp directory prepared in ${elapsed}s"
}
step3_copy_setup_script() {
log_step "COPY" "Step 3/5: Copying standalone setup script"
start_timer "copy"
local setup_src="$SCRIPTS_DIR/setup_vm.sh"
if [ ! -f "$setup_src" ]; then
log_error "COPY" "setup_vm.sh not found at: $setup_src"
die "setup_vm.sh not found in guest_agent source directory" "COPY"
fi
cp "$setup_src" "$TEMP_DIR/setup_vm.sh"
chmod +x "$TEMP_DIR/setup_vm.sh"
log_info "COPY" "setup_vm.sh copied to temp directory"
log_info "COPY" "Source: $setup_src"
log_info "COPY" "Dest: $TEMP_DIR/setup_vm.sh"
local elapsed
elapsed=$(stop_timer "copy")
log_info "COPY" "Setup script copied in ${elapsed}s"
}
step4_inject() {
log_step "INJECT" "Step 4/5: Injecting guest agent into VM image"
start_timer "inject"
log_info "INJECT" "Target image: $IMAGE_PATH"
log_info "INJECT" "Install dir (VM): $INSTALL_DIR"
log_info "INJECT" "Temp directory: $TEMP_DIR"
local has_certs=false
if [ -n "$SSL_CERT_SRC" ] && [ -n "$SSL_KEY_SRC" ]; then
mkdir -p "$TEMP_DIR/certs"
cp "$SSL_CERT_SRC" "$TEMP_DIR/certs/"
cp "$SSL_KEY_SRC" "$TEMP_DIR/certs/"
if [ -n "$SSL_CA_CERT_SRC" ]; then
cp "$SSL_CA_CERT_SRC" "$TEMP_DIR/certs/"
fi
has_certs=true
log_info "INJECT" "SSL certs copied to temp directory: $TEMP_DIR/certs"
fi
local conf_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../conf" && pwd)"
local has_ifcfg_template=false
if [ -f "$conf_dir/ifcfg-template" ]; then
mkdir -p "$TEMP_DIR/conf"
cp "$conf_dir/ifcfg-template" "$TEMP_DIR/conf/"
has_ifcfg_template=true
log_info "INJECT" "ifcfg-template copied to temp directory: $TEMP_DIR/conf/"
else
log_warn "INJECT" "ifcfg-template not found in $conf_dir"
fi
echo ""
echo -e "${CYAN}============================================================${NC}"
echo -e "${CYAN}Manual virt-customize command (for verification):${NC}"
echo -e "${CYAN}============================================================${NC}"
echo ""
echo "virt-customize -a \"$IMAGE_PATH\" \\"
echo " --run-command \"mkdir -p /tmp/ga_inject\" \\"
echo " --copy-in $TEMP_DIR/virtcca_deploy:/tmp/ga_inject/ \\"
echo " --copy-in $TEMP_DIR/setup_vm.sh:/tmp/ga_inject/"
if [ "$has_certs" = true ]; then
echo " --copy-in $TEMP_DIR/certs/agent.crt:$SSL_CERT_VM_DIR/ \\"
echo " --copy-in $TEMP_DIR/certs/agent.key:$SSL_CERT_VM_DIR/ \\"
echo " --copy-in $TEMP_DIR/certs/ca.crt:$SSL_CERT_VM_DIR/ \\"
fi
if [ "$has_ifcfg_template" = true ]; then
echo " --copy-in $TEMP_DIR/conf/ifcfg-template:/etc/virtcca_deploy/ \\"
fi
echo " --run-command \"chmod +x /tmp/ga_inject/setup_vm.sh\" \\"
echo " --run-command \"/tmp/ga_inject/setup_vm.sh \\\"$INSTALL_DIR\\\" \\\"$SSL_ENABLED\\\" \\\"$SSL_CERT_PATH\\\" \\\"$SSL_KEY_PATH\\\" \\\"$SSL_CA_CERT_PATH\\\"\" \\"
echo " --run-command \"rm -rf /tmp/ga_inject\""
echo ""
echo -e "${CYAN}============================================================${NC}"
echo ""
log_info "INJECT" "You can run the above command manually to verify the injection"
if [ "$DRY_RUN" = "true" ]; then
log_info "INJECT" "DRY RUN mode - skipping automatic injection"
log_info "INJECT" "Temp files preserved at: $TEMP_DIR"
log_info "INJECT" "Please run the virt-customize command above manually"
return 0
fi
local mtime_before
mtime_before=$(stat -c %Y "$IMAGE_PATH" 2>/dev/null || echo "unknown")
log_info "INJECT" "Image mtime before: $mtime_before"
local vc_cmd="virt-customize -a \"$IMAGE_PATH\""
vc_cmd+=" --run-command \"mkdir -p /tmp/ga_inject\""
vc_cmd+=" --copy-in \"$TEMP_DIR/virtcca_deploy:/tmp/ga_inject/\""
vc_cmd+=" --copy-in \"$TEMP_DIR/setup_vm.sh:/tmp/ga_inject/\""
if [ "$has_certs" = true ]; then
vc_cmd+=" --run-command \"mkdir -p $SSL_CERT_VM_DIR\""
vc_cmd+=" --copy-in \"$TEMP_DIR/certs/agent.crt:$SSL_CERT_VM_DIR/\""
vc_cmd+=" --copy-in \"$TEMP_DIR/certs/agent.key:$SSL_CERT_VM_DIR/\""
vc_cmd+=" --copy-in \"$TEMP_DIR/certs/ca.crt:$SSL_CERT_VM_DIR/\""
log_info "INJECT" "SSL certs will be copied to VM: $SSL_CERT_VM_DIR"
fi
if [ "$has_ifcfg_template" = true ]; then
vc_cmd+=" --run-command \"mkdir -p /etc/virtcca_deploy\""
vc_cmd+=" --copy-in \"$TEMP_DIR/conf/ifcfg-template:/etc/virtcca_deploy/\""
log_info "INJECT" "ifcfg-template will be copied to VM: /etc/virtcca_deploy/"
fi
vc_cmd+=" --run-command \"chmod +x /tmp/ga_inject/setup_vm.sh\""
vc_cmd+=" --run-command \"/tmp/ga_inject/setup_vm.sh \\\"$INSTALL_DIR\\\" \\\"$SSL_ENABLED\\\" \\\"$SSL_CERT_PATH\\\" \\\"$SSL_KEY_PATH\\\" \\\"$SSL_CA_CERT_PATH\\\"\""
vc_cmd+=" --run-command \"sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config\""
vc_cmd+=" --run-command \"rm -rf /tmp/ga_inject\""
log_info "INJECT" "Running virt-customize..."
eval $vc_cmd 2>&1 | tee -a "$LOG_FILE"
local inject_rc=${PIPESTATUS[0]}
if [ $inject_rc -eq 0 ]; then
log_info "INJECT" "virt-customize completed successfully"
else
log_error "INJECT" "virt-customize failed with exit code: $inject_rc"
log_info "INJECT" "Temp files preserved at: $TEMP_DIR"
log_info "INJECT" "You can retry with the manual command above"
die "virt-customize injection failed" "INJECT"
fi
local mtime_after
mtime_after=$(stat -c %Y "$IMAGE_PATH" 2>/dev/null || echo "unknown")
log_info "INJECT" "Image mtime after: $mtime_after"
if [ "$mtime_before" != "$mtime_after" ] && [ "$mtime_before" != "unknown" ]; then
log_info "INJECT" "Image file was modified (confirmed)"
else
log_warn "INJECT" "Image file timestamp unchanged - verify injection result"
fi
local elapsed
elapsed=$(stop_timer "inject")
log_info "INJECT" "Injection completed in ${elapsed}s"
}
step5_cleanup() {
log_step "CLEANUP" "Step 5/5: Cleanup"
start_timer "cleanup"
local end_time
end_time=$(date +%s)
local total_time=$((end_time - START_TIME))
local end_time_human
end_time_human=$(date '+%Y-%m-%d %H:%M:%S')
if [ -n "$TEMP_DIR" ] && [ "$KEEP_TEMP" = "false" ]; then
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
log_info "CLEANUP" "Temp directory removed: $TEMP_DIR"
fi
elif [ "$KEEP_TEMP" = "true" ] && [ -n "$TEMP_DIR" ]; then
log_info "CLEANUP" "Temp directory preserved: $TEMP_DIR"
log_info "CLEANUP" "Contents:"
find "$TEMP_DIR" -type f | while read -r f; do
log_info "CLEANUP" " $f"
done
fi
log_info "CLEANUP" "Full log saved to: $LOG_FILE"
local elapsed
elapsed=$(stop_timer "cleanup")
log_info "CLEANUP" "Cleanup completed in ${elapsed}s"
echo ""
echo -e "${BLUE}================================================================================${NC}"
echo -e "${GREEN} Guest Agent Injection Completed Successfully${NC}"
echo -e "${BLUE}================================================================================${NC}"
echo -e " Duration: ${total_time}s"
echo -e " Warnings: $WARN_COUNT"
echo -e " Errors: $ERROR_COUNT"
echo -e " Log File: $LOG_FILE"
echo -e "${BLUE}================================================================================${NC}"
}
error_handler() {
local line_no="$1"
local error_code="$2"
log_error "FATAL" "Error on line $line_no: exit code $error_code"
log_error "FATAL" "Temp directory: ${TEMP_DIR:-<not created>}"
log_error "FATAL" "Log file: $LOG_FILE"
exit "$error_code"
}
trap 'error_handler ${LINENO} $?' ERR
main() {
echo "" > "$LOG_FILE"
log_info "MAIN" "=========================================="
log_info "MAIN" "Guest Agent Injection Tool Starting"
log_info "MAIN" "=========================================="
log_info "MAIN" "Start time: $START_TIME_HUMAN"
log_info "MAIN" "Process ID: $SCRIPT_PID"
log_info "MAIN" "Log file: $LOG_FILE"
parse_args "$@"
if [ "$DRY_RUN" = "true" ]; then
log_info "MAIN" "DRY RUN mode enabled - no changes will be made"
fi
step1_check_environment
step2_prepare_temp
step3_copy_setup_script
step4_inject
step5_cleanup
log_info "MAIN" "All steps completed successfully"
}
main "$@"