mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-11 09:58:49 +00:00
feat: add RQLite command support and help documentation
- Introduced a new RQLite command in the CLI to handle RQLite-related operations. - Implemented the 'fix' subcommand to automatically repair common RQLite cluster issues, including correcting misconfigured join addresses and cleaning stale raft state. - Updated help documentation to include RQLite commands and their usage.
This commit is contained in:
parent
ed7f4ae3d9
commit
30d18aca02
@ -1,5 +1,23 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get the directory where this hook is located
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||||
|
CHANGELOG_SCRIPT="$REPO_ROOT/scripts/update_changelog.sh"
|
||||||
|
|
||||||
|
# Update changelog before push
|
||||||
|
if [ -f "$CHANGELOG_SCRIPT" ]; then
|
||||||
|
echo -e "\nUpdating changelog..."
|
||||||
|
bash "$CHANGELOG_SCRIPT"
|
||||||
|
changelog_status=$?
|
||||||
|
if [ $changelog_status -ne 0 ]; then
|
||||||
|
echo "Push aborted: changelog update failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: changelog update script not found at $CHANGELOG_SCRIPT"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\nRunning tests:"
|
echo -e "\nRunning tests:"
|
||||||
go test ./... # Runs all tests in your repo
|
go test ./... # Runs all tests in your repo
|
||||||
status=$?
|
status=$?
|
||||||
|
|||||||
@ -108,6 +108,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
cli.HandleConnectCommand(args[0], timeout)
|
cli.HandleConnectCommand(args[0], timeout)
|
||||||
|
|
||||||
|
// RQLite commands
|
||||||
|
case "rqlite":
|
||||||
|
cli.HandleRQLiteCommand(args)
|
||||||
|
|
||||||
// Help
|
// Help
|
||||||
case "help", "--help", "-h":
|
case "help", "--help", "-h":
|
||||||
showHelp()
|
showHelp()
|
||||||
@ -175,6 +179,9 @@ func showHelp() {
|
|||||||
fmt.Printf("🗄️ Database:\n")
|
fmt.Printf("🗄️ Database:\n")
|
||||||
fmt.Printf(" query <sql> 🔐 Execute database query\n\n")
|
fmt.Printf(" query <sql> 🔐 Execute database query\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("🔧 RQLite:\n")
|
||||||
|
fmt.Printf(" rqlite fix 🔧 Fix misconfigured join address and clean raft state\n\n")
|
||||||
|
|
||||||
fmt.Printf("📡 PubSub:\n")
|
fmt.Printf("📡 PubSub:\n")
|
||||||
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
||||||
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
||||||
|
|||||||
327
pkg/cli/rqlite_commands.go
Normal file
327
pkg/cli/rqlite_commands.go
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleRQLiteCommand handles rqlite-related commands
|
||||||
|
func HandleRQLiteCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
showRQLiteHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ RQLite commands are only supported on Linux\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
subargs := args[1:]
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case "fix":
|
||||||
|
handleRQLiteFix(subargs)
|
||||||
|
case "help":
|
||||||
|
showRQLiteHelp()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown rqlite subcommand: %s\n", subcommand)
|
||||||
|
showRQLiteHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showRQLiteHelp() {
|
||||||
|
fmt.Printf("🗄️ RQLite Commands\n\n")
|
||||||
|
fmt.Printf("Usage: network-cli rqlite <subcommand> [options]\n\n")
|
||||||
|
fmt.Printf("Subcommands:\n")
|
||||||
|
fmt.Printf(" fix - Fix misconfigured join address and clean stale raft state\n\n")
|
||||||
|
fmt.Printf("Description:\n")
|
||||||
|
fmt.Printf(" The 'fix' command automatically repairs common rqlite cluster issues:\n")
|
||||||
|
fmt.Printf(" - Corrects join address from HTTP port (5001) to Raft port (7001) if misconfigured\n")
|
||||||
|
fmt.Printf(" - Cleans stale raft state that prevents proper cluster formation\n")
|
||||||
|
fmt.Printf(" - Restarts the node service with corrected configuration\n\n")
|
||||||
|
fmt.Printf("Requirements:\n")
|
||||||
|
fmt.Printf(" - Must be run as root (use sudo)\n")
|
||||||
|
fmt.Printf(" - Only works on non-bootstrap nodes (nodes with join_address configured)\n")
|
||||||
|
fmt.Printf(" - Stops and restarts the debros-node service\n\n")
|
||||||
|
fmt.Printf("Examples:\n")
|
||||||
|
fmt.Printf(" sudo network-cli rqlite fix\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRQLiteFix(args []string) {
|
||||||
|
requireRoot()
|
||||||
|
|
||||||
|
// Parse optional flags
|
||||||
|
dryRun := false
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "--dry-run" || arg == "-n" {
|
||||||
|
dryRun = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("🔍 Dry-run mode - no changes will be made\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🔧 RQLite Cluster Repair\n\n")
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
configPath, err := config.DefaultPath("node.yaml")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to determine config path: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfigForRepair(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to load config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a bootstrap node
|
||||||
|
if cfg.Node.Type == "bootstrap" || cfg.Database.RQLiteJoinAddress == "" {
|
||||||
|
fmt.Printf("ℹ️ This is a bootstrap node (no join address configured)\n")
|
||||||
|
fmt.Printf(" Bootstrap nodes don't need repair - they are the cluster leader\n")
|
||||||
|
fmt.Printf(" Run this command on follower nodes instead\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
joinAddr := cfg.Database.RQLiteJoinAddress
|
||||||
|
|
||||||
|
// Check if join address needs fixing
|
||||||
|
needsConfigFix := needsFix(joinAddr, cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
|
||||||
|
var fixedAddr string
|
||||||
|
|
||||||
|
if needsConfigFix {
|
||||||
|
fmt.Printf("⚠️ Detected misconfigured join address: %s\n", joinAddr)
|
||||||
|
fmt.Printf(" Expected Raft port (%d) but found HTTP port (%d)\n", cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
|
||||||
|
|
||||||
|
// Extract host from join address
|
||||||
|
host, _, err := parseJoinAddress(joinAddr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to parse join address: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix the join address - rqlite expects Raft port for -join
|
||||||
|
fixedAddr = fmt.Sprintf("%s:%d", host, cfg.Database.RQLiteRaftPort)
|
||||||
|
fmt.Printf(" Corrected address: %s\n\n", fixedAddr)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Join address looks correct: %s\n", joinAddr)
|
||||||
|
fmt.Printf(" Will clean stale raft state to ensure proper cluster formation\n\n")
|
||||||
|
fixedAddr = joinAddr // No change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("🔍 Dry-run: Would clean raft state")
|
||||||
|
if needsConfigFix {
|
||||||
|
fmt.Printf(" and fix config")
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the service
|
||||||
|
fmt.Printf("⏹️ Stopping debros-node service...\n")
|
||||||
|
if err := stopService("debros-node"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to stop service: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Service stopped\n\n")
|
||||||
|
|
||||||
|
// Update config file if needed
|
||||||
|
if needsConfigFix {
|
||||||
|
fmt.Printf("📝 Updating configuration file...\n")
|
||||||
|
if err := updateConfigJoinAddress(configPath, fixedAddr); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to update config: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, " Service is stopped - please fix manually and restart\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Config updated: %s\n\n", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean raft state
|
||||||
|
fmt.Printf("🧹 Cleaning stale raft state...\n")
|
||||||
|
dataDir := expandDataDir(cfg.Node.DataDir)
|
||||||
|
raftDir := filepath.Join(dataDir, "rqlite", "raft")
|
||||||
|
if err := cleanRaftState(raftDir); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to clean raft state: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, " Continuing anyway - raft state may still exist\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Raft state cleaned\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the service
|
||||||
|
fmt.Printf("🚀 Restarting debros-node service...\n")
|
||||||
|
if err := startService("debros-node"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to start service: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, " Config has been fixed - please restart manually:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " sudo systemctl start debros-node\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Service started\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("✅ Repair complete!\n\n")
|
||||||
|
fmt.Printf("The node should now join the cluster correctly.\n")
|
||||||
|
fmt.Printf("Monitor logs with: sudo network-cli service logs node --follow\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfigForRepair(path string) (*config.Config, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open config file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var cfg config.Config
|
||||||
|
if err := config.DecodeStrict(file, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsFix(joinAddr string, raftPort int, httpPort int) bool {
|
||||||
|
if joinAddr == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove http:// or https:// prefix if present
|
||||||
|
addr := joinAddr
|
||||||
|
if strings.HasPrefix(addr, "http://") {
|
||||||
|
addr = strings.TrimPrefix(addr, "http://")
|
||||||
|
} else if strings.HasPrefix(addr, "https://") {
|
||||||
|
addr = strings.TrimPrefix(addr, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse host:port
|
||||||
|
_, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false // Can't parse, assume it's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if port matches HTTP port (incorrect - should be Raft port)
|
||||||
|
if port == fmt.Sprintf("%d", httpPort) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it matches Raft port, it's correct
|
||||||
|
if port == fmt.Sprintf("%d", raftPort) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown port - assume it's fine
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJoinAddress(joinAddr string) (host, port string, err error) {
|
||||||
|
// Remove http:// or https:// prefix if present
|
||||||
|
addr := joinAddr
|
||||||
|
if strings.HasPrefix(addr, "http://") {
|
||||||
|
addr = strings.TrimPrefix(addr, "http://")
|
||||||
|
} else if strings.HasPrefix(addr, "https://") {
|
||||||
|
addr = strings.TrimPrefix(addr, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err = net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("invalid join address format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return host, port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateConfigJoinAddress(configPath string, newJoinAddr string) error {
|
||||||
|
// Read the file
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse YAML into a generic map to preserve structure
|
||||||
|
var yamlData map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(data, &yamlData); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to database.rqlite_join_address
|
||||||
|
database, ok := yamlData["database"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("database section not found in config")
|
||||||
|
}
|
||||||
|
|
||||||
|
database["rqlite_join_address"] = newJoinAddr
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
updatedData, err := yaml.Marshal(yamlData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandDataDir(dataDir string) string {
|
||||||
|
expanded := os.ExpandEnv(dataDir)
|
||||||
|
if strings.HasPrefix(expanded, "~") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return expanded // Fallback to original
|
||||||
|
}
|
||||||
|
expanded = filepath.Join(home, expanded[1:])
|
||||||
|
}
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanRaftState(raftDir string) error {
|
||||||
|
if _, err := os.Stat(raftDir); os.IsNotExist(err) {
|
||||||
|
return nil // Directory doesn't exist, nothing to clean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove raft state files
|
||||||
|
filesToRemove := []string{
|
||||||
|
"peers.json",
|
||||||
|
"peers.json.backup",
|
||||||
|
"peers.info",
|
||||||
|
"raft.db",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range filesToRemove {
|
||||||
|
filePath := filepath.Join(raftDir, file)
|
||||||
|
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to remove %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopService(serviceName string) error {
|
||||||
|
cmd := exec.Command("systemctl", "stop", serviceName)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("systemctl stop failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(serviceName string) error {
|
||||||
|
cmd := exec.Command("systemctl", "start", serviceName)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("systemctl start failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -328,7 +328,6 @@ func (n *Node) startLibP2P() error {
|
|||||||
n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development")
|
n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development")
|
||||||
// Don't add NAT/AutoRelay options for localhost
|
// Don't add NAT/AutoRelay options for localhost
|
||||||
} else {
|
} else {
|
||||||
// Production: enable NAT traversal
|
|
||||||
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
|
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
libp2p.EnableNATService(),
|
libp2p.EnableNATService(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user