mirror of
https://ak-git.vectorsigma.ru/terghalin/metalcheck-cli.git
synced 2025-10-26 07:25:51 +09:00
add(app): create python application
This commit is contained in:
32
README.md
32
README.md
@@ -1,3 +1,31 @@
|
||||
# metalcheck-ui
|
||||
# metalcheck-cli
|
||||
|
||||
Text-based UI for Metal Check
|
||||
CLI tool for managing and visualizing Metal Check data"
|
||||
|
||||
# Installation
|
||||
|
||||
```shell
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
```shell
|
||||
pip uninstall metalcheck-cli
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
```plaintext
|
||||
Usage: metalcheck [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--help Show help message
|
||||
|
||||
Commands:
|
||||
export Export Metal Check data in the specified format (yaml or json).
|
||||
k8s Commands for managing Kubernetes Nodes.
|
||||
metal Commands for managing Metal Nodes.
|
||||
visual Displays the visual dashboard with Metal Nodes, Virtual...
|
||||
vm Commands for managing Virtual Machines.
|
||||
```
|
||||
|
||||
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
0
cli/commands/__init__.py
Normal file
0
cli/commands/__init__.py
Normal file
36
cli/commands/export.py
Normal file
36
cli/commands/export.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import click
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000/export" # Backend URL for exporting data
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--format",
|
||||
type=click.Choice(["yaml", "json"], case_sensitive=False),
|
||||
default="json",
|
||||
help="Specify the export format: yaml or json. Default is json.",
|
||||
)
|
||||
@click.argument("output", required=False, type=click.Path(writable=True))
|
||||
def export_data(format, output):
|
||||
"""
|
||||
Export Metal Check data in the specified format (yaml or json).
|
||||
|
||||
If an OUTPUT file is provided, the data will be saved to the file.
|
||||
Otherwise, it will be printed to the console.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}?format={format}")
|
||||
if response.status_code == 200:
|
||||
data = response.text
|
||||
if output:
|
||||
with open(output, "w") as file:
|
||||
file.write(data)
|
||||
click.echo(f"Data successfully exported to {output}.")
|
||||
else:
|
||||
click.echo("\n📤 Exported Data:")
|
||||
click.echo(data)
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
57
cli/commands/k8s.py
Normal file
57
cli/commands/k8s.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import click
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000/k8s" # Backend URL for Kubernetes API
|
||||
|
||||
@click.group()
|
||||
def kubernetes_nodes():
|
||||
"""Commands for managing Kubernetes Nodes."""
|
||||
pass
|
||||
|
||||
@kubernetes_nodes.command("list")
|
||||
def list_command():
|
||||
handle_command("list")
|
||||
|
||||
@kubernetes_nodes.command("think")
|
||||
def think_command():
|
||||
handle_command("think")
|
||||
|
||||
|
||||
def handle_command(action):
|
||||
"""Handle commands related to Kubernetes nodes."""
|
||||
if action == "list":
|
||||
list_kubernetes_nodes()
|
||||
elif action == "analyze":
|
||||
analyze_kubernetes_cluster()
|
||||
|
||||
def list_kubernetes_nodes():
|
||||
"""List all Kubernetes nodes."""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/data")
|
||||
if response.status_code == 200:
|
||||
nodes = response.json().get("nodes", [])
|
||||
click.echo("\n📦 Kubernetes Nodes:")
|
||||
for node in nodes:
|
||||
click.echo(
|
||||
f"Node Name: {node['node_name']}, CPU: {node['cpu']}, "
|
||||
f"Memory: {node['memory']} MiB, Storage: {node['storage']}, "
|
||||
f"Type: {node['instance_type']}, Max Pods: {node['pods_allocatable']}, "
|
||||
f"Time on Duty: {node['time_on_duty']}"
|
||||
)
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
def analyze_kubernetes_cluster():
|
||||
"""Request an AI analysis of the Kubernetes cluster."""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/think/k8s")
|
||||
if response.status_code == 200:
|
||||
summary = response.json().get("summary", "No analysis provided.")
|
||||
click.echo("\n🤖 AI Analysis of Kubernetes Cluster:")
|
||||
click.echo(summary)
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
96
cli/commands/metal.py
Normal file
96
cli/commands/metal.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import click
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000/metal" # Backend URL for Metal Nodes API
|
||||
|
||||
@click.group()
|
||||
def metal_nodes():
|
||||
"""Commands for managing Metal Nodes."""
|
||||
pass
|
||||
|
||||
@metal_nodes.command("list")
|
||||
def list_command():
|
||||
handle_command("list")
|
||||
|
||||
@metal_nodes.command("add")
|
||||
def add_command():
|
||||
handle_command("add")
|
||||
|
||||
@metal_nodes.command("delete")
|
||||
def delete_command():
|
||||
handle_command("delete")
|
||||
|
||||
def handle_command(action):
|
||||
"""Handle commands related to Metal Nodes."""
|
||||
if action == "list":
|
||||
list_metal_nodes()
|
||||
elif action == "add":
|
||||
add_metal_node()
|
||||
elif action == "delete":
|
||||
delete_metal_node()
|
||||
|
||||
def list_metal_nodes():
|
||||
"""List all metal nodes."""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/data")
|
||||
if response.status_code == 200:
|
||||
metal_nodes = response.json().get("metal_nodes", [])
|
||||
click.echo("\n🖥️ Metal Nodes:")
|
||||
for node in metal_nodes:
|
||||
click.echo(
|
||||
f"ID: {node[0]}, Name: {node[1]}, Location: {node[2]}, "
|
||||
f"Vendor: {node[3]}, CPU: {node[4]}, Memory: {node[5]}, "
|
||||
f"Storage: {node[6]}, Time on Duty: {node[7]}"
|
||||
)
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
def add_metal_node():
|
||||
"""Add a new metal node."""
|
||||
try:
|
||||
# Gather inputs from the prompt
|
||||
name = click.prompt("Name")
|
||||
location = click.prompt("Location")
|
||||
vendor = click.prompt("Vendor")
|
||||
cpu = click.prompt("CPU (cores)", type=int)
|
||||
memory = click.prompt("Memory (GB)")
|
||||
storage = click.prompt("Storage (e.g., 1TB SSD)")
|
||||
time_on_duty = click.prompt("Time on Duty (hours)", type=int)
|
||||
initial_cost = click.prompt("Initial Cost ($)", type=float)
|
||||
|
||||
data = {
|
||||
"name": name,
|
||||
"location": location,
|
||||
"vendor": vendor,
|
||||
"cpu": cpu,
|
||||
"memory": memory,
|
||||
"storage": storage,
|
||||
"time_on_duty": time_on_duty,
|
||||
"initial_cost": initial_cost,
|
||||
}
|
||||
|
||||
# Send the POST request to the backend
|
||||
response = requests.post(f"{BASE_URL}/data", json=data)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
response_data = response.json()
|
||||
message = response_data.get("message", "Metal node added successfully!")
|
||||
click.echo(f"✅ {message}")
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
def delete_metal_node():
|
||||
"""Delete a metal node."""
|
||||
try:
|
||||
node_id = click.prompt("Enter the ID of the metal node to delete", type=int)
|
||||
response = requests.delete(f"{BASE_URL}/data/{node_id}")
|
||||
if response.status_code == 200:
|
||||
click.echo("✅ Metal node deleted successfully!")
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
89
cli/commands/vm.py
Normal file
89
cli/commands/vm.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import click
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000/vm" # Backend URL for Virtual Machines API
|
||||
|
||||
@click.group()
|
||||
def virtual_machines():
|
||||
"""Commands for managing Virtual Machines."""
|
||||
pass
|
||||
|
||||
@virtual_machines.command("list")
|
||||
def list_command():
|
||||
handle_command("list")
|
||||
|
||||
@virtual_machines.command("add")
|
||||
def add_command():
|
||||
handle_command("add")
|
||||
|
||||
@virtual_machines.command("delete")
|
||||
def delete_command():
|
||||
handle_command("delete")
|
||||
|
||||
def handle_command(action):
|
||||
"""Handle commands related to Virtual Machines."""
|
||||
if action == "list":
|
||||
list_virtual_machines()
|
||||
elif action == "add":
|
||||
add_virtual_machine()
|
||||
elif action == "delete":
|
||||
delete_virtual_machine()
|
||||
|
||||
def list_virtual_machines():
|
||||
"""List all virtual machines."""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/data")
|
||||
if response.status_code == 200:
|
||||
virtual_machines = response.json().get("virtual_machines", [])
|
||||
click.echo("\n💻 Virtual Machines:")
|
||||
for vm in virtual_machines:
|
||||
click.echo(
|
||||
f"ID: {vm[0]}, Name: {vm[1]}, Location: {vm[2]}, "
|
||||
f"CPU: {vm[3]}, Memory: {vm[4]}, Storage: {vm[5]}, "
|
||||
f"Type: {vm[6]}"
|
||||
)
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
def add_virtual_machine():
|
||||
"""Add a new virtual machine."""
|
||||
try:
|
||||
name = click.prompt("Name")
|
||||
location = click.prompt("Location")
|
||||
cpu = click.prompt("CPU (cores)", type=int)
|
||||
memory = click.prompt("Memory (GB)")
|
||||
storage = click.prompt("Storage (e.g., 500GB SSD)")
|
||||
vm_type = click.prompt("Type (e.g., cx-21)")
|
||||
|
||||
data = {
|
||||
"name": name,
|
||||
"location": location,
|
||||
"cpu": cpu,
|
||||
"memory": memory,
|
||||
"storage": storage,
|
||||
"type": vm_type,
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/data", json=data)
|
||||
if response.status_code in [200, 201]:
|
||||
response_data = response.json()
|
||||
message = response_data.get("message", "Virtual machine added successfully!")
|
||||
click.echo(f"✅ {message}")
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
|
||||
def delete_virtual_machine():
|
||||
"""Delete a virtual machine."""
|
||||
try:
|
||||
vm_id = click.prompt("Enter the ID of the virtual machine to delete", type=int)
|
||||
response = requests.delete(f"{BASE_URL}/data/{vm_id}")
|
||||
if response.status_code == 200:
|
||||
click.echo("✅ Virtual machine deleted successfully!")
|
||||
else:
|
||||
click.echo(f"Error: {response.status_code} - {response.text}")
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Error: {e}")
|
||||
24
cli/main.py
Normal file
24
cli/main.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import click
|
||||
from .commands.metal import metal_nodes
|
||||
from .commands.vm import virtual_machines
|
||||
from .commands.k8s import kubernetes_nodes
|
||||
from .commands.export import export_data
|
||||
from .visual import visual_dashboard
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""
|
||||
Metal Check CLI: A command-line interface for managing and monitoring Metal Check resources.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Register commands
|
||||
cli.add_command(metal_nodes, name="metal")
|
||||
cli.add_command(virtual_machines, name="vm")
|
||||
cli.add_command(kubernetes_nodes, name="k8s")
|
||||
cli.add_command(export_data, name="export")
|
||||
cli.add_command(visual_dashboard, name="visual")
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
167
cli/visual.py
Normal file
167
cli/visual.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.progress import Progress
|
||||
import requests
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Define constants
|
||||
BACKEND_BASE_URL = "http://localhost:8000"
|
||||
THINK_K8S_URL = f"{BACKEND_BASE_URL}/think/k8s"
|
||||
METAL_NODES_URL = f"{BACKEND_BASE_URL}/metal/data"
|
||||
VM_URL = f"{BACKEND_BASE_URL}/vm/data"
|
||||
K8S_URL = f"{BACKEND_BASE_URL}/k8s/data"
|
||||
|
||||
console = Console()
|
||||
|
||||
# Helper functions for formatting
|
||||
def calculate_time_on_duty_hours(hours):
|
||||
if hours < 1:
|
||||
minutes = round(hours * 60)
|
||||
return f"{minutes} minutes" if minutes > 1 else "less than a minute"
|
||||
elif hours < 24:
|
||||
return f"{round(hours)} hours" if hours > 1 else "1 hour"
|
||||
else:
|
||||
days = hours // 24
|
||||
remaining_hours = hours % 24
|
||||
if remaining_hours:
|
||||
return f"{int(days)} days {int(remaining_hours)} hours"
|
||||
return f"{int(days)} days"
|
||||
|
||||
# Fetch and display metal nodes
|
||||
def display_metal_nodes():
|
||||
table = Table(title="🖥️ Metal Nodes", style="bold green")
|
||||
table.add_column("ID", justify="right", style="cyan")
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("Location", style="white")
|
||||
table.add_column("Vendor", style="green")
|
||||
table.add_column("CPU", justify="right", style="yellow")
|
||||
table.add_column("Memory (GB)", justify="right", style="cyan")
|
||||
table.add_column("Storage", style="magenta")
|
||||
table.add_column("Time on Duty", justify="right", style="magenta")
|
||||
|
||||
try:
|
||||
response = requests.get(METAL_NODES_URL)
|
||||
if response.status_code == 200:
|
||||
metal_nodes = response.json().get("metal_nodes", [])
|
||||
for node in metal_nodes:
|
||||
time_on_duty = calculate_time_on_duty_hours(node[7])
|
||||
table.add_row(
|
||||
f"{node[0]}",
|
||||
node[1],
|
||||
node[2],
|
||||
node[3],
|
||||
f"{node[4]}",
|
||||
node[5],
|
||||
node[6],
|
||||
time_on_duty,
|
||||
)
|
||||
else:
|
||||
console.print(f"[red]Failed to fetch metal nodes: {response.status_code}[/red]")
|
||||
except requests.RequestException as e:
|
||||
console.print(f"[red]Error fetching metal nodes: {e}[/red]")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Fetch and display virtual machines
|
||||
def display_virtual_machines():
|
||||
table = Table(title="💻 Virtual Machines", style="bold blue")
|
||||
table.add_column("ID", justify="right", style="cyan")
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("Location", style="white")
|
||||
table.add_column("CPU", justify="right", style="yellow")
|
||||
table.add_column("Memory (GB)", justify="right", style="cyan")
|
||||
table.add_column("Storage", style="magenta")
|
||||
table.add_column("Type", style="green")
|
||||
|
||||
try:
|
||||
response = requests.get(VM_URL)
|
||||
if response.status_code == 200:
|
||||
virtual_machines = response.json().get("virtual_machines", [])
|
||||
for vm in virtual_machines:
|
||||
table.add_row(
|
||||
f"{vm[0]}",
|
||||
vm[1],
|
||||
vm[2],
|
||||
f"{vm[3]}",
|
||||
vm[4],
|
||||
vm[5],
|
||||
vm[6],
|
||||
)
|
||||
else:
|
||||
console.print(f"[red]Failed to fetch virtual machines: {response.status_code}[/red]")
|
||||
except requests.RequestException as e:
|
||||
console.print(f"[red]Error fetching virtual machines: {e}[/red]")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Fetch and display Kubernetes nodes
|
||||
def display_kubernetes_nodes():
|
||||
table = Table(title="📦 Kubernetes Nodes", style="bold yellow")
|
||||
table.add_column("Node Name", style="white")
|
||||
table.add_column("CPU", justify="right", style="yellow")
|
||||
table.add_column("Memory (MiB)", justify="right", style="cyan")
|
||||
table.add_column("Storage (GB)", justify="right", style="green")
|
||||
table.add_column("Type", style="blue")
|
||||
table.add_column("Max Pods", justify="right", style="magenta")
|
||||
table.add_column("Time on Duty", justify="right", style="magenta")
|
||||
|
||||
try:
|
||||
response = requests.get(K8S_URL)
|
||||
if response.status_code == 200:
|
||||
nodes = response.json().get("nodes", [])
|
||||
for node in nodes:
|
||||
table.add_row(
|
||||
node["node_name"],
|
||||
node["cpu"],
|
||||
f"{node['memory']}",
|
||||
f"{node['storage']}",
|
||||
node["instance_type"],
|
||||
node["pods_allocatable"],
|
||||
node.get("time_on_duty", "N/A"),
|
||||
)
|
||||
else:
|
||||
console.print(f"[red]Failed to fetch Kubernetes nodes: {response.status_code}[/red]")
|
||||
except requests.RequestException as e:
|
||||
console.print(f"[red]Error fetching Kubernetes nodes: {e}[/red]")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Fetch and display AI summary
|
||||
def display_ai_summary():
|
||||
with Progress() as progress:
|
||||
task = progress.add_task("[cyan]Fetching AI Summary...", total=100)
|
||||
|
||||
try:
|
||||
for _ in range(10): # Simulate progress
|
||||
progress.update(task, advance=10)
|
||||
import time; time.sleep(0.1)
|
||||
|
||||
response = requests.get(THINK_K8S_URL)
|
||||
progress.update(task, completed=100)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
summary = data.get("summary", "No summary provided.")
|
||||
console.print("\n[bold magenta]AI Summary of Kubernetes Cluster:[/bold magenta]")
|
||||
console.print(f"[green]{summary}[/green]\n")
|
||||
else:
|
||||
console.print(f"[red]Failed to fetch AI summary: {response.status_code}[/red]")
|
||||
except requests.RequestException as e:
|
||||
console.print(f"[red]Error fetching AI summary: {e}[/red]")
|
||||
|
||||
# Click command for visual dashboard
|
||||
@click.command()
|
||||
@click.option('--summary', is_flag=True, help="Include AI summary in the dashboard.")
|
||||
def visual_dashboard(summary):
|
||||
"""
|
||||
Displays the visual dashboard with Metal Nodes, Virtual Machines,
|
||||
Kubernetes Nodes, and optionally AI Summary.
|
||||
"""
|
||||
console.print("✨ [bold green]Welcome to the Metal Check Dashboard![/bold green] ✨\n")
|
||||
display_metal_nodes()
|
||||
display_virtual_machines()
|
||||
display_kubernetes_nodes()
|
||||
|
||||
if summary:
|
||||
display_ai_summary()
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
click==8.1.3
|
||||
rich==13.4.0
|
||||
requests==2.31.0
|
||||
pandas==2.1.2
|
||||
28
setup.py
Normal file
28
setup.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="metalcheck-cli",
|
||||
version="0.1.0",
|
||||
description="CLI tool for managing and visualizing Metal Check data",
|
||||
author="Aleksandr Tcitlionok",
|
||||
author_email="satos@vskp.su",
|
||||
url="https://github.com/terghalin/metalcheck-cli",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"click",
|
||||
"requests",
|
||||
"rich",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"metalcheck=cli.main:cli",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU Affero General Public License 3.0 (AGPL 3.0)",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires=">=3.9",
|
||||
)
|
||||
Reference in New Issue
Block a user