mirror of
https://github.com/peasead/elastic-container
synced 2026-06-08 16:43:23 +00:00
3c21bd7d14
Detection Engine improvements (kibana_manager.py): - Increase prepackaged rules installation timeout: 60s → 180s - Increase bulk enable rules timeout: 60s → 120s - Fixes timeout errors when installing ~1400 prebuilt detection rules - Rules install successfully even with previous timeout, this eliminates warnings Output formatting improvements (docker_manager.py): - Remove bash-style separators (##### lines) - Standardize all command output to match 'start' command style - Use consistent cyan for actions, green for success - Add blank lines before final success messages - Commands affected: stop, destroy, restart, stage
453 lines
14 KiB
Python
453 lines
14 KiB
Python
"""
|
|
Docker management for Elastic Container
|
|
Handles all Docker Compose operations
|
|
"""
|
|
|
|
import subprocess
|
|
import click
|
|
import requests
|
|
from typing import Optional
|
|
from urllib3.exceptions import InsecureRequestWarning
|
|
|
|
from .config import Config
|
|
from .utils import run_command, show_docker_error
|
|
|
|
# Suppress SSL warnings for self-signed certificates
|
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
|
|
|
|
class DockerManager:
|
|
"""
|
|
Manages Docker Compose operations for Elastic Container
|
|
"""
|
|
|
|
def __init__(self, config: Config, verbose: bool = False):
|
|
"""
|
|
Initialize Docker Manager
|
|
|
|
Args:
|
|
config: Configuration object
|
|
verbose: Enable verbose output
|
|
"""
|
|
self.config = config
|
|
self.verbose = verbose
|
|
self.compose_command = self._detect_compose_command()
|
|
|
|
def _detect_compose_command(self) -> Optional[list]:
|
|
"""
|
|
Detect available docker compose command
|
|
|
|
Returns:
|
|
Command as list (e.g., ["docker", "compose"]) or None if not found
|
|
"""
|
|
# Try docker compose (plugin) first
|
|
try:
|
|
result = subprocess.run(
|
|
["docker", "compose", "version"],
|
|
capture_output=True,
|
|
check=True
|
|
)
|
|
if self.verbose:
|
|
click.secho("✓ Found: docker compose (plugin)", fg="green")
|
|
return ["docker", "compose"]
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
pass
|
|
|
|
# Try docker-compose (standalone)
|
|
try:
|
|
result = subprocess.run(
|
|
["docker-compose", "version"],
|
|
capture_output=True,
|
|
check=True
|
|
)
|
|
if self.verbose:
|
|
click.secho("✓ Found: docker-compose (standalone)", fg="green")
|
|
return ["docker-compose"]
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
pass
|
|
|
|
return None
|
|
|
|
def check_docker_available(self) -> bool:
|
|
"""
|
|
Check if Docker is installed and running
|
|
|
|
Returns:
|
|
True if Docker is ready, False otherwise
|
|
"""
|
|
# Check if docker command exists
|
|
try:
|
|
subprocess.run(
|
|
["docker", "--version"],
|
|
capture_output=True,
|
|
check=True
|
|
)
|
|
except FileNotFoundError:
|
|
show_docker_error(
|
|
"Docker is not installed!",
|
|
"Docker is required to run Elastic Container.",
|
|
[
|
|
"macOS: https://docs.docker.com/desktop/install/mac-install/",
|
|
"Linux: https://docs.docker.com/engine/install/",
|
|
"Windows: https://docs.docker.com/desktop/install/windows-install/"
|
|
]
|
|
)
|
|
return False
|
|
|
|
# Check if Docker daemon is running
|
|
try:
|
|
subprocess.run(
|
|
["docker", "ps"],
|
|
capture_output=True,
|
|
check=True
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
show_docker_error(
|
|
"Docker is installed but not running!",
|
|
"Please start Docker:",
|
|
[
|
|
"macOS/Windows: Open Docker Desktop application",
|
|
"Linux: Run 'sudo systemctl start docker' or 'sudo service docker start'"
|
|
]
|
|
)
|
|
return False
|
|
|
|
# Check if compose is available
|
|
if self.compose_command is None:
|
|
show_docker_error(
|
|
"Docker Compose is not available!",
|
|
"Elastic Container requires 'docker compose' (plugin) or 'docker-compose' (standalone).",
|
|
[
|
|
"Plugin (recommended): Included with Docker Desktop or install docker-compose-plugin",
|
|
"Standalone: https://docs.docker.com/compose/install/"
|
|
]
|
|
)
|
|
return False
|
|
|
|
if self.verbose:
|
|
click.secho("✓ Docker is available and running", fg="green")
|
|
|
|
return True
|
|
|
|
def _run_compose(self, args: list, check: bool = True) -> tuple[int, str, str]:
|
|
"""
|
|
Execute a docker compose command
|
|
|
|
Args:
|
|
args: Additional arguments for docker compose
|
|
check: Raise exception on error
|
|
|
|
Returns:
|
|
Tuple of (exit_code, stdout, stderr)
|
|
"""
|
|
command = self.compose_command + args
|
|
return run_command(command, check=check, verbose=self.verbose)
|
|
|
|
def stage(self) -> bool:
|
|
"""
|
|
Pull all required Docker images
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Staging Docker images...", fg="cyan")
|
|
click.echo()
|
|
|
|
images = [
|
|
f"docker.elastic.co/elasticsearch/elasticsearch:{self.config.stack_version}",
|
|
f"docker.elastic.co/kibana/kibana:{self.config.stack_version}",
|
|
f"docker.elastic.co/elastic-agent/elastic-agent:{self.config.stack_version}"
|
|
]
|
|
|
|
for image in images:
|
|
click.echo(f"Pulling {image}...")
|
|
exit_code, stdout, stderr = run_command(
|
|
["docker", "pull", image],
|
|
check=False,
|
|
verbose=self.verbose
|
|
)
|
|
|
|
if exit_code != 0:
|
|
click.secho(f"✗ Failed to pull {image}", fg="red")
|
|
click.echo(stderr)
|
|
return False
|
|
|
|
click.secho(f"✓ Pulled {image}", fg="green")
|
|
|
|
click.echo()
|
|
click.secho("✓ Images staged successfully", fg="green")
|
|
return True
|
|
|
|
def start(self) -> bool:
|
|
"""
|
|
Start the Elastic Stack containers
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Starting Elastic Stack network and containers...", fg="cyan", bold=True)
|
|
click.echo()
|
|
|
|
exit_code, stdout, stderr = self._run_compose(
|
|
["up", "-d", "--no-deps"],
|
|
check=False
|
|
)
|
|
|
|
if exit_code != 0:
|
|
show_docker_error(
|
|
"Failed to start containers!",
|
|
f"Command: {' '.join(self.compose_command)} up -d\nExit code: {exit_code}\n\nError output:\n{stderr}",
|
|
[
|
|
"Check if ports 5601, 9200, 8220 are already in use",
|
|
"Ensure you have enough disk space",
|
|
"Try 'docker compose down -v' to clean up first",
|
|
"Run with -v flag for verbose output"
|
|
]
|
|
)
|
|
return False
|
|
|
|
# Show output (docker compose shows container status)
|
|
if stdout:
|
|
click.echo(stdout)
|
|
|
|
return True
|
|
|
|
def stop(self) -> bool:
|
|
"""
|
|
Stop running containers without removing them
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Stopping containers...", fg="cyan")
|
|
click.echo()
|
|
|
|
exit_code, stdout, stderr = self._run_compose(["stop"], check=False)
|
|
|
|
if exit_code != 0:
|
|
click.secho("✗ Failed to stop containers", fg="red")
|
|
click.echo(stderr)
|
|
return False
|
|
|
|
if stdout:
|
|
click.echo(stdout)
|
|
|
|
click.echo()
|
|
click.secho("✓ Containers stopped", fg="green")
|
|
return True
|
|
|
|
def destroy(self) -> bool:
|
|
"""
|
|
Stop and remove containers, networks, and volumes
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Destroying Elastic Stack...", fg="cyan")
|
|
click.echo()
|
|
|
|
exit_code, stdout, stderr = self._run_compose(["down", "-v"], check=False)
|
|
|
|
if exit_code != 0:
|
|
click.secho("✗ Failed to destroy stack", fg="red")
|
|
click.echo(stderr)
|
|
return False
|
|
|
|
if stdout:
|
|
click.echo(stdout)
|
|
|
|
click.echo()
|
|
click.secho("✓ Stack destroyed", fg="green")
|
|
return True
|
|
|
|
def restart(self) -> bool:
|
|
"""
|
|
Restart Elastic Stack containers
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Restarting containers...", fg="cyan")
|
|
click.echo()
|
|
|
|
exit_code, stdout, stderr = self._run_compose(
|
|
["restart", "elasticsearch", "kibana", "fleet-server"],
|
|
check=False
|
|
)
|
|
|
|
if exit_code != 0:
|
|
click.secho("✗ Failed to restart containers", fg="red")
|
|
click.echo(stderr)
|
|
return False
|
|
|
|
if stdout:
|
|
click.echo(stdout)
|
|
|
|
click.echo()
|
|
click.secho("✓ Containers restarted", fg="green")
|
|
return True
|
|
|
|
def status(self) -> bool:
|
|
"""
|
|
Show status of running containers
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
exit_code, stdout, stderr = self._run_compose(["ps"], check=False)
|
|
|
|
if exit_code != 0:
|
|
click.secho(f"✗ Failed to get status", fg="red")
|
|
click.echo(stderr)
|
|
return False
|
|
|
|
# Filter out setup container
|
|
lines = stdout.split('\n')
|
|
filtered_lines = [line for line in lines if 'setup' not in line.lower() or line.startswith('NAME')]
|
|
|
|
click.echo('\n'.join(filtered_lines))
|
|
return True
|
|
|
|
def check_containers_exist(self) -> tuple[bool, bool]:
|
|
"""
|
|
Check if containers exist and whether they're running
|
|
|
|
Returns:
|
|
Tuple of (containers_exist, containers_running)
|
|
"""
|
|
# Check all containers including stopped ones
|
|
exit_code, stdout, stderr = self._run_compose(["ps", "-a"], check=False)
|
|
|
|
if exit_code != 0:
|
|
return False, False
|
|
|
|
# Check for main containers (elasticsearch, kibana, fleet-server)
|
|
main_containers = ["elasticsearch", "kibana", "fleet-server"]
|
|
found_containers = []
|
|
running_containers = []
|
|
|
|
for line in stdout.split('\n'):
|
|
line_lower = line.lower()
|
|
for container in main_containers:
|
|
if container in line_lower:
|
|
found_containers.append(container)
|
|
# Check if running (has "Up" in status or "running")
|
|
if 'up' in line_lower or 'running' in line_lower:
|
|
running_containers.append(container)
|
|
|
|
# Containers exist if we found at least 2 of the 3 main ones
|
|
containers_exist = len(found_containers) >= 2
|
|
# All running if we found all 3 running
|
|
all_running = len(running_containers) >= 3
|
|
|
|
return containers_exist, all_running
|
|
|
|
def get_ca_fingerprint(self) -> Optional[str]:
|
|
"""
|
|
Extract CA certificate fingerprint from Elasticsearch container
|
|
|
|
Returns:
|
|
Fingerprint string or None on failure
|
|
"""
|
|
try:
|
|
# Get the CA cert from the container
|
|
cat_command = self.compose_command + [
|
|
"exec", "-w", "/usr/share/elasticsearch/config/certs/ca",
|
|
"elasticsearch", "cat", "ca.crt"
|
|
]
|
|
|
|
cat_result = subprocess.run(
|
|
cat_command,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
|
|
# Get fingerprint using openssl
|
|
openssl_result = subprocess.run(
|
|
["openssl", "x509", "-noout", "-fingerprint", "-sha256"],
|
|
input=cat_result.stdout,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
|
|
# Parse fingerprint (format: "SHA256 Fingerprint=XX:XX:XX...")
|
|
fingerprint_line = openssl_result.stdout.strip()
|
|
if "=" in fingerprint_line:
|
|
fingerprint = fingerprint_line.split("=")[1].replace(":", "")
|
|
return fingerprint
|
|
|
|
return None
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
if self.verbose:
|
|
click.secho(f"Failed to get CA fingerprint: {e}", fg="red")
|
|
return None
|
|
|
|
def clear_documents(self, host_ip: str) -> bool:
|
|
"""
|
|
Clear all documents in logs and metrics data streams
|
|
|
|
Args:
|
|
host_ip: Elasticsearch host IP
|
|
|
|
Returns:
|
|
True on success, False on failure
|
|
"""
|
|
click.echo()
|
|
click.secho("Clearing data streams...", fg="cyan", bold=True)
|
|
click.echo()
|
|
|
|
es_url = f"https://{host_ip}:{self.config.es_port}"
|
|
auth = (self.config.elastic_username, self.config.elastic_password)
|
|
|
|
success = True
|
|
|
|
# Clear logs data stream
|
|
try:
|
|
response = requests.delete(
|
|
f"{es_url}/_data_stream/logs-*",
|
|
auth=auth,
|
|
verify=False,
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code == 200 and "true" in response.text:
|
|
click.secho("✓ Successfully cleared logs data stream", fg="green")
|
|
else:
|
|
click.secho("✗ Failed to clear logs data stream", fg="red")
|
|
success = False
|
|
|
|
except Exception as e:
|
|
click.secho(f"✗ Failed to clear logs data stream: {e}", fg="red")
|
|
success = False
|
|
|
|
# Clear metrics data stream
|
|
try:
|
|
response = requests.delete(
|
|
f"{es_url}/_data_stream/metrics-*",
|
|
auth=auth,
|
|
verify=False,
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code == 200 and "true" in response.text:
|
|
click.secho("✓ Successfully cleared metrics data stream", fg="green")
|
|
else:
|
|
click.secho("✗ Failed to clear metrics data stream", fg="red")
|
|
success = False
|
|
|
|
except Exception as e:
|
|
click.secho(f"✗ Failed to clear metrics data stream: {e}", fg="red")
|
|
success = False
|
|
|
|
click.echo()
|
|
return success
|
|
|