Files
aitbc/cli/aitbc_cli/commands/node.py
aitbc d72945f20c
Some checks failed
Systemd Sync / sync-systemd (push) Waiting to run
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
network: add hub registration, Redis persistence, and federated mesh join protocol
- Change default P2P port from 7070 to 8001 in config and .env.example
- Add redis_url configuration option for hub persistence (default: redis://localhost:6379)
- Implement DNS-based hub registration/unregistration via HTTPS API endpoints
- Add Redis persistence for hub registrations with 1-hour TTL
- Add island join request/response protocol with member list and blockchain credentials
- Add GPU marketplace tracking (offers, bids, providers) in hub manager
- Add
2026-04-13 11:47:34 +02:00

1026 lines
35 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
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
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('--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()
# 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 = 8001 # Default hub 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 = 8001 # Default hub port
info(f"Connecting to hub {hub} ({hub_ip}:{hub_port})...")
# Create P2P network service instance for sending join request
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
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, [])
# 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:
info("Registering as hub...")
# Hub registration would happen here via the hub register command
info("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()