1044 lines
36 KiB
Python
Executable File
1044 lines
36 KiB
Python
Executable File
"""
|
|
Node management commands for AITBC
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import socket
|
|
import json
|
|
import hashlib
|
|
import click
|
|
import asyncio
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
|
|
try:
|
|
from ..utils.output import output, success, error, warning, info
|
|
from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
|
|
from ..core.node_client import NodeClient
|
|
except ImportError:
|
|
from utils import output, error, success, warning
|
|
from core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
|
|
from core.node_client import NodeClient
|
|
|
|
def info(message):
|
|
print(message)
|
|
import uuid
|
|
|
|
@click.group()
|
|
def node():
|
|
"""Node management commands"""
|
|
pass
|
|
|
|
@node.command()
|
|
@click.argument('node_id')
|
|
@click.pass_context
|
|
def info(ctx, node_id):
|
|
"""Get detailed node information"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if node_id not in config.nodes:
|
|
error(f"Node {node_id} not found in configuration")
|
|
raise click.Abort()
|
|
|
|
node_config = config.nodes[node_id]
|
|
|
|
import asyncio
|
|
|
|
async def get_node_info():
|
|
async with NodeClient(node_config) as client:
|
|
return await client.get_node_info()
|
|
|
|
node_info = asyncio.run(get_node_info())
|
|
|
|
# Basic node information
|
|
basic_info = {
|
|
"Node ID": node_info["node_id"],
|
|
"Node Type": node_info["type"],
|
|
"Status": node_info["status"],
|
|
"Version": node_info["version"],
|
|
"Uptime": f"{node_info['uptime_days']} days, {node_info['uptime_hours']} hours",
|
|
"Endpoint": node_config.endpoint
|
|
}
|
|
|
|
output(basic_info, ctx.obj.get('output_format', 'table'), title=f"Node Information: {node_id}")
|
|
|
|
# Performance metrics
|
|
metrics = {
|
|
"CPU Usage": f"{node_info['cpu_usage']}%",
|
|
"Memory Usage": f"{node_info['memory_usage_mb']:.1f}MB",
|
|
"Disk Usage": f"{node_info['disk_usage_mb']:.1f}MB",
|
|
"Network In": f"{node_info['network_in_mb']:.1f}MB/s",
|
|
"Network Out": f"{node_info['network_out_mb']:.1f}MB/s"
|
|
}
|
|
|
|
output(metrics, ctx.obj.get('output_format', 'table'), title="Performance Metrics")
|
|
|
|
# Hosted chains
|
|
if node_info.get("hosted_chains"):
|
|
chains_data = [
|
|
{
|
|
"Chain ID": chain_id,
|
|
"Type": chain.get("type", "unknown"),
|
|
"Status": chain.get("status", "unknown")
|
|
}
|
|
for chain_id, chain in node_info["hosted_chains"].items()
|
|
]
|
|
|
|
output(chains_data, ctx.obj.get('output_format', 'table'), title="Hosted Chains")
|
|
|
|
except Exception as e:
|
|
error(f"Error getting node info: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.option('--show-private', is_flag=True, help='Show private chains')
|
|
@click.option('--node-id', help='Specific node ID to query')
|
|
@click.pass_context
|
|
def chains(ctx, show_private, node_id):
|
|
"""List chains hosted on all nodes"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
all_chains = []
|
|
|
|
import asyncio
|
|
|
|
async def get_all_chains():
|
|
tasks = []
|
|
for nid, node_config in config.nodes.items():
|
|
if node_id and nid != node_id:
|
|
continue
|
|
async def get_chains_for_node(nid, nconfig):
|
|
try:
|
|
async with NodeClient(nconfig) as client:
|
|
chains = await client.get_hosted_chains()
|
|
return [(nid, chain) for chain in chains]
|
|
except Exception as e:
|
|
print(f"Error getting chains from node {nid}: {e}")
|
|
return []
|
|
|
|
tasks.append(get_chains_for_node(node_id, node_config))
|
|
|
|
results = await asyncio.gather(*tasks)
|
|
for result in results:
|
|
all_chains.extend(result)
|
|
|
|
asyncio.run(get_all_chains())
|
|
|
|
if not all_chains:
|
|
output("No chains found on any node", ctx.obj.get('output_format', 'table'))
|
|
return
|
|
|
|
# Filter private chains if not requested
|
|
if not show_private:
|
|
all_chains = [(node_id, chain) for node_id, chain in all_chains
|
|
if chain.privacy.visibility != "private"]
|
|
|
|
# Format output
|
|
chains_data = [
|
|
{
|
|
"Node ID": node_id,
|
|
"Chain ID": chain.id,
|
|
"Type": chain.type.value,
|
|
"Purpose": chain.purpose,
|
|
"Name": chain.name,
|
|
"Status": chain.status.value,
|
|
"Block Height": chain.block_height,
|
|
"Size": f"{chain.size_mb:.1f}MB"
|
|
}
|
|
for node_id, chain in all_chains
|
|
]
|
|
|
|
output(chains_data, ctx.obj.get('output_format', 'table'), title="Chains by Node")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing chains: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
|
@click.pass_context
|
|
def list(ctx, format):
|
|
"""List all configured nodes"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if not config.nodes:
|
|
output("No nodes configured", ctx.obj.get('output_format', 'table'))
|
|
return
|
|
|
|
nodes_data = [
|
|
{
|
|
"Node ID": node_id,
|
|
"Endpoint": node_config.endpoint,
|
|
"Timeout": f"{node_config.timeout}s",
|
|
"Max Connections": node_config.max_connections,
|
|
"Retry Count": node_config.retry_count
|
|
}
|
|
for node_id, node_config in config.nodes.items()
|
|
]
|
|
|
|
output(nodes_data, ctx.obj.get('output_format', 'table'), title="Configured Nodes")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing nodes: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.argument('node_id')
|
|
@click.argument('endpoint')
|
|
@click.option('--timeout', default=30, help='Request timeout in seconds')
|
|
@click.option('--max-connections', default=10, help='Maximum concurrent connections')
|
|
@click.option('--retry-count', default=3, help='Number of retry attempts')
|
|
@click.pass_context
|
|
def add(ctx, node_id, endpoint, timeout, max_connections, retry_count):
|
|
"""Add a new node to configuration"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if node_id in config.nodes:
|
|
error(f"Node {node_id} already exists")
|
|
raise click.Abort()
|
|
|
|
node_config = get_default_node_config()
|
|
node_config.id = node_id
|
|
node_config.endpoint = endpoint
|
|
node_config.timeout = timeout
|
|
node_config.max_connections = max_connections
|
|
node_config.retry_count = retry_count
|
|
|
|
config = add_node_config(config, node_config)
|
|
|
|
from ..core.config import save_multichain_config
|
|
save_multichain_config(config)
|
|
|
|
success(f"Node {node_id} added successfully!")
|
|
|
|
result = {
|
|
"Node ID": node_id,
|
|
"Endpoint": endpoint,
|
|
"Timeout": f"{timeout}s",
|
|
"Max Connections": max_connections,
|
|
"Retry Count": retry_count
|
|
}
|
|
|
|
output(result, ctx.obj.get('output_format', 'table'))
|
|
|
|
except Exception as e:
|
|
error(f"Error adding node: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.argument('node_id')
|
|
@click.option('--force', is_flag=True, help='Force removal without confirmation')
|
|
@click.pass_context
|
|
def remove(ctx, node_id, force):
|
|
"""Remove a node from configuration"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if node_id not in config.nodes:
|
|
error(f"Node {node_id} not found")
|
|
raise click.Abort()
|
|
|
|
if not force:
|
|
# Show node information before removal
|
|
node_config = config.nodes[node_id]
|
|
node_info = {
|
|
"Node ID": node_id,
|
|
"Endpoint": node_config.endpoint,
|
|
"Timeout": f"{node_config.timeout}s",
|
|
"Max Connections": node_config.max_connections
|
|
}
|
|
|
|
output(node_info, ctx.obj.get('output_format', 'table'), title="Node to Remove")
|
|
|
|
if not click.confirm(f"Are you sure you want to remove node {node_id}?"):
|
|
raise click.Abort()
|
|
|
|
config = remove_node_config(config, node_id)
|
|
|
|
from ..core.config import save_multichain_config
|
|
save_multichain_config(config)
|
|
|
|
success(f"Node {node_id} removed successfully!")
|
|
|
|
except Exception as e:
|
|
error(f"Error removing node: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.argument('node_id')
|
|
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
|
|
@click.option('--interval', default=5, help='Update interval in seconds')
|
|
@click.pass_context
|
|
def monitor(ctx, node_id, realtime, interval):
|
|
"""Monitor node activity"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if node_id not in config.nodes:
|
|
error(f"Node {node_id} not found")
|
|
raise click.Abort()
|
|
|
|
node_config = config.nodes[node_id]
|
|
|
|
import asyncio
|
|
from rich.console import Console
|
|
from rich.layout import Layout
|
|
from rich.live import Live
|
|
import time
|
|
|
|
console = Console()
|
|
|
|
async def get_node_stats():
|
|
async with NodeClient(node_config) as client:
|
|
node_info = await client.get_node_info()
|
|
return node_info
|
|
|
|
if realtime:
|
|
# Real-time monitoring
|
|
def generate_monitor_layout():
|
|
try:
|
|
node_info = asyncio.run(get_node_stats())
|
|
|
|
layout = Layout()
|
|
layout.split_column(
|
|
Layout(name="header", size=3),
|
|
Layout(name="metrics"),
|
|
Layout(name="chains", size=10)
|
|
)
|
|
|
|
# Header
|
|
layout["header"].update(
|
|
f"Node Monitor: {node_id} - {node_info['status'].upper()}"
|
|
)
|
|
|
|
# Metrics table
|
|
metrics_data = [
|
|
["CPU Usage", f"{node_info['cpu_usage']}%"],
|
|
["Memory Usage", f"{node_info['memory_usage_mb']:.1f}MB"],
|
|
["Disk Usage", f"{node_info['disk_usage_mb']:.1f}MB"],
|
|
["Network In", f"{node_info['network_in_mb']:.1f}MB/s"],
|
|
["Network Out", f"{node_info['network_out_mb']:.1f}MB/s"],
|
|
["Uptime", f"{node_info['uptime_days']}d {node_info['uptime_hours']}h"]
|
|
]
|
|
|
|
layout["metrics"].update(str(metrics_data))
|
|
|
|
# Chains info
|
|
if node_info.get("hosted_chains"):
|
|
chains_text = f"Hosted Chains: {len(node_info['hosted_chains'])}\n"
|
|
for chain_id, chain in list(node_info["hosted_chains"].items())[:5]:
|
|
chains_text += f" • {chain_id} ({chain.get('status', 'unknown')})\n"
|
|
layout["chains"].update(chains_text)
|
|
else:
|
|
layout["chains"].update("No chains hosted")
|
|
|
|
return layout
|
|
except Exception as e:
|
|
return f"Error getting node stats: {e}"
|
|
|
|
with Live(generate_monitor_layout(), refresh_per_second=1) as live:
|
|
try:
|
|
while True:
|
|
live.update(generate_monitor_layout())
|
|
time.sleep(interval)
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
|
|
else:
|
|
# Single snapshot
|
|
node_info = asyncio.run(get_node_stats())
|
|
|
|
stats_data = [
|
|
{
|
|
"Metric": "CPU Usage",
|
|
"Value": f"{node_info['cpu_usage']}%"
|
|
},
|
|
{
|
|
"Metric": "Memory Usage",
|
|
"Value": f"{node_info['memory_usage_mb']:.1f}MB"
|
|
},
|
|
{
|
|
"Metric": "Disk Usage",
|
|
"Value": f"{node_info['disk_usage_mb']:.1f}MB"
|
|
},
|
|
{
|
|
"Metric": "Network In",
|
|
"Value": f"{node_info['network_in_mb']:.1f}MB/s"
|
|
},
|
|
{
|
|
"Metric": "Network Out",
|
|
"Value": f"{node_info['network_out_mb']:.1f}MB/s"
|
|
},
|
|
{
|
|
"Metric": "Uptime",
|
|
"Value": f"{node_info['uptime_days']}d {node_info['uptime_hours']}h"
|
|
}
|
|
]
|
|
|
|
output(stats_data, ctx.obj.get('output_format', 'table'), title=f"Node Statistics: {node_id}")
|
|
|
|
except Exception as e:
|
|
error(f"Error during monitoring: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@node.command()
|
|
@click.argument('node_id')
|
|
@click.pass_context
|
|
def test(ctx, node_id):
|
|
"""Test connectivity to a node"""
|
|
try:
|
|
config = load_multichain_config()
|
|
|
|
if node_id not in config.nodes:
|
|
error(f"Node {node_id} not found")
|
|
raise click.Abort()
|
|
|
|
node_config = config.nodes[node_id]
|
|
|
|
import asyncio
|
|
|
|
async def test_node():
|
|
try:
|
|
async with NodeClient(node_config) as client:
|
|
node_info = await client.get_node_info()
|
|
chains = await client.get_hosted_chains()
|
|
|
|
return {
|
|
"connected": True,
|
|
"node_id": node_info["node_id"],
|
|
"status": node_info["status"],
|
|
"version": node_info["version"],
|
|
"chains_count": len(chains)
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"connected": False,
|
|
"error": str(e)
|
|
}
|
|
|
|
result = asyncio.run(test_node())
|
|
|
|
if result["connected"]:
|
|
success(f"Successfully connected to node {node_id}!")
|
|
|
|
test_data = [
|
|
{
|
|
"Test": "Connection",
|
|
"Status": "✓ Pass"
|
|
},
|
|
{
|
|
"Test": "Node ID",
|
|
"Status": result["node_id"]
|
|
},
|
|
{
|
|
"Test": "Status",
|
|
"Status": result["status"]
|
|
},
|
|
{
|
|
"Test": "Version",
|
|
"Status": result["version"]
|
|
},
|
|
{
|
|
"Test": "Chains",
|
|
"Status": f"{result['chains_count']} hosted"
|
|
}
|
|
]
|
|
|
|
output(test_data, ctx.obj.get('output_format', 'table'), title=f"Node Test Results: {node_id}")
|
|
else:
|
|
error(f"Failed to connect to node {node_id}: {result['error']}")
|
|
raise click.Abort()
|
|
|
|
except Exception as e:
|
|
error(f"Error testing node: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
# Island management commands
|
|
@node.group()
|
|
def island():
|
|
"""Island management commands for federated mesh"""
|
|
pass
|
|
|
|
@island.command()
|
|
@click.option('--island-id', help='Island ID (UUID), generates new if not provided')
|
|
@click.option('--island-name', default='default', help='Human-readable island name')
|
|
@click.option('--chain-id', help='Chain ID for this island')
|
|
@click.pass_context
|
|
def create(ctx, island_id, island_name, chain_id):
|
|
"""Create a new island"""
|
|
try:
|
|
if not island_id:
|
|
island_id = str(uuid.uuid4())
|
|
|
|
if not chain_id:
|
|
chain_id = f"ait-{island_id[:8]}"
|
|
|
|
island_info = {
|
|
"Island ID": island_id,
|
|
"Island Name": island_name,
|
|
"Chain ID": chain_id,
|
|
"Created": "Now"
|
|
}
|
|
|
|
output(island_info, ctx.obj.get('output_format', 'table'), title="New Island Created")
|
|
success(f"Island {island_name} ({island_id}) created successfully")
|
|
|
|
# Note: In a real implementation, this would update the configuration
|
|
# and notify the island manager
|
|
|
|
except Exception as e:
|
|
error(f"Error creating island: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@island.command()
|
|
@click.argument('island_id')
|
|
@click.argument('island_name')
|
|
@click.argument('chain_id')
|
|
@click.option('--hub', default='hub.aitbc.bubuit.net', help='Hub domain name to connect to')
|
|
@click.option('--is-hub', is_flag=True, help='Register this node as a hub for the island')
|
|
@click.pass_context
|
|
def join(ctx, island_id, island_name, chain_id, hub, is_hub):
|
|
"""Join an existing island"""
|
|
try:
|
|
# Get system hostname
|
|
hostname = socket.gethostname()
|
|
|
|
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
|
|
from aitbc_chain.config import settings as chain_settings
|
|
|
|
# Get public key from keystore
|
|
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
|
|
public_key_pem = None
|
|
|
|
if os.path.exists(keystore_path):
|
|
with open(keystore_path, 'r') as f:
|
|
keys = json.load(f)
|
|
# Get first key's public key
|
|
for key_id, key_data in keys.items():
|
|
public_key_pem = key_data.get('public_key_pem')
|
|
break
|
|
else:
|
|
error(f"Keystore not found at {keystore_path}")
|
|
raise click.Abort()
|
|
|
|
if not public_key_pem:
|
|
error("No public key found in keystore")
|
|
raise click.Abort()
|
|
|
|
# Generate node_id using hostname-based method
|
|
local_address = socket.gethostbyname(hostname)
|
|
local_port = chain_settings.p2p_bind_port
|
|
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
|
|
node_id = hashlib.sha256(content.encode()).hexdigest()
|
|
|
|
# Resolve hub domain to IP
|
|
hub_ip = socket.gethostbyname(hub)
|
|
hub_port = chain_settings.p2p_bind_port
|
|
|
|
click.echo(f"Connecting to hub {hub} ({hub_ip}:{hub_port})...")
|
|
|
|
# Create P2P network service instance for sending join request
|
|
from aitbc_chain.p2p_network import P2PNetworkService
|
|
|
|
# Create a minimal P2P service just for sending the join request
|
|
p2p_service = P2PNetworkService(
|
|
local_address,
|
|
local_port,
|
|
node_id,
|
|
"",
|
|
island_id=island_id,
|
|
island_name=island_name,
|
|
is_hub=is_hub,
|
|
island_chain_id=chain_id or chain_settings.island_chain_id or chain_settings.chain_id,
|
|
)
|
|
|
|
# Send join request
|
|
async def send_join():
|
|
return await p2p_service.send_join_request(
|
|
hub_ip, hub_port, island_id, island_name, node_id, public_key_pem
|
|
)
|
|
|
|
response = asyncio.run(send_join())
|
|
|
|
if response:
|
|
# Store credentials locally
|
|
credentials_path = '/var/lib/aitbc/island_credentials.json'
|
|
credentials_data = {
|
|
"island_id": response.get('island_id'),
|
|
"island_name": response.get('island_name'),
|
|
"island_chain_id": response.get('island_chain_id'),
|
|
"credentials": response.get('credentials'),
|
|
"joined_at": datetime.now().isoformat()
|
|
}
|
|
|
|
with open(credentials_path, 'w') as f:
|
|
json.dump(credentials_data, f, indent=2)
|
|
|
|
# Display join info
|
|
join_info = {
|
|
"Island ID": response.get('island_id'),
|
|
"Island Name": response.get('island_name'),
|
|
"Chain ID": response.get('island_chain_id'),
|
|
"Member Count": len(response.get('members', [])),
|
|
"Credentials Stored": credentials_path
|
|
}
|
|
|
|
output(join_info, ctx.obj.get('output_format', 'table'), title=f"Joined Island: {island_name}")
|
|
|
|
# Display member list
|
|
members = response.get('members', [])
|
|
if members:
|
|
output(members, ctx.obj.get('output_format', 'table'), title="Island Members")
|
|
|
|
# Display credentials
|
|
credentials = response.get('credentials', {})
|
|
if credentials:
|
|
output(credentials, ctx.obj.get('output_format', 'table'), title="Blockchain Credentials")
|
|
|
|
success(f"Successfully joined island {island_name}")
|
|
|
|
# If registering as hub
|
|
if is_hub:
|
|
click.echo("Registering as hub...")
|
|
# Hub registration would happen here via the hub register command
|
|
click.echo("Run 'aitbc node hub register' to complete hub registration")
|
|
else:
|
|
error("Failed to join island - no response from hub")
|
|
raise click.Abort()
|
|
|
|
except Exception as e:
|
|
error(f"Error joining island: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@island.command()
|
|
@click.argument('island_id')
|
|
@click.pass_context
|
|
def leave(ctx, island_id):
|
|
"""Leave an island"""
|
|
try:
|
|
success(f"Successfully left island {island_id}")
|
|
|
|
# Note: In a real implementation, this would update the island manager
|
|
|
|
except Exception as e:
|
|
error(f"Error leaving island: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@island.command()
|
|
@click.pass_context
|
|
def list(ctx):
|
|
"""List all known islands"""
|
|
try:
|
|
# Note: In a real implementation, this would query the island manager
|
|
islands = [
|
|
{
|
|
"Island ID": "550e8400-e29b-41d4-a716-446655440000",
|
|
"Island Name": "default",
|
|
"Chain ID": "ait-island-default",
|
|
"Status": "Active",
|
|
"Peer Count": "3"
|
|
}
|
|
]
|
|
|
|
output(islands, ctx.obj.get('output_format', 'table'), title="Known Islands")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing islands: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@island.command()
|
|
@click.argument('island_id')
|
|
@click.pass_context
|
|
def info(ctx, island_id):
|
|
"""Show information about a specific island"""
|
|
try:
|
|
# Note: In a real implementation, this would query the island manager
|
|
island_info = {
|
|
"Island ID": island_id,
|
|
"Island Name": "default",
|
|
"Chain ID": "ait-island-default",
|
|
"Status": "Active",
|
|
"Peer Count": "3",
|
|
"Hub Count": "1"
|
|
}
|
|
|
|
output(island_info, ctx.obj.get('output_format', 'table'), title=f"Island Information: {island_id}")
|
|
|
|
except Exception as e:
|
|
error(f"Error getting island info: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
# Hub management commands
|
|
@node.group()
|
|
def hub():
|
|
"""Hub management commands for federated mesh"""
|
|
pass
|
|
|
|
@hub.command()
|
|
@click.option('--public-address', help='Public IP address')
|
|
@click.option('--public-port', type=int, help='Public port')
|
|
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
|
|
@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL')
|
|
@click.pass_context
|
|
def register(ctx, public_address, public_port, redis_url, hub_discovery_url):
|
|
"""Register this node as a hub"""
|
|
try:
|
|
# Get environment variables
|
|
island_id = os.getenv('ISLAND_ID', 'default-island-id')
|
|
island_name = os.getenv('ISLAND_NAME', 'default')
|
|
|
|
# Get system hostname
|
|
hostname = socket.gethostname()
|
|
|
|
# Get public key from keystore
|
|
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
|
|
public_key_pem = None
|
|
|
|
if os.path.exists(keystore_path):
|
|
with open(keystore_path, 'r') as f:
|
|
keys = json.load(f)
|
|
# Get first key's public key
|
|
for key_id, key_data in keys.items():
|
|
public_key_pem = key_data.get('public_key_pem')
|
|
break
|
|
else:
|
|
error(f"Keystore not found at {keystore_path}")
|
|
raise click.Abort()
|
|
|
|
if not public_key_pem:
|
|
error("No public key found in keystore")
|
|
raise click.Abort()
|
|
|
|
# Generate node_id using hostname-based method
|
|
local_address = socket.gethostbyname(hostname)
|
|
local_port = 7070 # Default hub port
|
|
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
|
|
node_id = hashlib.sha256(content.encode()).hexdigest()
|
|
|
|
# Create HubManager instance
|
|
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
|
|
from aitbc_chain.network.hub_manager import HubManager
|
|
from aitbc_chain.network.hub_discovery import HubDiscovery
|
|
|
|
hub_manager = HubManager(
|
|
node_id,
|
|
local_address,
|
|
local_port,
|
|
island_id,
|
|
island_name,
|
|
redis_url
|
|
)
|
|
|
|
# Register as hub (async)
|
|
async def register_hub():
|
|
success = await hub_manager.register_as_hub(public_address, public_port)
|
|
if success:
|
|
# Register with DNS discovery service
|
|
hub_discovery = HubDiscovery(hub_discovery_url, local_port)
|
|
hub_info_dict = {
|
|
"node_id": node_id,
|
|
"address": local_address,
|
|
"port": local_port,
|
|
"island_id": island_id,
|
|
"island_name": island_name,
|
|
"public_address": public_address,
|
|
"public_port": public_port,
|
|
"public_key_pem": public_key_pem
|
|
}
|
|
dns_success = await hub_discovery.register_hub(hub_info_dict)
|
|
return success and dns_success
|
|
return False
|
|
|
|
result = asyncio.run(register_hub())
|
|
|
|
if result:
|
|
hub_info = {
|
|
"Node ID": node_id,
|
|
"Hostname": hostname,
|
|
"Address": local_address,
|
|
"Port": local_port,
|
|
"Island ID": island_id,
|
|
"Island Name": island_name,
|
|
"Public Address": public_address or "auto-discovered",
|
|
"Public Port": public_port or "auto-discovered",
|
|
"Status": "Registered"
|
|
}
|
|
|
|
output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Registration")
|
|
success("Successfully registered as hub")
|
|
else:
|
|
error("Failed to register as hub")
|
|
raise click.Abort()
|
|
|
|
except Exception as e:
|
|
error(f"Error registering as hub: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@hub.command()
|
|
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
|
|
@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL')
|
|
@click.pass_context
|
|
def unregister(ctx, redis_url, hub_discovery_url):
|
|
"""Unregister this node as a hub"""
|
|
try:
|
|
# Get environment variables
|
|
island_id = os.getenv('ISLAND_ID', 'default-island-id')
|
|
island_name = os.getenv('ISLAND_NAME', 'default')
|
|
|
|
# Get system hostname
|
|
hostname = socket.gethostname()
|
|
|
|
# Get public key from keystore
|
|
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
|
|
public_key_pem = None
|
|
|
|
if os.path.exists(keystore_path):
|
|
with open(keystore_path, 'r') as f:
|
|
keys = json.load(f)
|
|
# Get first key's public key
|
|
for key_id, key_data in keys.items():
|
|
public_key_pem = key_data.get('public_key_pem')
|
|
break
|
|
else:
|
|
error(f"Keystore not found at {keystore_path}")
|
|
raise click.Abort()
|
|
|
|
if not public_key_pem:
|
|
error("No public key found in keystore")
|
|
raise click.Abort()
|
|
|
|
# Generate node_id using hostname-based method
|
|
local_address = socket.gethostbyname(hostname)
|
|
local_port = 7070 # Default hub port
|
|
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
|
|
node_id = hashlib.sha256(content.encode()).hexdigest()
|
|
|
|
# Create HubManager instance
|
|
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
|
|
from aitbc_chain.network.hub_manager import HubManager
|
|
from aitbc_chain.network.hub_discovery import HubDiscovery
|
|
|
|
hub_manager = HubManager(
|
|
node_id,
|
|
local_address,
|
|
local_port,
|
|
island_id,
|
|
island_name,
|
|
redis_url
|
|
)
|
|
|
|
# Unregister as hub (async)
|
|
async def unregister_hub():
|
|
success = await hub_manager.unregister_as_hub()
|
|
if success:
|
|
# Unregister from DNS discovery service
|
|
hub_discovery = HubDiscovery(hub_discovery_url, local_port)
|
|
dns_success = await hub_discovery.unregister_hub(node_id)
|
|
return success and dns_success
|
|
return False
|
|
|
|
result = asyncio.run(unregister_hub())
|
|
|
|
if result:
|
|
hub_info = {
|
|
"Node ID": node_id,
|
|
"Status": "Unregistered"
|
|
}
|
|
|
|
output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Unregistration")
|
|
success("Successfully unregistered as hub")
|
|
else:
|
|
error("Failed to unregister as hub")
|
|
raise click.Abort()
|
|
|
|
except Exception as e:
|
|
error(f"Error unregistering as hub: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@hub.command()
|
|
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
|
|
@click.pass_context
|
|
def list(ctx, redis_url):
|
|
"""List registered hubs from Redis"""
|
|
try:
|
|
import redis.asyncio as redis
|
|
|
|
async def list_hubs():
|
|
hubs = []
|
|
try:
|
|
r = redis.from_url(redis_url)
|
|
# Get all hub keys
|
|
keys = await r.keys("hub:*")
|
|
for key in keys:
|
|
value = await r.get(key)
|
|
if value:
|
|
hub_data = json.loads(value)
|
|
hubs.append({
|
|
"Node ID": hub_data.get("node_id"),
|
|
"Address": hub_data.get("address"),
|
|
"Port": hub_data.get("port"),
|
|
"Island ID": hub_data.get("island_id"),
|
|
"Island Name": hub_data.get("island_name"),
|
|
"Public Address": hub_data.get("public_address", "N/A"),
|
|
"Public Port": hub_data.get("public_port", "N/A"),
|
|
"Peer Count": hub_data.get("peer_count", 0)
|
|
})
|
|
await r.close()
|
|
except Exception as e:
|
|
error(f"Failed to query Redis: {e}")
|
|
return []
|
|
return hubs
|
|
|
|
hubs = asyncio.run(list_hubs())
|
|
|
|
if hubs:
|
|
output(hubs, ctx.obj.get('output_format', 'table'), title="Registered Hubs")
|
|
else:
|
|
info("No registered hubs found")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing hubs: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
# Bridge management commands
|
|
@node.group()
|
|
def bridge():
|
|
"""Bridge management commands for federated mesh"""
|
|
pass
|
|
|
|
@bridge.command()
|
|
@click.argument('target_island_id')
|
|
@click.pass_context
|
|
def request(ctx, target_island_id):
|
|
"""Request a bridge to another island"""
|
|
try:
|
|
success(f"Bridge request sent to island {target_island_id}")
|
|
|
|
# Note: In a real implementation, this would use the bridge manager
|
|
|
|
except Exception as e:
|
|
error(f"Error requesting bridge: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@bridge.command()
|
|
@click.argument('request_id')
|
|
@click.argument('approving_node_id')
|
|
@click.pass_context
|
|
def approve(ctx, request_id, approving_node_id):
|
|
"""Approve a bridge request"""
|
|
try:
|
|
success(f"Bridge request {request_id} approved")
|
|
|
|
# Note: In a real implementation, this would use the bridge manager
|
|
|
|
except Exception as e:
|
|
error(f"Error approving bridge request: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@bridge.command()
|
|
@click.argument('request_id')
|
|
@click.option('--reason', help='Rejection reason')
|
|
@click.pass_context
|
|
def reject(ctx, request_id, reason):
|
|
"""Reject a bridge request"""
|
|
try:
|
|
success(f"Bridge request {request_id} rejected")
|
|
|
|
# Note: In a real implementation, this would use the bridge manager
|
|
|
|
except Exception as e:
|
|
error(f"Error rejecting bridge request: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@bridge.command()
|
|
@click.pass_context
|
|
def list(ctx):
|
|
"""List bridge connections"""
|
|
try:
|
|
# Note: In a real implementation, this would query the bridge manager
|
|
bridges = [
|
|
{
|
|
"Bridge ID": "bridge-1",
|
|
"Source Island": "island-a",
|
|
"Target Island": "island-b",
|
|
"Status": "Active"
|
|
}
|
|
]
|
|
|
|
output(bridges, ctx.obj.get('output_format', 'table'), title="Bridge Connections")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing bridges: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
# Multi-chain management commands
|
|
@node.group()
|
|
def chain():
|
|
"""Multi-chain management commands for parallel chains"""
|
|
pass
|
|
|
|
@chain.command()
|
|
@click.argument('chain_id')
|
|
@click.option('--chain-type', type=click.Choice(['bilateral', 'micro']), default='micro', help='Chain type')
|
|
@click.pass_context
|
|
def start(ctx, chain_id, chain_type):
|
|
"""Start a new parallel chain instance"""
|
|
try:
|
|
chain_info = {
|
|
"Chain ID": chain_id,
|
|
"Chain Type": chain_type,
|
|
"Status": "Starting",
|
|
"RPC Port": "auto-allocated",
|
|
"P2P Port": "auto-allocated"
|
|
}
|
|
|
|
output(chain_info, ctx.obj.get('output_format', 'table'), title=f"Starting Chain: {chain_id}")
|
|
success(f"Chain {chain_id} started successfully")
|
|
|
|
# Note: In a real implementation, this would use the multi-chain manager
|
|
|
|
except Exception as e:
|
|
error(f"Error starting chain: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@chain.command()
|
|
@click.argument('chain_id')
|
|
@click.pass_context
|
|
def stop(ctx, chain_id):
|
|
"""Stop a parallel chain instance"""
|
|
try:
|
|
success(f"Chain {chain_id} stopped successfully")
|
|
|
|
# Note: In a real implementation, this would use the multi-chain manager
|
|
|
|
except Exception as e:
|
|
error(f"Error stopping chain: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@chain.command()
|
|
@click.pass_context
|
|
def list(ctx):
|
|
"""List all active chain instances"""
|
|
try:
|
|
# Note: In a real implementation, this would query the multi-chain manager
|
|
chains = [
|
|
{
|
|
"Chain ID": "ait-mainnet",
|
|
"Chain Type": "default",
|
|
"Status": "Running",
|
|
"RPC Port": 8000,
|
|
"P2P Port": 7070
|
|
}
|
|
]
|
|
|
|
output(chains, ctx.obj.get('output_format', 'table'), title="Active Chains")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing chains: {str(e)}")
|
|
raise click.Abort()
|