diff --git a/.githooks/pre-push b/.githooks/pre-push index 5718cb0..0a0f281 100644 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,5 +1,23 @@ #!/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:" go test ./... # Runs all tests in your repo status=$? diff --git a/cmd/cli/main.go b/cmd/cli/main.go index e191e64..e396013 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -108,6 +108,10 @@ func main() { } cli.HandleConnectCommand(args[0], timeout) + // RQLite commands + case "rqlite": + cli.HandleRQLiteCommand(args) + // Help case "help", "--help", "-h": showHelp() @@ -175,6 +179,9 @@ func showHelp() { fmt.Printf("๐Ÿ—„๏ธ Database:\n") fmt.Printf(" query ๐Ÿ” 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 publish ๐Ÿ” Publish message\n") fmt.Printf(" pubsub subscribe ๐Ÿ” Subscribe to topic\n") diff --git a/pkg/cli/rqlite_commands.go b/pkg/cli/rqlite_commands.go new file mode 100644 index 0000000..b9961cb --- /dev/null +++ b/pkg/cli/rqlite_commands.go @@ -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 [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 +} diff --git a/pkg/node/node.go b/pkg/node/node.go index 7aa7824..af840e8 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -328,7 +328,6 @@ func (n *Node) startLibP2P() error { n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development") // Don't add NAT/AutoRelay options for localhost } else { - // Production: enable NAT traversal n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services") opts = append(opts, libp2p.EnableNATService(),