"""Node management commands for AITBC CLI""" import click from typing import Optional from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config from ..core.node_client import NodeClient from ..utils import output, error, success 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('--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, is_hub): """Join an existing island""" try: join_info = { "Island ID": island_id, "Island Name": island_name, "Chain ID": chain_id, "As Hub": is_hub } output(join_info, ctx.obj.get('output_format', 'table'), title=f"Joining Island: {island_name}") success(f"Successfully joined island {island_name}") # Note: In a real implementation, this would update the island manager 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.pass_context def register(ctx, public_address, public_port): """Register this node as a hub""" try: hub_info = { "Node ID": "local-node", "Status": "Registered", "Public Address": public_address or "auto-discovered", "Public Port": public_port or "auto-discovered" } output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Registration") success("Successfully registered as hub") # Note: In a real implementation, this would update the hub manager except Exception as e: error(f"Error registering as hub: {str(e)}") raise click.Abort() @hub.command() @click.pass_context def unregister(ctx): """Unregister this node as a hub""" try: success("Successfully unregistered as hub") # Note: In a real implementation, this would update the hub manager except Exception as e: error(f"Error unregistering as hub: {str(e)}") raise click.Abort() @hub.command() @click.pass_context def list(ctx): """List known hubs""" try: # Note: In a real implementation, this would query the hub manager hubs = [ { "Node ID": "hub-node-1", "Address": "10.1.1.1", "Port": 7070, "Island ID": "550e8400-e29b-41d4-a716-446655440000", "Peer Count": "5" } ] output(hubs, ctx.obj.get('output_format', 'table'), title="Known Hubs") 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()