Add full Oculog codebase
This commit is contained in:
35
clients/ubuntu/build-deb.sh
Executable file
35
clients/ubuntu/build-deb.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}Building Oculog Client Debian Package${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check if dpkg-buildpackage is available
|
||||
if ! command -v dpkg-buildpackage &> /dev/null; then
|
||||
echo -e "${YELLOW}dpkg-buildpackage not found. Installing build dependencies...${NC}"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y devscripts build-essential debhelper
|
||||
fi
|
||||
|
||||
# Clean previous builds
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf debian/oculog-client
|
||||
rm -f ../oculog-client_*.deb
|
||||
rm -f ../oculog-client_*.changes
|
||||
rm -f ../oculog-client_*.buildinfo
|
||||
|
||||
# Build the package
|
||||
echo "Building package..."
|
||||
dpkg-buildpackage -b -us -uc
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Package built successfully!${NC}"
|
||||
echo "Package location: ../oculog-client_*.deb"
|
||||
|
||||
47
clients/ubuntu/check-client-version.sh
Executable file
47
clients/ubuntu/check-client-version.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Quick script to check client version status
|
||||
|
||||
echo "=== Client Version Diagnostic ==="
|
||||
echo ""
|
||||
|
||||
# 1. Check what version is in the file
|
||||
echo "1. Version in client.py file:"
|
||||
if sudo grep -q 'CLIENT_VERSION_BUILD_TIMESTAMP = "' /opt/oculog/client.py 2>/dev/null; then
|
||||
VERSION=$(sudo grep 'CLIENT_VERSION_BUILD_TIMESTAMP = "' /opt/oculog/client.py | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo " ✓ Injected version: $VERSION"
|
||||
else
|
||||
echo " ⚠ No injected version found (using file modification time)"
|
||||
MTIME=$(sudo stat -c %y /opt/oculog/client.py 2>/dev/null | cut -d' ' -f1,2 | sed 's/ /T/' | cut -d'.' -f1 | sed 's/T/-/' | sed 's/:/-/g' | cut -d'-' -f1-5)
|
||||
echo " File modification time: $MTIME"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 2. Check what version the client is reporting
|
||||
echo "2. Version reported by running client:"
|
||||
VERSION_IN_LOG=$(sudo journalctl -u oculog-client.service -n 100 2>/dev/null | grep -i "version:" | tail -1 | sed 's/.*version: \([^,]*\).*/\1/')
|
||||
if [ -n "$VERSION_IN_LOG" ]; then
|
||||
echo " ✓ Client reports: $VERSION_IN_LOG"
|
||||
else
|
||||
echo " ⚠ No version found in recent logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 3. Check recent metrics sends
|
||||
echo "3. Recent metrics activity:"
|
||||
RECENT_SENDS=$(sudo journalctl -u oculog-client.service -n 20 2>/dev/null | grep -i "metrics sent successfully" | wc -l)
|
||||
if [ "$RECENT_SENDS" -gt 0 ]; then
|
||||
echo " ✓ Metrics are being sent successfully"
|
||||
echo " Last successful send:"
|
||||
sudo journalctl -u oculog-client.service -n 20 2>/dev/null | grep -i "metrics sent successfully" | tail -1
|
||||
else
|
||||
echo " ⚠ No successful metric sends in recent logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Diagnostic Complete ==="
|
||||
echo ""
|
||||
echo "If version injection failed, the client will use file modification time."
|
||||
echo "To force re-injection, run the update script again."
|
||||
|
||||
7
clients/ubuntu/client.conf.example
Normal file
7
clients/ubuntu/client.conf.example
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"server_url": "http://your-server-ip:3001",
|
||||
"server_id": "ubuntu-server-01",
|
||||
"api_key": "your-api-key-here",
|
||||
"interval": 30
|
||||
}
|
||||
|
||||
902
clients/ubuntu/client.py
Executable file
902
clients/ubuntu/client.py
Executable file
@@ -0,0 +1,902 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Oculog Client Agent
|
||||
Collects system metrics and sends them to the server
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import socket
|
||||
import requests
|
||||
import psutil
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Configuration
|
||||
CONFIG_FILE = '/etc/oculog/client.conf'
|
||||
LOG_FILE = '/var/log/oculog/client.log'
|
||||
PID_FILE = '/var/run/oculog-client.pid'
|
||||
CLIENT_SCRIPT_PATH = '/opt/oculog/client.py'
|
||||
|
||||
# Client version - build timestamp in format year-month-day-hour-minute
|
||||
# This will be injected by the server when serving the script
|
||||
# Format: CLIENT_VERSION_BUILD_TIMESTAMP = "YYYY-MM-DD-HH-MM"
|
||||
# If not injected, will use file modification time
|
||||
CLIENT_VERSION_BUILD_TIMESTAMP = None # Will be injected by server
|
||||
|
||||
def get_client_version():
|
||||
"""Get client version from build timestamp or file modification time"""
|
||||
# First check if build timestamp was injected
|
||||
if CLIENT_VERSION_BUILD_TIMESTAMP:
|
||||
return CLIENT_VERSION_BUILD_TIMESTAMP
|
||||
|
||||
# Fallback to file modification time
|
||||
try:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
script_path = os.path.join(script_dir, 'client.py')
|
||||
if not os.path.exists(script_path):
|
||||
script_path = CLIENT_SCRIPT_PATH
|
||||
|
||||
if os.path.exists(script_path):
|
||||
mtime = os.path.getmtime(script_path)
|
||||
dt = datetime.fromtimestamp(mtime)
|
||||
return dt.strftime('%Y-%m-%d-%H-%M')
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not determine client version: {e}")
|
||||
|
||||
# Final fallback: use current time (for new installations)
|
||||
return datetime.now().strftime('%Y-%m-%d-%H-%M')
|
||||
|
||||
CLIENT_VERSION = get_client_version()
|
||||
|
||||
# Ensure log directory exists
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_FILE),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('oculog-client')
|
||||
|
||||
|
||||
class VulnerabilityScanner:
|
||||
"""Scans for vulnerabilities in installed packages"""
|
||||
|
||||
def __init__(self, enabled=True):
|
||||
self.enabled = enabled
|
||||
self.last_scan_time = 0
|
||||
self.scan_interval = 86400 # 1 day in seconds
|
||||
|
||||
def get_installed_packages(self):
|
||||
"""Get list of all installed packages (excluding removed packages)"""
|
||||
try:
|
||||
# Use dpkg-query with status to filter out removed packages
|
||||
# Status format: wantok installed, config-files, half-configured, etc.
|
||||
# We only want packages with "install ok installed" status
|
||||
result = subprocess.run(
|
||||
['dpkg-query', '-W', '-f=${Status}\t${Package}\t${Version}\n'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"dpkg-query failed: {result.stderr}")
|
||||
return []
|
||||
|
||||
packages = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 3:
|
||||
status, name, version = parts[0], parts[1], parts[2]
|
||||
# Only include packages that are actually installed
|
||||
# Status format: "install ok installed" means fully installed
|
||||
# "deinstall ok config-files" (rc) means removed but config remains - exclude these
|
||||
if 'install ok installed' in status:
|
||||
packages.append({
|
||||
'name': name.strip(),
|
||||
'version': version.strip()
|
||||
})
|
||||
|
||||
return packages
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("dpkg-query timed out")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting installed packages: {e}")
|
||||
return []
|
||||
|
||||
def should_scan(self):
|
||||
"""Check if it's time to run a vulnerability scan"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
current_time = time.time()
|
||||
return (current_time - self.last_scan_time) >= self.scan_interval
|
||||
|
||||
def get_os_version(self):
|
||||
"""Get Ubuntu version from /etc/os-release"""
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith('VERSION_ID='):
|
||||
# Extract version, remove quotes
|
||||
version = line.split('=', 1)[1].strip('"').strip("'")
|
||||
return version
|
||||
elif line.startswith('VERSION='):
|
||||
# Fallback: try to extract version from VERSION field
|
||||
version_str = line.split('=', 1)[1].strip('"').strip("'")
|
||||
# Try to extract version number (e.g., "22.04" from "22.04 LTS")
|
||||
import re
|
||||
match = re.search(r'(\d+\.\d+)', version_str)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading OS version: {e}")
|
||||
return None
|
||||
|
||||
def scan(self):
|
||||
"""Perform vulnerability scan"""
|
||||
try:
|
||||
logger.info("Starting vulnerability scan...")
|
||||
packages = self.get_installed_packages()
|
||||
|
||||
if not packages:
|
||||
logger.warning("No packages found to scan")
|
||||
return False
|
||||
|
||||
logger.info(f"Scanning {len(packages)} packages for vulnerabilities")
|
||||
self.last_scan_time = time.time()
|
||||
|
||||
# Return packages with OS version info
|
||||
return {
|
||||
'packages': packages,
|
||||
'os_version': self.get_os_version()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error during vulnerability scan: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class MetricsCollector:
|
||||
"""Collects system metrics"""
|
||||
|
||||
def __init__(self):
|
||||
self.hostname = socket.gethostname()
|
||||
self.last_network_stats = None
|
||||
self.last_network_time = None
|
||||
# Cache for rarely-changing data
|
||||
self.cached_os_release = None
|
||||
self.cached_public_ip = None
|
||||
self.cached_public_ip_time = 0
|
||||
self.public_ip_cache_duration = 3600 # Cache public IP for 1 hour
|
||||
self.last_docker_check = 0
|
||||
self.docker_check_interval = 300 # Check docker every 5 minutes
|
||||
self.cached_docker_available = None
|
||||
self.cached_containers = None
|
||||
|
||||
def get_cpu_metrics(self):
|
||||
"""Get CPU usage percentage"""
|
||||
try:
|
||||
# Use interval=None to avoid blocking - first call returns 0.0,
|
||||
# but since client runs continuously, subsequent calls will be accurate
|
||||
cpu_percent = psutil.cpu_percent(interval=None)
|
||||
cpu_count = psutil.cpu_count()
|
||||
return {
|
||||
'usage': round(cpu_percent, 2),
|
||||
'cores': cpu_count
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting CPU metrics: {e}")
|
||||
return {'usage': 0, 'cores': 0}
|
||||
|
||||
def get_memory_metrics(self):
|
||||
"""Get memory usage"""
|
||||
try:
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
'total': round(mem.total / (1024**3), 2), # GB
|
||||
'used': round(mem.used / (1024**3), 2), # GB
|
||||
'available': round(mem.available / (1024**3), 2), # GB
|
||||
'percent': round(mem.percent, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting memory metrics: {e}")
|
||||
return {'total': 0, 'used': 0, 'available': 0, 'percent': 0}
|
||||
|
||||
def get_swap_metrics(self):
|
||||
"""Get swap usage"""
|
||||
try:
|
||||
swap = psutil.swap_memory()
|
||||
return {
|
||||
'total': round(swap.total / (1024**3), 2), # GB
|
||||
'used': round(swap.used / (1024**3), 2), # GB
|
||||
'free': round(swap.free / (1024**3), 2), # GB
|
||||
'percent': round(swap.percent, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting swap metrics: {e}")
|
||||
return {'total': 0, 'used': 0, 'free': 0, 'percent': 0}
|
||||
|
||||
def get_process_count(self):
|
||||
"""Get total process count"""
|
||||
try:
|
||||
# More efficient: use process_iter with a counter instead of creating full list
|
||||
count = 0
|
||||
for _ in psutil.process_iter():
|
||||
count += 1
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting process count: {e}")
|
||||
return 0
|
||||
|
||||
def get_disk_metrics(self):
|
||||
"""Get disk usage for all mounted filesystems"""
|
||||
try:
|
||||
disks = []
|
||||
partitions = psutil.disk_partitions()
|
||||
|
||||
for partition in partitions:
|
||||
try:
|
||||
# Skip virtual filesystems and network mounts
|
||||
if partition.fstype in ['tmpfs', 'devtmpfs', 'sysfs', 'proc', 'devpts', 'cgroup', 'cgroup2', 'pstore', 'bpf', 'tracefs', 'debugfs', 'securityfs', 'hugetlbfs', 'mqueue', 'overlay', 'autofs', 'squashfs']:
|
||||
continue
|
||||
|
||||
# Skip network filesystems (optional - comment out if you want to include them)
|
||||
if partition.fstype.startswith('nfs') or partition.fstype.startswith('cifs') or partition.fstype.startswith('smb'):
|
||||
continue
|
||||
|
||||
# Skip loop devices (snap packages, etc.)
|
||||
if partition.device.startswith('/dev/loop'):
|
||||
continue
|
||||
|
||||
# Skip snap mount points
|
||||
if partition.mountpoint.startswith('/snap/'):
|
||||
continue
|
||||
|
||||
disk_usage = psutil.disk_usage(partition.mountpoint)
|
||||
disk_info = {
|
||||
'mountpoint': partition.mountpoint,
|
||||
'device': partition.device,
|
||||
'fstype': partition.fstype,
|
||||
'total': round(disk_usage.total / (1024**3), 2), # GB
|
||||
'used': round(disk_usage.used / (1024**3), 2), # GB
|
||||
'free': round(disk_usage.free / (1024**3), 2), # GB
|
||||
'percent': round(disk_usage.percent, 2)
|
||||
}
|
||||
disks.append(disk_info)
|
||||
logger.debug(f"Collected disk metrics: {partition.mountpoint} ({partition.device}) - {disk_info['used']:.2f}GB / {disk_info['total']:.2f}GB ({disk_info['percent']:.1f}%)")
|
||||
except PermissionError:
|
||||
# Skip partitions we don't have permission to access
|
||||
logger.debug(f"Skipping {partition.mountpoint} due to permission error")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting disk usage for {partition.mountpoint}: {e}")
|
||||
continue
|
||||
|
||||
# Return root disk for backward compatibility, plus all disks
|
||||
root_disk = next((d for d in disks if d['mountpoint'] == '/'), disks[0] if disks else None)
|
||||
|
||||
logger.info(f"Collected metrics for {len(disks)} disk(s): {[d['mountpoint'] for d in disks]}")
|
||||
|
||||
if root_disk:
|
||||
return {
|
||||
'total': root_disk['total'],
|
||||
'used': root_disk['used'],
|
||||
'free': root_disk['free'],
|
||||
'percent': root_disk['percent'],
|
||||
'disks': disks # Include all disks
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'free': 0,
|
||||
'percent': 0,
|
||||
'disks': disks
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting disk metrics: {e}")
|
||||
return {'total': 0, 'used': 0, 'free': 0, 'percent': 0, 'disks': []}
|
||||
|
||||
def get_network_metrics(self):
|
||||
"""Get network throughput"""
|
||||
try:
|
||||
net_io = psutil.net_io_counters()
|
||||
current_time = time.time()
|
||||
|
||||
if self.last_network_stats is None:
|
||||
self.last_network_stats = net_io
|
||||
self.last_network_time = current_time
|
||||
return {'rx': 0, 'tx': 0, 'rx_total': 0, 'tx_total': 0}
|
||||
|
||||
time_delta = current_time - self.last_network_time
|
||||
if time_delta == 0:
|
||||
return {'rx': 0, 'tx': 0, 'rx_total': 0, 'tx_total': 0}
|
||||
|
||||
rx_bytes = net_io.bytes_recv - self.last_network_stats.bytes_recv
|
||||
tx_bytes = net_io.bytes_sent - self.last_network_stats.bytes_sent
|
||||
|
||||
rx_mbps = (rx_bytes * 8) / (time_delta * 1024 * 1024) # Mbps
|
||||
tx_mbps = (tx_bytes * 8) / (time_delta * 1024 * 1024) # Mbps
|
||||
|
||||
self.last_network_stats = net_io
|
||||
self.last_network_time = current_time
|
||||
|
||||
return {
|
||||
'rx': round(rx_mbps, 2), # Mbps
|
||||
'tx': round(tx_mbps, 2), # Mbps
|
||||
'rx_total': round(net_io.bytes_recv / (1024**3), 2), # GB
|
||||
'tx_total': round(net_io.bytes_sent / (1024**3), 2) # GB
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error collecting network metrics: {e}")
|
||||
return {'rx': 0, 'tx': 0, 'rx_total': 0, 'tx_total': 0}
|
||||
|
||||
def get_server_info(self):
|
||||
"""Get server information: OS release, status, processes, containers, IP info"""
|
||||
server_info = {
|
||||
'os_release': None,
|
||||
'live_status': None,
|
||||
'top_processes': None,
|
||||
'containers': None,
|
||||
'ip_info': None
|
||||
}
|
||||
|
||||
# Get OS release info (cached since it rarely changes)
|
||||
if self.cached_os_release is None:
|
||||
try:
|
||||
os_release = {}
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if '=' in line and not line.startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
# Remove quotes from value
|
||||
value = value.strip('"').strip("'")
|
||||
os_release[key] = value
|
||||
self.cached_os_release = os_release if os_release else None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading /etc/os-release: {e}")
|
||||
self.cached_os_release = None
|
||||
server_info['os_release'] = self.cached_os_release
|
||||
|
||||
# Get live status (uptime, load average)
|
||||
try:
|
||||
uptime_seconds = time.time() - psutil.boot_time()
|
||||
uptime_days = int(uptime_seconds // 86400)
|
||||
uptime_hours = int((uptime_seconds % 86400) // 3600)
|
||||
uptime_minutes = int((uptime_seconds % 3600) // 60)
|
||||
|
||||
load_avg = os.getloadavg()
|
||||
|
||||
server_info['live_status'] = {
|
||||
'uptime_days': uptime_days,
|
||||
'uptime_hours': uptime_hours,
|
||||
'uptime_minutes': uptime_minutes,
|
||||
'uptime_seconds': round(uptime_seconds, 2),
|
||||
'load_average_1min': round(load_avg[0], 2),
|
||||
'load_average_5min': round(load_avg[1], 2),
|
||||
'load_average_15min': round(load_avg[2], 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting live status: {e}")
|
||||
|
||||
# Get top processes by CPU usage
|
||||
try:
|
||||
# Collect CPU usage for all processes, then sort to get actual top processes
|
||||
# This ensures we find the highest CPU-consuming processes regardless of process order
|
||||
processes = []
|
||||
|
||||
for proc in psutil.process_iter(['pid', 'name', 'memory_percent', 'username']):
|
||||
try:
|
||||
# Get CPU percent - first call may return 0.0, but that's acceptable
|
||||
# The client runs continuously so subsequent calls will have accurate values
|
||||
cpu_pct = proc.cpu_percent(interval=None)
|
||||
if cpu_pct is None:
|
||||
cpu_pct = 0.0
|
||||
|
||||
proc_info = proc.info
|
||||
proc_info['cpu_percent'] = round(cpu_pct, 2)
|
||||
processes.append(proc_info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
continue
|
||||
|
||||
# Sort by CPU usage and take top 10
|
||||
processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True)
|
||||
server_info['top_processes'] = processes[:10]
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting top processes: {e}")
|
||||
|
||||
# Get running containers (Docker) - check less frequently
|
||||
current_time = time.time()
|
||||
if current_time - self.last_docker_check >= self.docker_check_interval:
|
||||
self.last_docker_check = current_time
|
||||
try:
|
||||
# First check if docker is available (cached)
|
||||
if self.cached_docker_available is None:
|
||||
# Check if docker command exists
|
||||
docker_check = subprocess.run(
|
||||
['which', 'docker'],
|
||||
capture_output=True,
|
||||
timeout=1
|
||||
)
|
||||
self.cached_docker_available = docker_check.returncode == 0
|
||||
|
||||
if self.cached_docker_available:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--format', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
containers = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
try:
|
||||
container = json.loads(line)
|
||||
containers.append({
|
||||
'id': container.get('ID', '')[:12],
|
||||
'image': container.get('Image', ''),
|
||||
'command': container.get('Command', ''),
|
||||
'created': container.get('CreatedAt', ''),
|
||||
'status': container.get('Status', ''),
|
||||
'ports': container.get('Ports', ''),
|
||||
'names': container.get('Names', '')
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
self.cached_containers = containers if containers else None
|
||||
else:
|
||||
self.cached_containers = None
|
||||
else:
|
||||
self.cached_containers = None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
|
||||
# Docker might not be installed or not running
|
||||
self.cached_docker_available = False
|
||||
self.cached_containers = None
|
||||
|
||||
# Use cached containers value
|
||||
server_info['containers'] = self.cached_containers
|
||||
|
||||
# Get IP info (private and public)
|
||||
try:
|
||||
ip_info = {
|
||||
'private': [],
|
||||
'public': None
|
||||
}
|
||||
|
||||
# Get private IPs from network interfaces
|
||||
for interface, addrs in psutil.net_if_addrs().items():
|
||||
for addr in addrs:
|
||||
if addr.family == socket.AF_INET: # IPv4
|
||||
ip_addr = addr.address
|
||||
# Skip loopback and link-local
|
||||
if not ip_addr.startswith('127.') and not ip_addr.startswith('169.254.'):
|
||||
ip_info['private'].append({
|
||||
'interface': interface,
|
||||
'ip': ip_addr,
|
||||
'netmask': addr.netmask
|
||||
})
|
||||
|
||||
# Try to get public IP (cached since it rarely changes)
|
||||
current_time = time.time()
|
||||
if self.cached_public_ip is None or (current_time - self.cached_public_ip_time) >= self.public_ip_cache_duration:
|
||||
try:
|
||||
# Try multiple services for reliability
|
||||
public_ip_services = [
|
||||
'https://api.ipify.org?format=json',
|
||||
'https://ifconfig.me/ip',
|
||||
'https://icanhazip.com'
|
||||
]
|
||||
|
||||
for service_url in public_ip_services:
|
||||
try:
|
||||
response = requests.get(service_url, timeout=3)
|
||||
if response.status_code == 200:
|
||||
# Handle different response formats
|
||||
if 'ipify' in service_url:
|
||||
public_ip = response.json().get('ip', '').strip()
|
||||
else:
|
||||
public_ip = response.text.strip()
|
||||
|
||||
if public_ip:
|
||||
self.cached_public_ip = public_ip
|
||||
self.cached_public_ip_time = current_time
|
||||
ip_info['public'] = public_ip
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch public IP: {e}")
|
||||
else:
|
||||
# Use cached public IP
|
||||
ip_info['public'] = self.cached_public_ip
|
||||
|
||||
server_info['ip_info'] = ip_info if ip_info['private'] or ip_info['public'] else None
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting IP info: {e}")
|
||||
|
||||
return server_info
|
||||
|
||||
def collect_all_metrics(self):
|
||||
"""Collect all system metrics"""
|
||||
server_info = self.get_server_info()
|
||||
|
||||
# Extract load average and uptime from server_info for metrics payload
|
||||
load_avg = None
|
||||
uptime_seconds = None
|
||||
if server_info.get('live_status'):
|
||||
live_status = server_info['live_status']
|
||||
load_avg = {
|
||||
'1min': live_status.get('load_average_1min'),
|
||||
'5min': live_status.get('load_average_5min'),
|
||||
'15min': live_status.get('load_average_15min')
|
||||
}
|
||||
uptime_seconds = live_status.get('uptime_seconds')
|
||||
|
||||
return {
|
||||
'hostname': self.hostname,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'cpu': self.get_cpu_metrics(),
|
||||
'memory': self.get_memory_metrics(),
|
||||
'swap': self.get_swap_metrics(),
|
||||
'disk': self.get_disk_metrics(),
|
||||
'network': self.get_network_metrics(),
|
||||
'process_count': self.get_process_count(),
|
||||
'load_avg': load_avg,
|
||||
'uptime_seconds': uptime_seconds,
|
||||
'server_info': server_info,
|
||||
'client_version': CLIENT_VERSION
|
||||
}
|
||||
|
||||
|
||||
class OculogClient:
|
||||
"""Main client class"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.server_url = config.get('server_url', 'http://localhost:3001')
|
||||
self.server_id = config.get('server_id', socket.gethostname())
|
||||
self.api_key = config.get('api_key', '')
|
||||
self.interval = config.get('interval', 30) # seconds
|
||||
self.collector = MetricsCollector()
|
||||
# Vulnerability scanning is enabled by default, but can be disabled via config
|
||||
vulnerability_scan_enabled = config.get('vulnerability_scan_enabled', True)
|
||||
self.vulnerability_scanner = VulnerabilityScanner(enabled=vulnerability_scan_enabled)
|
||||
self.running = False
|
||||
self.last_update_check = 0
|
||||
self.update_check_interval = 3600 # Check for updates every hour
|
||||
|
||||
def send_metrics(self, metrics):
|
||||
"""Send metrics to server"""
|
||||
try:
|
||||
url = f"{self.server_url}/api/servers/{self.server_id}/metrics"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': self.api_key
|
||||
}
|
||||
|
||||
if not self.api_key:
|
||||
logger.warning("No API key configured. Metrics may be rejected by server.")
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json=metrics,
|
||||
timeout=10,
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.info(f"Metrics sent successfully: {response.json()}")
|
||||
return True
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to send metrics: {e}")
|
||||
return False
|
||||
|
||||
def send_vulnerability_scan(self, scan_data):
|
||||
"""Send vulnerability scan results to server in batches if needed"""
|
||||
try:
|
||||
url = f"{self.server_url}/api/servers/{self.server_id}/vulnerabilities/scan"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': self.api_key
|
||||
}
|
||||
|
||||
if not self.api_key:
|
||||
logger.warning("No API key configured. Vulnerability scan may be rejected by server.")
|
||||
return False
|
||||
|
||||
# Extract packages and os_version
|
||||
if isinstance(scan_data, dict) and 'packages' in scan_data:
|
||||
packages = scan_data['packages']
|
||||
os_version = scan_data.get('os_version')
|
||||
else:
|
||||
# Legacy format: just a list of packages
|
||||
packages = scan_data
|
||||
os_version = None
|
||||
|
||||
# Batch packages to avoid payload size limits (send 500 packages per batch)
|
||||
batch_size = 500
|
||||
total_packages = len(packages)
|
||||
total_vulnerabilities = 0
|
||||
success_count = 0
|
||||
|
||||
logger.info(f"Sending vulnerability scan for {total_packages} packages in batches of {batch_size}")
|
||||
|
||||
for i in range(0, total_packages, batch_size):
|
||||
batch = packages[i:i + batch_size]
|
||||
batch_num = (i // batch_size) + 1
|
||||
total_batches = (total_packages + batch_size - 1) // batch_size
|
||||
|
||||
payload = {
|
||||
'packages': batch,
|
||||
'os_version': os_version,
|
||||
'batch_info': {
|
||||
'batch_number': batch_num,
|
||||
'total_batches': total_batches,
|
||||
'is_last_batch': (i + batch_size >= total_packages)
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=120, # Longer timeout for vulnerability scans
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
batch_vulns = result.get('vulnerabilities_found', 0)
|
||||
total_vulnerabilities += batch_vulns
|
||||
success_count += 1
|
||||
logger.info(f"Batch {batch_num}/{total_batches} sent successfully: {len(batch)} packages, {batch_vulns} vulnerabilities found")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to send vulnerability scan batch {batch_num}/{total_batches}: {e}")
|
||||
# Continue with other batches even if one fails
|
||||
continue
|
||||
|
||||
if success_count > 0:
|
||||
logger.info(f"Vulnerability scan completed: {success_count}/{total_batches} batches successful, {total_vulnerabilities} total vulnerabilities found")
|
||||
return True
|
||||
else:
|
||||
logger.error("All vulnerability scan batches failed")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send vulnerability scan: {e}")
|
||||
return False
|
||||
|
||||
def check_for_updates(self):
|
||||
"""Check if a newer client version is available"""
|
||||
try:
|
||||
url = f"{self.server_url}/api/client-version/latest"
|
||||
headers = {
|
||||
'X-API-Key': self.api_key
|
||||
}
|
||||
|
||||
logger.debug(f"Checking for updates: current version={CLIENT_VERSION}, server={url}")
|
||||
response = requests.get(url, timeout=5, headers=headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
latest_version = data.get('version')
|
||||
|
||||
if not latest_version:
|
||||
logger.warning(f"Update check returned no version data from server")
|
||||
return False
|
||||
|
||||
logger.debug(f"Server reports latest version: {latest_version}")
|
||||
|
||||
# Compare versions (format: YYYY-MM-DD-HH-MM)
|
||||
# Simple string comparison works for this format
|
||||
if CLIENT_VERSION < latest_version:
|
||||
logger.info(f"Update available: current={CLIENT_VERSION}, latest={latest_version}")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"No update needed: current={CLIENT_VERSION}, latest={latest_version}")
|
||||
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Could not check for updates (network error): {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check for updates (unexpected error): {e}")
|
||||
return False
|
||||
|
||||
def perform_auto_update(self):
|
||||
"""Perform automatic update of the client"""
|
||||
try:
|
||||
logger.info("Starting automatic client update...")
|
||||
|
||||
# Download updated client script
|
||||
script_url = f"{self.server_url}/api/client-script"
|
||||
temp_script = '/opt/oculog/client.py.new'
|
||||
|
||||
response = requests.get(script_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Backup current script
|
||||
if os.path.exists(CLIENT_SCRIPT_PATH):
|
||||
backup_path = f"{CLIENT_SCRIPT_PATH}.backup"
|
||||
subprocess.run(['cp', CLIENT_SCRIPT_PATH, backup_path], check=False)
|
||||
|
||||
# Write new script
|
||||
with open(temp_script, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Make executable
|
||||
os.chmod(temp_script, 0o755)
|
||||
|
||||
# Replace old script atomically
|
||||
subprocess.run(['mv', temp_script, CLIENT_SCRIPT_PATH], check=True)
|
||||
|
||||
logger.info("Client script updated successfully")
|
||||
|
||||
# Trigger systemd restart by exiting (systemd will restart due to Restart=always)
|
||||
# We use exit code 0 to indicate successful update
|
||||
logger.info("Exiting to allow systemd to restart with new version")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Auto-update failed: {e}")
|
||||
# Try to restore backup if update failed
|
||||
backup_path = f"{CLIENT_SCRIPT_PATH}.backup"
|
||||
if os.path.exists(backup_path):
|
||||
try:
|
||||
subprocess.run(['cp', backup_path, CLIENT_SCRIPT_PATH], check=False)
|
||||
logger.info("Restored backup after failed update")
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
"""Main loop"""
|
||||
self.running = True
|
||||
logger.info(f"Starting Oculog client (server_id: {self.server_id}, version: {CLIENT_VERSION}, interval: {self.interval}s)")
|
||||
|
||||
# Initial network stats collection (needed for first calculation)
|
||||
self.collector.get_network_metrics()
|
||||
time.sleep(1)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# Check for updates periodically
|
||||
current_time = time.time()
|
||||
if current_time - self.last_update_check >= self.update_check_interval:
|
||||
logger.info(f"Checking for client updates (current version: {CLIENT_VERSION})...")
|
||||
self.last_update_check = current_time
|
||||
try:
|
||||
if self.check_for_updates():
|
||||
logger.info("Newer version detected, performing auto-update...")
|
||||
if self.perform_auto_update():
|
||||
logger.info("Auto-update completed, exiting to allow restart...")
|
||||
# Exit gracefully - systemd will restart with Restart=always
|
||||
self.running = False
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.warning("Auto-update failed, continuing with current version")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during update check: {e}")
|
||||
# Continue running even if update check fails
|
||||
|
||||
# Check for vulnerability scan (hourly)
|
||||
if self.vulnerability_scanner.should_scan():
|
||||
try:
|
||||
scan_data = self.vulnerability_scanner.scan()
|
||||
if scan_data:
|
||||
package_count = len(scan_data['packages']) if isinstance(scan_data, dict) else len(scan_data)
|
||||
logger.info(f"Sending vulnerability scan for {package_count} packages...")
|
||||
if self.send_vulnerability_scan(scan_data):
|
||||
logger.info("Vulnerability scan completed successfully")
|
||||
else:
|
||||
logger.warning("Vulnerability scan failed to send, will retry on next cycle")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during vulnerability scan: {e}")
|
||||
# Continue running even if vulnerability scan fails
|
||||
|
||||
metrics = self.collector.collect_all_metrics()
|
||||
logger.debug(f"Collected metrics: {json.dumps(metrics, indent=2)}")
|
||||
|
||||
if self.send_metrics(metrics):
|
||||
logger.info("Metrics collection cycle completed successfully")
|
||||
else:
|
||||
logger.warning("Failed to send metrics, will retry on next cycle")
|
||||
|
||||
time.sleep(self.interval)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Received interrupt signal, shutting down...")
|
||||
self.running = False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in main loop: {e}")
|
||||
time.sleep(self.interval)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the client"""
|
||||
self.running = False
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Load configuration from file"""
|
||||
default_config = {
|
||||
'server_url': 'http://localhost:3001',
|
||||
'server_id': socket.gethostname(),
|
||||
'interval': 30
|
||||
}
|
||||
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
config = json.load(f)
|
||||
default_config.update(config)
|
||||
logger.info(f"Loaded configuration from {CONFIG_FILE}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load config file, using defaults: {e}")
|
||||
else:
|
||||
logger.info(f"Config file not found at {CONFIG_FILE}, using defaults")
|
||||
|
||||
return default_config
|
||||
|
||||
|
||||
def write_pid_file():
|
||||
"""Write PID file"""
|
||||
try:
|
||||
pid_dir = os.path.dirname(PID_FILE)
|
||||
os.makedirs(pid_dir, exist_ok=True)
|
||||
with open(PID_FILE, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write PID file: {e}")
|
||||
|
||||
|
||||
def remove_pid_file():
|
||||
"""Remove PID file"""
|
||||
try:
|
||||
if os.path.exists(PID_FILE):
|
||||
os.remove(PID_FILE)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove PID file: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == '--version':
|
||||
print(f"Oculog Client version {CLIENT_VERSION}")
|
||||
sys.exit(0)
|
||||
elif sys.argv[1] == '--help':
|
||||
print("Usage: oculog-client [--version|--help]")
|
||||
print("\nOculog Client Agent")
|
||||
print("Collects system metrics and sends them to the server")
|
||||
sys.exit(0)
|
||||
|
||||
# Ensure log directory exists
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
config = load_config()
|
||||
client = OculogClient(config)
|
||||
|
||||
try:
|
||||
write_pid_file()
|
||||
client.run()
|
||||
finally:
|
||||
remove_pid_file()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
10
clients/ubuntu/debian/control
Normal file
10
clients/ubuntu/debian/control
Normal file
@@ -0,0 +1,10 @@
|
||||
Package: oculog-client
|
||||
Version: 1.0.0
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Depends: python3 (>= 3.6), python3-pip
|
||||
Maintainer: Ormentia <support@ormentia.com>
|
||||
Description: Oculog Client Agent for Server Metrics Collection
|
||||
Oculog client agent that collects system metrics (CPU, memory, disk, network)
|
||||
and sends them to the Oculog observability platform server.
|
||||
55
clients/ubuntu/debian/postinst
Executable file
55
clients/ubuntu/debian/postinst
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Post-installation script
|
||||
echo "Configuring Oculog client..."
|
||||
|
||||
# Create directories
|
||||
mkdir -p /var/log/oculog
|
||||
mkdir -p /etc/oculog
|
||||
|
||||
# Install Python dependencies using system-managed approach
|
||||
# Try to install via apt first (for newer Ubuntu versions)
|
||||
if apt-cache show python3-psutil >/dev/null 2>&1 && apt-cache show python3-requests >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
if apt-get install -y -qq python3-psutil python3-requests >/dev/null 2>&1; then
|
||||
PYTHON_BIN="/usr/bin/python3"
|
||||
else
|
||||
USE_VENV=1
|
||||
fi
|
||||
else
|
||||
USE_VENV=1
|
||||
fi
|
||||
|
||||
# If apt packages aren't available, use a virtual environment
|
||||
if [ "$USE_VENV" = "1" ]; then
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq python3-venv python3-pip >/dev/null 2>&1 || true
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv /opt/oculog/venv
|
||||
|
||||
# Install packages in virtual environment
|
||||
/opt/oculog/venv/bin/pip install --quiet --upgrade pip
|
||||
/opt/oculog/venv/bin/pip install --quiet psutil==5.9.6 requests==2.31.0
|
||||
|
||||
PYTHON_BIN="/opt/oculog/venv/bin/python3"
|
||||
|
||||
# Update shebang in client script
|
||||
sed -i "1s|.*|#!${PYTHON_BIN}|" /opt/oculog/client.py
|
||||
|
||||
# Update systemd service to use venv Python
|
||||
sed -i "s|ExecStart=.*|ExecStart=${PYTHON_BIN} /opt/oculog/client.py|" /etc/systemd/system/oculog-client.service
|
||||
fi
|
||||
|
||||
# Enable and start service if config exists
|
||||
if [ -f /etc/oculog/client.conf ]; then
|
||||
systemctl daemon-reload
|
||||
systemctl enable oculog-client.service 2>/dev/null || true
|
||||
systemctl start oculog-client.service 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Oculog client installed successfully!"
|
||||
echo "Please configure /etc/oculog/client.conf before starting the service."
|
||||
|
||||
exit 0
|
||||
26
clients/ubuntu/debian/postrm
Executable file
26
clients/ubuntu/debian/postrm
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Post-removal script
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
# Stop and disable service
|
||||
systemctl stop oculog-client.service 2>/dev/null || true
|
||||
systemctl disable oculog-client.service 2>/dev/null || true
|
||||
|
||||
# Remove service file
|
||||
rm -f /etc/systemd/system/oculog-client.service
|
||||
systemctl daemon-reload
|
||||
|
||||
# Remove symlink
|
||||
rm -f /usr/local/bin/oculog-client
|
||||
|
||||
if [ "$1" = "purge" ]; then
|
||||
# Remove configuration and logs (optional - commented out to preserve data)
|
||||
# rm -rf /etc/oculog
|
||||
# rm -rf /var/log/oculog
|
||||
# rm -rf /opt/oculog
|
||||
echo "Configuration and logs preserved at /etc/oculog and /var/log/oculog"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
16
clients/ubuntu/debian/rules
Executable file
16
clients/ubuntu/debian/rules
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
# Install client script
|
||||
install -d $(CURDIR)/debian/oculog-client/opt/oculog
|
||||
install -m 755 client.py $(CURDIR)/debian/oculog-client/opt/oculog/
|
||||
# Install systemd service
|
||||
install -d $(CURDIR)/debian/oculog-client/etc/systemd/system
|
||||
install -m 644 oculog-client.service $(CURDIR)/debian/oculog-client/etc/systemd/system/
|
||||
# Create symlink
|
||||
install -d $(CURDIR)/debian/oculog-client/usr/local/bin
|
||||
ln -sf /opt/oculog/client.py $(CURDIR)/debian/oculog-client/usr/local/bin/oculog-client
|
||||
189
clients/ubuntu/install.sh
Executable file
189
clients/ubuntu/install.sh
Executable file
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Oculog Client Installation${NC}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root (use sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Python 3 is installed
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo -e "${RED}Python 3 is not installed. Please install it first:${NC}"
|
||||
echo " sudo apt update && sudo apt install -y python3 python3-pip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get installation directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
INSTALL_DIR="/opt/oculog"
|
||||
|
||||
# Check if this is an upgrade
|
||||
IS_UPGRADE=false
|
||||
SERVICE_WAS_RUNNING=false
|
||||
SERVICE_WAS_ENABLED=false
|
||||
|
||||
# Check if service unit file exists
|
||||
if [ -f /etc/systemd/system/oculog-client.service ] || systemctl list-unit-files | grep -q "^oculog-client.service"; then
|
||||
IS_UPGRADE=true
|
||||
echo -e "${YELLOW}Detected existing installation - performing upgrade...${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if service is enabled
|
||||
if systemctl is-enabled --quiet oculog-client.service 2>/dev/null; then
|
||||
SERVICE_WAS_ENABLED=true
|
||||
fi
|
||||
|
||||
# Check if service is running
|
||||
if systemctl is-active --quiet oculog-client.service 2>/dev/null; then
|
||||
SERVICE_WAS_RUNNING=true
|
||||
echo -e "${YELLOW}Service is currently running. Stopping it...${NC}"
|
||||
systemctl stop oculog-client.service || true
|
||||
sleep 1
|
||||
if systemctl is-active --quiet oculog-client.service 2>/dev/null; then
|
||||
echo -e "${RED}Warning: Service did not stop cleanly${NC}"
|
||||
else
|
||||
echo -e "${GREEN}Service stopped${NC}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}Fresh installation detected${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Step 1: Creating installation directory...${NC}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
mkdir -p /var/log/oculog
|
||||
mkdir -p /etc/oculog
|
||||
|
||||
echo -e "${GREEN}Step 2: Copying client files...${NC}"
|
||||
cp "$SCRIPT_DIR/client.py" "$INSTALL_DIR/"
|
||||
chmod +x "$INSTALL_DIR/client.py"
|
||||
|
||||
echo -e "${GREEN}Step 3: Installing Python dependencies...${NC}"
|
||||
|
||||
# Try to install via apt first (for newer Ubuntu versions with externally-managed environment)
|
||||
if apt-cache show python3-psutil >/dev/null 2>&1 && apt-cache show python3-requests >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Installing via apt (system-managed packages)...${NC}"
|
||||
apt-get update
|
||||
if apt-get install -y python3-psutil python3-requests; then
|
||||
PYTHON_BIN="/usr/bin/python3"
|
||||
echo -e "${GREEN}System packages installed successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Apt installation failed, using virtual environment...${NC}"
|
||||
USE_VENV=1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}System packages not available, using virtual environment...${NC}"
|
||||
USE_VENV=1
|
||||
fi
|
||||
|
||||
# If apt packages aren't available, use a virtual environment
|
||||
if [ "$USE_VENV" = "1" ]; then
|
||||
if [ -d "$INSTALL_DIR/venv" ]; then
|
||||
echo -e "${YELLOW}Updating existing virtual environment...${NC}"
|
||||
PYTHON_BIN="$INSTALL_DIR/venv/bin/python3"
|
||||
else
|
||||
echo -e "${YELLOW}Setting up virtual environment...${NC}"
|
||||
apt-get update
|
||||
apt-get install -y python3-venv python3-pip || true
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv "$INSTALL_DIR/venv"
|
||||
PYTHON_BIN="$INSTALL_DIR/venv/bin/python3"
|
||||
fi
|
||||
|
||||
# Install/upgrade packages in virtual environment
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade pip
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade -r "$SCRIPT_DIR/requirements.txt"
|
||||
|
||||
echo -e "${GREEN}Virtual environment ready and packages installed${NC}"
|
||||
fi
|
||||
|
||||
# Update shebang in client script to use correct Python
|
||||
sed -i "1s|.*|#!${PYTHON_BIN}|" "$INSTALL_DIR/client.py"
|
||||
|
||||
echo -e "${GREEN}Step 4: Creating symlink...${NC}"
|
||||
ln -sf "$INSTALL_DIR/client.py" /usr/local/bin/oculog-client
|
||||
|
||||
echo -e "${GREEN}Step 5: Setting up configuration...${NC}"
|
||||
if [ ! -f /etc/oculog/client.conf ]; then
|
||||
# Fresh installation - create new config
|
||||
if [ -f "$SCRIPT_DIR/client.conf.example" ]; then
|
||||
cp "$SCRIPT_DIR/client.conf.example" /etc/oculog/client.conf
|
||||
echo -e "${YELLOW}Configuration file created at /etc/oculog/client.conf${NC}"
|
||||
echo -e "${YELLOW}Please edit it with your server URL and server ID${NC}"
|
||||
else
|
||||
cat > /etc/oculog/client.conf << EOF
|
||||
{
|
||||
"server_url": "http://localhost:3001",
|
||||
"server_id": "$(hostname)",
|
||||
"interval": 30
|
||||
}
|
||||
EOF
|
||||
echo -e "${YELLOW}Default configuration created at /etc/oculog/client.conf${NC}"
|
||||
fi
|
||||
else
|
||||
# Existing installation - preserve config
|
||||
if [ "$IS_UPGRADE" = true ]; then
|
||||
# Backup existing config before upgrade
|
||||
BACKUP_FILE="/etc/oculog/client.conf.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp /etc/oculog/client.conf "$BACKUP_FILE"
|
||||
echo -e "${GREEN}Configuration file preserved (backup: $BACKUP_FILE)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}Configuration file already exists, preserving...${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Step 6: Installing systemd service...${NC}"
|
||||
# Update service file to use correct Python binary
|
||||
sed "s|ExecStart=/usr/local/bin/oculog-client|ExecStart=${PYTHON_BIN} $INSTALL_DIR/client.py|" "$SCRIPT_DIR/oculog-client.service" > /etc/systemd/system/oculog-client.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable oculog-client.service
|
||||
|
||||
# Restart service if it was running before upgrade
|
||||
if [ "$IS_UPGRADE" = true ]; then
|
||||
if [ "$SERVICE_WAS_RUNNING" = true ] || [ "$SERVICE_WAS_ENABLED" = true ]; then
|
||||
echo -e "${GREEN}Step 7: Starting service...${NC}"
|
||||
systemctl start oculog-client.service || true
|
||||
sleep 2
|
||||
if systemctl is-active --quiet oculog-client.service 2>/dev/null; then
|
||||
echo -e "${GREEN}Service started successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: Service may have failed to start. Check status with: systemctl status oculog-client${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ "$IS_UPGRADE" = true ]; then
|
||||
echo -e "${GREEN}Upgrade completed successfully!${NC}"
|
||||
else
|
||||
echo -e "${GREEN}Installation completed successfully!${NC}"
|
||||
fi
|
||||
echo ""
|
||||
if [ "$IS_UPGRADE" = false ]; then
|
||||
echo "Next steps:"
|
||||
echo "1. Edit /etc/oculog/client.conf with your server URL"
|
||||
echo "2. Start the service: sudo systemctl start oculog-client"
|
||||
echo "3. Check status: sudo systemctl status oculog-client"
|
||||
echo "4. View logs: sudo journalctl -u oculog-client -f"
|
||||
else
|
||||
echo "Upgrade complete. Service status:"
|
||||
systemctl status oculog-client.service --no-pager -l || true
|
||||
echo ""
|
||||
echo "View logs: sudo journalctl -u oculog-client -f"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
15
clients/ubuntu/oculog-client.service
Normal file
15
clients/ubuntu/oculog-client.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Oculog Client Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/local/bin/oculog-client
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
3
clients/ubuntu/requirements.txt
Normal file
3
clients/ubuntu/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
psutil==5.9.6
|
||||
requests==2.31.0
|
||||
|
||||
87
clients/ubuntu/troubleshoot-install.sh
Executable file
87
clients/ubuntu/troubleshoot-install.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# Troubleshooting script for Oculog client installation
|
||||
|
||||
echo "Checking Oculog client installation..."
|
||||
echo ""
|
||||
|
||||
# Check if client script exists
|
||||
if [ -f /opt/oculog/client.py ]; then
|
||||
echo "✓ Client script found at /opt/oculog/client.py"
|
||||
else
|
||||
echo "✗ Client script NOT found at /opt/oculog/client.py"
|
||||
echo " You may need to download it manually or re-run the installer"
|
||||
fi
|
||||
|
||||
# Check if config exists
|
||||
if [ -f /etc/oculog/client.conf ]; then
|
||||
echo "✓ Configuration file found at /etc/oculog/client.conf"
|
||||
echo " Current config:"
|
||||
cat /etc/oculog/client.conf | sed 's/^/ /'
|
||||
else
|
||||
echo "✗ Configuration file NOT found at /etc/oculog/client.conf"
|
||||
fi
|
||||
|
||||
# Check if service file exists
|
||||
if [ -f /etc/systemd/system/oculog-client.service ]; then
|
||||
echo "✓ Systemd service file found"
|
||||
echo " Service file contents:"
|
||||
cat /etc/systemd/system/oculog-client.service | sed 's/^/ /'
|
||||
else
|
||||
echo "✗ Systemd service file NOT found"
|
||||
echo ""
|
||||
echo "Creating systemd service file..."
|
||||
|
||||
# Determine Python path
|
||||
if [ -f /opt/oculog/venv/bin/python3 ]; then
|
||||
PYTHON_BIN="/opt/oculog/venv/bin/python3"
|
||||
echo " Using virtual environment Python: $PYTHON_BIN"
|
||||
else
|
||||
PYTHON_BIN="/usr/bin/python3"
|
||||
echo " Using system Python: $PYTHON_BIN"
|
||||
fi
|
||||
|
||||
# Create service file
|
||||
cat > /etc/systemd/system/oculog-client.service << EOF
|
||||
[Unit]
|
||||
Description=Oculog Client Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=${PYTHON_BIN} /opt/oculog/client.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "✓ Service file created"
|
||||
systemctl daemon-reload
|
||||
echo "✓ Systemd daemon reloaded"
|
||||
fi
|
||||
|
||||
# Check if symlink exists
|
||||
if [ -L /usr/local/bin/oculog-client ]; then
|
||||
echo "✓ Symlink found at /usr/local/bin/oculog-client"
|
||||
else
|
||||
echo "✗ Symlink NOT found"
|
||||
if [ -f /opt/oculog/client.py ]; then
|
||||
echo " Creating symlink..."
|
||||
ln -sf /opt/oculog/client.py /usr/local/bin/oculog-client
|
||||
echo "✓ Symlink created"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installation check complete!"
|
||||
echo ""
|
||||
echo "To start the service:"
|
||||
echo " sudo systemctl start oculog-client"
|
||||
echo " sudo systemctl enable oculog-client"
|
||||
echo ""
|
||||
echo "To check status:"
|
||||
echo " sudo systemctl status oculog-client"
|
||||
41
clients/ubuntu/uninstall.sh
Executable file
41
clients/ubuntu/uninstall.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}Oculog Client Uninstallation${NC}"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root (use sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Step 1: Stopping and disabling service...${NC}"
|
||||
systemctl stop oculog-client.service 2>/dev/null || true
|
||||
systemctl disable oculog-client.service 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}Step 2: Removing systemd service file...${NC}"
|
||||
rm -f /etc/systemd/system/oculog-client.service
|
||||
systemctl daemon-reload
|
||||
|
||||
echo -e "${GREEN}Step 3: Removing symlink...${NC}"
|
||||
rm -f /usr/local/bin/oculog-client
|
||||
|
||||
echo -e "${GREEN}Step 4: Removing installation directory...${NC}"
|
||||
rm -rf /opt/oculog
|
||||
|
||||
echo -e "${YELLOW}Note: Configuration files and logs are preserved:${NC}"
|
||||
echo " - /etc/oculog/client.conf"
|
||||
echo " - /var/log/oculog/client.log"
|
||||
echo ""
|
||||
echo -e "${GREEN}Uninstallation completed!${NC}"
|
||||
echo ""
|
||||
|
||||
146
clients/ubuntu/update-client.sh
Executable file
146
clients/ubuntu/update-client.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
# Oculog Client Update Script
|
||||
# Updates the Oculog client with success/failure reporting for each step
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash update-client.sh [OCULOG_SERVER_URL]
|
||||
# Example: sudo bash update-client.sh http://YOUR_SERVER_IP:3001
|
||||
#
|
||||
# Or set environment variable:
|
||||
# export OCULOG_SERVER=http://YOUR_SERVER_IP:3001
|
||||
# sudo bash update-client.sh
|
||||
|
||||
set -e # Exit on error (we'll handle errors manually)
|
||||
|
||||
# Configuration - use argument, environment variable, or default
|
||||
if [ -n "$1" ]; then
|
||||
OCULOG_SERVER="$1"
|
||||
elif [ -n "$OCULOG_SERVER" ]; then
|
||||
OCULOG_SERVER="$OCULOG_SERVER"
|
||||
else
|
||||
echo "Error: Oculog server URL not specified"
|
||||
echo ""
|
||||
echo "Usage: sudo bash update-client.sh [OCULOG_SERVER_URL]"
|
||||
echo "Example: sudo bash update-client.sh http://YOUR_SERVER_IP:3001"
|
||||
echo ""
|
||||
echo "Or set environment variable:"
|
||||
echo " export OCULOG_SERVER=http://YOUR_SERVER_IP:3001"
|
||||
echo " sudo bash update-client.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Oculog Client Update Script ==="
|
||||
echo "Server: $OCULOG_SERVER"
|
||||
echo ""
|
||||
|
||||
# Function to check command success
|
||||
check_success() {
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Success"
|
||||
return 0
|
||||
else
|
||||
echo "✗ Failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Stop the Oculog client service
|
||||
echo -n "Step 1: Stopping oculog-client service... "
|
||||
if sudo systemctl stop oculog-client.service 2>/dev/null; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed (service may not be running)"
|
||||
fi
|
||||
|
||||
# 2. Download the updated client script
|
||||
echo -n "Step 2: Downloading updated client script... "
|
||||
if sudo curl -s "$OCULOG_SERVER/api/client-script" | sudo tee /opt/oculog/client.py.new > /dev/null 2>&1; then
|
||||
if [ -f /opt/oculog/client.py.new ] && [ -s /opt/oculog/client.py.new ]; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed - File not created or empty"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✗ Failed - Download error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Backup the current client script
|
||||
echo -n "Step 3: Backing up current client script... "
|
||||
if [ -f /opt/oculog/client.py ]; then
|
||||
if sudo cp /opt/oculog/client.py /opt/oculog/client.py.backup 2>/dev/null; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠ Skipped - No existing client.py found"
|
||||
fi
|
||||
|
||||
# 4. Replace the client script
|
||||
echo -n "Step 4: Replacing client script... "
|
||||
if sudo mv /opt/oculog/client.py.new /opt/oculog/client.py 2>/dev/null; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Verify version was injected
|
||||
echo -n "Step 5: Verifying version injection... "
|
||||
if sudo grep -q 'CLIENT_VERSION_BUILD_TIMESTAMP = "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}"' /opt/oculog/client.py 2>/dev/null; then
|
||||
VERSION=$(sudo grep 'CLIENT_VERSION_BUILD_TIMESTAMP = "' /opt/oculog/client.py | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "✓ Success (Version: $VERSION)"
|
||||
else
|
||||
echo "⚠ Warning - Version may not have been injected (will use file modification time)"
|
||||
fi
|
||||
|
||||
# 6. Ensure the script is executable
|
||||
echo -n "Step 6: Setting executable permissions... "
|
||||
if sudo chmod +x /opt/oculog/client.py 2>/dev/null; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 7. Restart the service
|
||||
echo -n "Step 7: Starting oculog-client service... "
|
||||
if sudo systemctl start oculog-client.service 2>/dev/null; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 8. Wait a moment for service to start
|
||||
sleep 2
|
||||
|
||||
# 9. Verify the service is running
|
||||
echo -n "Step 8: Verifying service status... "
|
||||
if sudo systemctl is-active --quiet oculog-client.service 2>/dev/null; then
|
||||
echo "✓ Success - Service is running"
|
||||
else
|
||||
echo "✗ Failed - Service is not running"
|
||||
echo ""
|
||||
echo "Checking service status:"
|
||||
sudo systemctl status oculog-client.service --no-pager -l || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Update Complete ==="
|
||||
echo ""
|
||||
echo "Note: The client version in the web UI will update after the client sends"
|
||||
echo " its next metrics update (usually within 30 seconds)."
|
||||
echo ""
|
||||
echo "To view logs, run:"
|
||||
echo " sudo journalctl -u oculog-client.service -f"
|
||||
echo ""
|
||||
echo "To check service status, run:"
|
||||
echo " sudo systemctl status oculog-client.service"
|
||||
echo ""
|
||||
echo "To verify the client version immediately, check logs for:"
|
||||
echo " sudo journalctl -u oculog-client.service -n 50 | grep -i version"
|
||||
Reference in New Issue
Block a user