add(app): create python application

This commit is contained in:
Aleksandr Tcitlionok
2024-12-11 12:14:25 +00:00
parent 11641e5e10
commit 24e09330ea
11 changed files with 531 additions and 2 deletions

View File

@@ -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
View File

0
cli/commands/__init__.py Normal file
View File

36
cli/commands/export.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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",
)