mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Refactored code to remove duplicate backend api endpoints for counting Improved connection persistence issues Improved database connection pooling issues Fixed redis connection efficiency Changed version to 1.3.0 Fixed GO binary detection based on package manager rather than OS
497 lines
16 KiB
Bash
Executable File
497 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# PatchMon Docker Agent Script v1.3.0
|
||
# 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.3.0"
|
||
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 "$@"
|
||
|