mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-25 00:53:48 +00:00
497 lines
16 KiB
Bash
Executable File
497 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# PatchMon Docker Agent Script v1.2.9
|
||
# This script collects Docker container and image information and sends it to PatchMon
|
||
|
||
# Configuration
|
||
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
|
||
API_VERSION="v1"
|
||
AGENT_VERSION="1.2.9"
|
||
CONFIG_FILE="/etc/patchmon/agent.conf"
|
||
CREDENTIALS_FILE="/etc/patchmon/credentials"
|
||
LOG_FILE="/var/log/patchmon-docker-agent.log"
|
||
|
||
# Curl flags placeholder (replaced by server based on SSL settings)
|
||
CURL_FLAGS=""
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Logging function
|
||
log() {
|
||
if [[ -w "$(dirname "$LOG_FILE")" ]] 2>/dev/null; then
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# Error handling
|
||
error() {
|
||
echo -e "${RED}ERROR: $1${NC}" >&2
|
||
log "ERROR: $1"
|
||
exit 1
|
||
}
|
||
|
||
# Info logging
|
||
info() {
|
||
echo -e "${BLUE}ℹ️ $1${NC}" >&2
|
||
log "INFO: $1"
|
||
}
|
||
|
||
# Success logging
|
||
success() {
|
||
echo -e "${GREEN}✅ $1${NC}" >&2
|
||
log "SUCCESS: $1"
|
||
}
|
||
|
||
# Warning logging
|
||
warning() {
|
||
echo -e "${YELLOW}⚠️ $1${NC}" >&2
|
||
log "WARNING: $1"
|
||
}
|
||
|
||
# Check if Docker is installed and running
|
||
check_docker() {
|
||
if ! command -v docker &> /dev/null; then
|
||
error "Docker is not installed on this system"
|
||
fi
|
||
|
||
if ! docker info &> /dev/null; then
|
||
error "Docker daemon is not running or you don't have permission to access it. Try running with sudo."
|
||
fi
|
||
}
|
||
|
||
# Load credentials
|
||
load_credentials() {
|
||
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
|
||
error "Credentials file not found at $CREDENTIALS_FILE. Please configure the main PatchMon agent first."
|
||
fi
|
||
|
||
source "$CREDENTIALS_FILE"
|
||
|
||
if [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then
|
||
error "API credentials not found in $CREDENTIALS_FILE"
|
||
fi
|
||
|
||
# Use PATCHMON_URL from credentials if available, otherwise use default
|
||
if [[ -n "$PATCHMON_URL" ]]; then
|
||
PATCHMON_SERVER="$PATCHMON_URL"
|
||
fi
|
||
}
|
||
|
||
# Load configuration
|
||
load_config() {
|
||
if [[ -f "$CONFIG_FILE" ]]; then
|
||
source "$CONFIG_FILE"
|
||
if [[ -n "$SERVER_URL" ]]; then
|
||
PATCHMON_SERVER="$SERVER_URL"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Collect Docker containers
|
||
collect_containers() {
|
||
info "Collecting Docker container information..."
|
||
|
||
local containers_json="["
|
||
local first=true
|
||
|
||
# Get all containers (running and stopped)
|
||
while IFS='|' read -r container_id name image status state created started ports; do
|
||
if [[ -z "$container_id" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# Parse image name and tag
|
||
local image_name="${image%%:*}"
|
||
local image_tag="${image##*:}"
|
||
if [[ "$image_tag" == "$image_name" ]]; then
|
||
image_tag="latest"
|
||
fi
|
||
|
||
# Determine image source based on registry
|
||
local image_source="docker-hub"
|
||
if [[ "$image_name" == ghcr.io/* ]]; then
|
||
image_source="github"
|
||
elif [[ "$image_name" == registry.gitlab.com/* ]]; then
|
||
image_source="gitlab"
|
||
elif [[ "$image_name" == *"/"*"/"* ]]; then
|
||
image_source="private"
|
||
fi
|
||
|
||
# Get repository name (without registry prefix for common registries)
|
||
local image_repository="$image_name"
|
||
image_repository="${image_repository#ghcr.io/}"
|
||
image_repository="${image_repository#registry.gitlab.com/}"
|
||
|
||
# Get image ID
|
||
local full_image_id=$(docker inspect --format='{{.Image}}' "$container_id" 2>/dev/null || echo "unknown")
|
||
full_image_id="${full_image_id#sha256:}"
|
||
|
||
# Normalize status (extract just the status keyword)
|
||
local normalized_status="unknown"
|
||
if [[ "$status" =~ ^Up ]]; then
|
||
normalized_status="running"
|
||
elif [[ "$status" =~ ^Exited ]]; then
|
||
normalized_status="exited"
|
||
elif [[ "$status" =~ ^Created ]]; then
|
||
normalized_status="created"
|
||
elif [[ "$status" =~ ^Restarting ]]; then
|
||
normalized_status="restarting"
|
||
elif [[ "$status" =~ ^Paused ]]; then
|
||
normalized_status="paused"
|
||
elif [[ "$status" =~ ^Dead ]]; then
|
||
normalized_status="dead"
|
||
fi
|
||
|
||
# Parse ports
|
||
local ports_json="null"
|
||
if [[ -n "$ports" && "$ports" != "null" ]]; then
|
||
# Convert Docker port format to JSON
|
||
ports_json=$(echo "$ports" | jq -R -s -c 'split(",") | map(select(length > 0)) | map(split("->") | {(.[0]): .[1]}) | add // {}')
|
||
fi
|
||
|
||
# Convert dates to ISO 8601 format
|
||
# If date conversion fails, use null instead of invalid date string
|
||
local created_iso=$(date -d "$created" -Iseconds 2>/dev/null || echo "null")
|
||
local started_iso="null"
|
||
if [[ -n "$started" && "$started" != "null" ]]; then
|
||
started_iso=$(date -d "$started" -Iseconds 2>/dev/null || echo "null")
|
||
fi
|
||
|
||
# Add comma for JSON array
|
||
if [[ "$first" == false ]]; then
|
||
containers_json+=","
|
||
fi
|
||
first=false
|
||
|
||
# Build JSON object for this container
|
||
containers_json+="{\"container_id\":\"$container_id\","
|
||
containers_json+="\"name\":\"$name\","
|
||
containers_json+="\"image_name\":\"$image_name\","
|
||
containers_json+="\"image_tag\":\"$image_tag\","
|
||
containers_json+="\"image_repository\":\"$image_repository\","
|
||
containers_json+="\"image_source\":\"$image_source\","
|
||
containers_json+="\"image_id\":\"$full_image_id\","
|
||
containers_json+="\"status\":\"$normalized_status\","
|
||
containers_json+="\"state\":\"$state\","
|
||
containers_json+="\"ports\":$ports_json"
|
||
|
||
# Only add created_at if we have a valid date
|
||
if [[ "$created_iso" != "null" ]]; then
|
||
containers_json+=",\"created_at\":\"$created_iso\""
|
||
fi
|
||
|
||
# Only add started_at if we have a valid date
|
||
if [[ "$started_iso" != "null" ]]; then
|
||
containers_json+=",\"started_at\":\"$started_iso\""
|
||
fi
|
||
|
||
containers_json+="}"
|
||
|
||
done < <(docker ps -a --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.State}}|{{.CreatedAt}}|{{.RunningFor}}|{{.Ports}}' 2>/dev/null)
|
||
|
||
containers_json+="]"
|
||
|
||
echo "$containers_json"
|
||
}
|
||
|
||
# Collect Docker images
|
||
collect_images() {
|
||
info "Collecting Docker image information..."
|
||
|
||
local images_json="["
|
||
local first=true
|
||
|
||
while IFS='|' read -r repository tag image_id created size digest; do
|
||
if [[ -z "$repository" || "$repository" == "<none>" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# Clean up tag
|
||
if [[ -z "$tag" || "$tag" == "<none>" ]]; then
|
||
tag="latest"
|
||
fi
|
||
|
||
# Clean image ID
|
||
image_id="${image_id#sha256:}"
|
||
|
||
# Determine source
|
||
local source="docker-hub"
|
||
if [[ "$repository" == ghcr.io/* ]]; then
|
||
source="github"
|
||
elif [[ "$repository" == registry.gitlab.com/* ]]; then
|
||
source="gitlab"
|
||
elif [[ "$repository" == *"/"*"/"* ]]; then
|
||
source="private"
|
||
fi
|
||
|
||
# Convert size to bytes (approximate)
|
||
local size_bytes=0
|
||
if [[ "$size" =~ ([0-9.]+)([KMGT]?B) ]]; then
|
||
local num="${BASH_REMATCH[1]}"
|
||
local unit="${BASH_REMATCH[2]}"
|
||
case "$unit" in
|
||
KB) size_bytes=$(echo "$num * 1024" | bc | cut -d. -f1) ;;
|
||
MB) size_bytes=$(echo "$num * 1024 * 1024" | bc | cut -d. -f1) ;;
|
||
GB) size_bytes=$(echo "$num * 1024 * 1024 * 1024" | bc | cut -d. -f1) ;;
|
||
TB) size_bytes=$(echo "$num * 1024 * 1024 * 1024 * 1024" | bc | cut -d. -f1) ;;
|
||
B) size_bytes=$(echo "$num" | cut -d. -f1) ;;
|
||
esac
|
||
fi
|
||
|
||
# Convert created date to ISO 8601
|
||
# If date conversion fails, use null instead of invalid date string
|
||
local created_iso=$(date -d "$created" -Iseconds 2>/dev/null || echo "null")
|
||
|
||
# Add comma for JSON array
|
||
if [[ "$first" == false ]]; then
|
||
images_json+=","
|
||
fi
|
||
first=false
|
||
|
||
# Build JSON object for this image
|
||
images_json+="{\"repository\":\"$repository\","
|
||
images_json+="\"tag\":\"$tag\","
|
||
images_json+="\"image_id\":\"$image_id\","
|
||
images_json+="\"source\":\"$source\","
|
||
images_json+="\"size_bytes\":$size_bytes"
|
||
|
||
# Only add created_at if we have a valid date
|
||
if [[ "$created_iso" != "null" ]]; then
|
||
images_json+=",\"created_at\":\"$created_iso\""
|
||
fi
|
||
|
||
# Only add digest if present
|
||
if [[ -n "$digest" && "$digest" != "<none>" ]]; then
|
||
images_json+=",\"digest\":\"$digest\""
|
||
fi
|
||
|
||
images_json+="}"
|
||
|
||
done < <(docker images --format '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.CreatedAt}}|{{.Size}}|{{.Digest}}' --no-trunc 2>/dev/null)
|
||
|
||
images_json+="]"
|
||
|
||
echo "$images_json"
|
||
}
|
||
|
||
# Check for image updates
|
||
check_image_updates() {
|
||
info "Checking for image updates..."
|
||
|
||
local updates_json="["
|
||
local first=true
|
||
local update_count=0
|
||
|
||
# Get all images
|
||
while IFS='|' read -r repository tag image_id digest; do
|
||
if [[ -z "$repository" || "$repository" == "<none>" || "$tag" == "<none>" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# Skip checking 'latest' tag as it's always considered current by name
|
||
# We'll still check digest though
|
||
local full_image="${repository}:${tag}"
|
||
|
||
# Try to get remote digest from registry
|
||
# Use docker manifest inspect to avoid pulling the image
|
||
local remote_digest=$(docker manifest inspect "$full_image" 2>/dev/null | jq -r '.config.digest // .manifests[0].digest // empty' 2>/dev/null)
|
||
|
||
if [[ -z "$remote_digest" ]]; then
|
||
# If manifest inspect fails, try buildx imagetools inspect (works for more registries)
|
||
remote_digest=$(docker buildx imagetools inspect "$full_image" 2>/dev/null | grep -oP 'Digest:\s*\K\S+' | head -1)
|
||
fi
|
||
|
||
# Clean up digests for comparison
|
||
local local_digest="${digest#sha256:}"
|
||
remote_digest="${remote_digest#sha256:}"
|
||
|
||
# If we got a remote digest and it's different from local, there's an update
|
||
if [[ -n "$remote_digest" && -n "$local_digest" && "$remote_digest" != "$local_digest" ]]; then
|
||
if [[ "$first" == false ]]; then
|
||
updates_json+=","
|
||
fi
|
||
first=false
|
||
|
||
# Build update JSON object
|
||
updates_json+="{\"repository\":\"$repository\","
|
||
updates_json+="\"current_tag\":\"$tag\","
|
||
updates_json+="\"available_tag\":\"$tag\","
|
||
updates_json+="\"current_digest\":\"$local_digest\","
|
||
updates_json+="\"available_digest\":\"$remote_digest\","
|
||
updates_json+="\"image_id\":\"${image_id#sha256:}\""
|
||
updates_json+="}"
|
||
|
||
((update_count++))
|
||
fi
|
||
|
||
done < <(docker images --format '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.Digest}}' --no-trunc 2>/dev/null)
|
||
|
||
updates_json+="]"
|
||
|
||
info "Found $update_count image update(s) available"
|
||
|
||
echo "$updates_json"
|
||
}
|
||
|
||
# Send Docker data to server
|
||
send_docker_data() {
|
||
load_credentials
|
||
|
||
info "Collecting Docker data..."
|
||
|
||
local containers=$(collect_containers)
|
||
local images=$(collect_images)
|
||
local updates=$(check_image_updates)
|
||
|
||
# Count collected items
|
||
local container_count=$(echo "$containers" | jq '. | length' 2>/dev/null || echo "0")
|
||
local image_count=$(echo "$images" | jq '. | length' 2>/dev/null || echo "0")
|
||
local update_count=$(echo "$updates" | jq '. | length' 2>/dev/null || echo "0")
|
||
|
||
info "Found $container_count containers, $image_count images, and $update_count update(s) available"
|
||
|
||
# Build payload
|
||
local payload="{\"apiId\":\"$API_ID\",\"apiKey\":\"$API_KEY\",\"containers\":$containers,\"images\":$images,\"updates\":$updates}"
|
||
|
||
# Send to server
|
||
info "Sending Docker data to PatchMon server..."
|
||
|
||
local response=$(curl $CURL_FLAGS -s -w "\n%{http_code}" -X POST \
|
||
-H "Content-Type: application/json" \
|
||
-d "$payload" \
|
||
"${PATCHMON_SERVER}/api/${API_VERSION}/docker/collect" 2>&1)
|
||
|
||
local http_code=$(echo "$response" | tail -n1)
|
||
local response_body=$(echo "$response" | head -n-1)
|
||
|
||
if [[ "$http_code" == "200" ]]; then
|
||
success "Docker data sent successfully!"
|
||
log "Docker data sent: $container_count containers, $image_count images"
|
||
return 0
|
||
else
|
||
error "Failed to send Docker data. HTTP Status: $http_code\nResponse: $response_body"
|
||
fi
|
||
}
|
||
|
||
# Test Docker data collection without sending
|
||
test_collection() {
|
||
check_docker
|
||
|
||
info "Testing Docker data collection (dry run)..."
|
||
echo ""
|
||
|
||
local containers=$(collect_containers)
|
||
local images=$(collect_images)
|
||
local updates=$(check_image_updates)
|
||
|
||
local container_count=$(echo "$containers" | jq '. | length' 2>/dev/null || echo "0")
|
||
local image_count=$(echo "$images" | jq '. | length' 2>/dev/null || echo "0")
|
||
local update_count=$(echo "$updates" | jq '. | length' 2>/dev/null || echo "0")
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "${GREEN}Docker Data Collection Results${NC}"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo -e "Containers found: ${GREEN}$container_count${NC}"
|
||
echo -e "Images found: ${GREEN}$image_count${NC}"
|
||
echo -e "Updates available: ${YELLOW}$update_count${NC}"
|
||
echo ""
|
||
|
||
if command -v jq &> /dev/null; then
|
||
echo "━━━ Containers ━━━"
|
||
echo "$containers" | jq -r '.[] | "\(.name) (\(.status)) - \(.image_name):\(.image_tag)"' | head -10
|
||
if [[ $container_count -gt 10 ]]; then
|
||
echo "... and $((container_count - 10)) more"
|
||
fi
|
||
echo ""
|
||
echo "━━━ Images ━━━"
|
||
echo "$images" | jq -r '.[] | "\(.repository):\(.tag) (\(.size_bytes / 1024 / 1024 | floor)MB)"' | head -10
|
||
if [[ $image_count -gt 10 ]]; then
|
||
echo "... and $((image_count - 10)) more"
|
||
fi
|
||
|
||
if [[ $update_count -gt 0 ]]; then
|
||
echo ""
|
||
echo "━━━ Available Updates ━━━"
|
||
echo "$updates" | jq -r '.[] | "\(.repository):\(.current_tag) → \(.available_tag)"'
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
success "Test collection completed successfully!"
|
||
}
|
||
|
||
# Show help
|
||
show_help() {
|
||
cat << EOF
|
||
PatchMon Docker Agent v${AGENT_VERSION}
|
||
|
||
This agent collects Docker container and image information and sends it to PatchMon.
|
||
|
||
USAGE:
|
||
$0 <command>
|
||
|
||
COMMANDS:
|
||
collect Collect and send Docker data to PatchMon server
|
||
test Test Docker data collection without sending (dry run)
|
||
help Show this help message
|
||
|
||
REQUIREMENTS:
|
||
- Docker must be installed and running
|
||
- Main PatchMon agent must be configured first
|
||
- Credentials file must exist at $CREDENTIALS_FILE
|
||
|
||
EXAMPLES:
|
||
# Test collection (dry run)
|
||
sudo $0 test
|
||
|
||
# Collect and send Docker data
|
||
sudo $0 collect
|
||
|
||
SCHEDULING:
|
||
To run this agent automatically, add a cron job:
|
||
|
||
# Run every 5 minutes
|
||
*/5 * * * * /usr/local/bin/patchmon-docker-agent.sh collect
|
||
|
||
# Run every hour
|
||
0 * * * * /usr/local/bin/patchmon-docker-agent.sh collect
|
||
|
||
FILES:
|
||
Config: $CONFIG_FILE
|
||
Credentials: $CREDENTIALS_FILE
|
||
Log: $LOG_FILE
|
||
|
||
EOF
|
||
}
|
||
|
||
# Main function
|
||
main() {
|
||
case "$1" in
|
||
"collect")
|
||
check_docker
|
||
load_config
|
||
send_docker_data
|
||
;;
|
||
"test")
|
||
check_docker
|
||
load_config
|
||
test_collection
|
||
;;
|
||
"help"|"--help"|"-h"|"")
|
||
show_help
|
||
;;
|
||
*)
|
||
error "Unknown command: $1\n\nRun '$0 help' for usage information."
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Run main function
|
||
main "$@"
|
||
|