From 7980450e2c6ec4f08a72f7543ef5794022d0ed2c Mon Sep 17 00:00:00 2001 From: Tyler Sturos <55340199+tjsturos@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:35:03 -0800 Subject: [PATCH] working node commands --- client/cmd/node/autoUpdate.go | 143 ++++++++++++----------- client/cmd/node/clean.go | 10 ++ client/cmd/node/config/assign-rewards.go | 43 +++++++ client/cmd/node/config/config.go | 57 ++++----- client/cmd/node/config/create-default.go | 91 --------------- client/cmd/node/config/create.go | 107 +++++++++++++++++ client/cmd/node/config/import.go | 10 +- client/cmd/node/config/set-default.go | 56 --------- client/cmd/node/config/set.go | 7 +- client/cmd/node/config/switch.go | 82 +++++++++++++ client/cmd/node/config/utils.go | 16 +++ client/cmd/node/install.go | 127 +++++++++++++++----- client/cmd/node/log/clean.go | 3 + client/cmd/node/log/log.go | 3 + client/cmd/node/node.go | 48 ++++---- client/cmd/node/service.go | 36 +++--- client/cmd/node/shared.go | 27 ++++- client/cmd/node/uninstall.go | 10 ++ client/cmd/node/update.go | 4 +- client/cmd/node/user.go | 112 ------------------ client/cmd/node/utils.go | 26 ----- client/cmd/root.go | 22 +--- client/utils/download.go | 42 ++++--- client/utils/signatures.go | 34 ------ client/utils/types.go | 8 +- 25 files changed, 579 insertions(+), 545 deletions(-) create mode 100644 client/cmd/node/clean.go create mode 100644 client/cmd/node/config/assign-rewards.go delete mode 100644 client/cmd/node/config/create-default.go create mode 100644 client/cmd/node/config/create.go delete mode 100644 client/cmd/node/config/set-default.go create mode 100644 client/cmd/node/config/switch.go create mode 100644 client/cmd/node/log/clean.go create mode 100644 client/cmd/node/log/log.go create mode 100644 client/cmd/node/uninstall.go delete mode 100644 client/cmd/node/user.go delete mode 100644 client/utils/signatures.go diff --git a/client/cmd/node/autoUpdate.go b/client/cmd/node/autoUpdate.go index 48b1e10..06816fa 100644 --- a/client/cmd/node/autoUpdate.go +++ b/client/cmd/node/autoUpdate.go @@ -13,11 +13,11 @@ import ( // autoUpdateCmd represents the command to setup automatic updates var autoUpdateCmd = &cobra.Command{ - Use: "auto-update [enable|disable]", + Use: "auto-update [enable|disable|status]", Short: "Setup automatic update checks", - Long: `Setup or remove a cron job to automatically check for Quilibrium node updates every 10 minutes. + Long: `Setup, remove, or check status of a cron job to automatically check for Quilibrium node updates every 10 minutes. -This command will create or remove a cron entry that runs 'qclient node update' every 10 minutes +This command will create, remove, or check a cron entry that runs 'qclient node update' every 10 minutes to check for and apply any available updates. Example: @@ -25,18 +25,23 @@ Example: qclient node auto-update enable # Remove automatic update checks - qclient node auto-update disable`, + qclient node auto-update disable + + # Check if automatic update is enabled + qclient node auto-update status`, Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 || (args[0] != "enable" && args[0] != "disable") { - fmt.Fprintf(os.Stderr, "Error: must specify either 'enable' or 'disable'\n") + if len(args) != 1 || (args[0] != "enable" && args[0] != "disable" && args[0] != "status") { + fmt.Fprintf(os.Stderr, "Error: must specify 'enable', 'disable', or 'status'\n") cmd.Help() return } if args[0] == "enable" { setupCronJob() - } else { + } else if args[0] == "disable" { removeCronJob() + } else if args[0] == "status" { + checkAutoUpdateStatus() } }, } @@ -137,39 +142,19 @@ func setupUnixCron(qclientPath string) { } // Check if our update command is already in crontab - if strings.Contains(currentCrontab, "### qclient managed cron tasks") && - strings.Contains(currentCrontab, "#### node auto-update") { + if strings.Contains(currentCrontab, "### qclient-auto-update") { fmt.Fprintf(os.Stdout, "Automatic update check is already configured in crontab\n") return } // Add new cron entry with indicators - var newCrontab string - - // If qclient section exists but node auto-update doesn't, we need to add it - if strings.Contains(currentCrontab, "### qclient managed cron tasks") && - strings.Contains(currentCrontab, "### end qclient managed cron tasks") { - // Insert node auto-update section before the end marker - parts := strings.Split(currentCrontab, "### end qclient managed cron tasks") - if len(parts) >= 2 { - newCrontab = parts[0] + - "#### node auto-update\n" + - cronExpression + "\n" + - "#### end node auto-update (DO NOT DELETE)\n\n" + - "### end qclient managed cron tasks" + parts[1] - } - } else { - // Add the entire section with markers - newCrontab = currentCrontab - if strings.TrimSpace(newCrontab) != "" && !strings.HasSuffix(newCrontab, "\n") { - newCrontab += "\n" - } - newCrontab += "\n### qclient managed cron tasks\n" + - "#### node auto-update\n" + - cronExpression + "\n" + - "#### end node auto-update (DO NOT DELETE)\n\n" + - "### end qclient managed cron tasks (DO NOT DELETE)\n" + newCrontab := currentCrontab + if strings.TrimSpace(newCrontab) != "" && !strings.HasSuffix(newCrontab, "\n") { + newCrontab += "\n" } + newCrontab += "### qclient-auto-update\n" + + cronExpression + "\n" + + "### end-qclient-auto-update\n" // Write to temporary file tempFile, err := os.CreateTemp("", "qclient-crontab") @@ -216,46 +201,25 @@ func removeUnixCron() { currentCrontab := string(checkOutput) // No crontab or doesn't contain our section - if currentCrontab == "" || - !strings.Contains(currentCrontab, "### qclient managed cron tasks") || - !strings.Contains(currentCrontab, "#### node auto-update") { + if currentCrontab == "" || !strings.Contains(currentCrontab, "### qclient-auto-update") { fmt.Fprintf(os.Stdout, "No auto-update job found in crontab\n") return } + // Remove our section + startMarker := "### qclient-auto-update" + endMarker := "### end-qclient-auto-update" + + startIdx := strings.Index(currentCrontab, startMarker) + endIdx := strings.Index(currentCrontab, endMarker) + var newCrontab string - - // If only node auto-update section exists, remove the whole qclient section - if !strings.Contains(currentCrontab, "#### end node auto-update") || - !strings.Contains(strings.Split(currentCrontab, "### end qclient managed cron tasks")[0], "####") || - strings.Count(strings.Split(currentCrontab, "### end qclient managed cron tasks")[0], "####") <= 2 { - // Remove entire qclient section - parts := strings.Split(currentCrontab, "### qclient managed cron tasks") - if len(parts) >= 2 { - endParts := strings.Split(parts[1], "### end qclient managed cron tasks") - if len(endParts) >= 2 { - newCrontab = parts[0] + endParts[1] - } else { - newCrontab = parts[0] - } - } else { - newCrontab = currentCrontab - } + if startIdx >= 0 && endIdx >= 0 { + endIdx += len(endMarker) + // Remove the section including markers + newCrontab = currentCrontab[:startIdx] + currentCrontab[endIdx:] } else { - // Remove just the auto-update section - startMarker := "#### node auto-update" - endMarker := "#### end node auto-update (DO NOT DELETE)" - - startIdx := strings.Index(currentCrontab, startMarker) - endIdx := strings.Index(currentCrontab, endMarker) - - if startIdx >= 0 && endIdx >= 0 { - endIdx += len(endMarker) - // Remove the section including markers - newCrontab = currentCrontab[:startIdx] + currentCrontab[endIdx:] - } else { - newCrontab = currentCrontab - } + newCrontab = currentCrontab } // Clean up any leftover double newlines @@ -287,3 +251,46 @@ func removeUnixCron() { fmt.Fprintf(os.Stdout, "Successfully removed auto-update cron job\n") } + +func checkAutoUpdateStatus() { + if !isCrontabInstalled() { + fmt.Fprintf(os.Stderr, "Error: crontab command not found\n") + fmt.Fprintf(os.Stdout, "Auto-update is not enabled (crontab not installed)\n") + return + } + + // Check existing crontab + checkCmd := exec.Command("crontab", "-l") + checkOutput, err := checkCmd.CombinedOutput() + + if err != nil { + fmt.Fprintf(os.Stdout, "Auto-update is not enabled (no crontab found)\n") + return + } + + currentCrontab := string(checkOutput) + + if strings.Contains(currentCrontab, "### qclient-auto-update") { + // Extract the cron expression + startMarker := "### qclient-auto-update" + endMarker := "### end-qclient-auto-update" + + startIdx := strings.Index(currentCrontab, startMarker) + len(startMarker) + endIdx := strings.Index(currentCrontab, endMarker) + + if startIdx >= 0 && endIdx >= 0 { + cronSection := currentCrontab[startIdx:endIdx] + cronLines := strings.Split(strings.TrimSpace(cronSection), "\n") + if len(cronLines) > 0 { + fmt.Fprintf(os.Stdout, "Auto-update is enabled.") + fmt.Fprintf(os.Stdout, "The installed schedule is: %s\n", strings.TrimSpace(cronLines[0])) + } else { + fmt.Fprintf(os.Stdout, "Auto-update is enabled\n") + } + } else { + fmt.Fprintf(os.Stdout, "Auto-update is enabled\n") + } + } else { + fmt.Fprintf(os.Stdout, "Auto-update is not enabled\n") + } +} diff --git a/client/cmd/node/clean.go b/client/cmd/node/clean.go new file mode 100644 index 0000000..425869e --- /dev/null +++ b/client/cmd/node/clean.go @@ -0,0 +1,10 @@ +// TODO: Implement a clean command that will remove old versions of the node, +// signatures, and logs +// qlient node clean +// qlient node clean --all (all logs, old node binaries and signatures) +// qlient node clean --logs (remove all logs) +// qlient node clean --node (remove all old node binary versions, including signatures) + +// to remove even current versions, they must run 'qclient node uninstall' + +package node diff --git a/client/cmd/node/config/assign-rewards.go b/client/cmd/node/config/assign-rewards.go new file mode 100644 index 0000000..f55b380 --- /dev/null +++ b/client/cmd/node/config/assign-rewards.go @@ -0,0 +1,43 @@ +package config + +// TODO: Implement a command to assign a config's rewards to a config's rewards address +// Should be able to assign to a specific address or by name of a config directory +// e.g. qclient node config assign-rewards my-config my-throwaway-config +// this finds the address from my-throwaway-config and assigns it to my-config +// qlient node config assign-rewards my-config --reset will reset the rewards address to the default address +// or +// qclient node config assign-rewards my-config --address 0x1234567890abcdef1234567890abcdef1234567890abcdef +// +// If no address is provided, the command will prompt for an address +// the prompt should prompt clearly the user for each part, asking if +// the user wants to use one of the config files as the source of the address +// or if they want to enter a new address manually +// if no configs are found locally, it should prompt the user to create a new config +// or import one + +// i.e. Which config do you want to re-assign rewards? +// (1) my-config +// (2) my-other-config +// (3) my-throwaway-config +// +// Enter the number of the config you want to re-assign rewards: 1 +// Finding address from my-config... +// Successfully found address 0x1234567890abcdef1234567890abcdef1234567890abcdef +// Which reward address do you want to assign to my-config? +// (1) my-other-config +// (2) my-throwaway-config +// (3) Enter a new address manually +// (4) Reset to default address +// +// Enter the number of the reward address you want to assign: 2 +// +// Finding address from my-throwaway-config... +// Successfully found address 0x1234567890abcdef1234567890abcdef1234567890abcdef +// Assigning rewards from my-config to my-throwaway-config +// Successfully assigned rewards. +// +// Summary: +// Node address: 0x1234567890abcdef1234567890abcdef1234567890abcdef +// Rewards address: 0x1234567890abcdef1234567890abcdef1234567890abcdef +// +// Successfully updated my-config diff --git a/client/cmd/node/config/config.go b/client/cmd/node/config/config.go index 9768aa8..6a7d655 100644 --- a/client/cmd/node/config/config.go +++ b/client/cmd/node/config/config.go @@ -1,11 +1,21 @@ package config import ( + "fmt" + "os" + "os/user" + "path/filepath" + "github.com/spf13/cobra" - clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" "source.quilibrium.com/quilibrium/monorepo/client/utils" ) +var ( + NodeUser *user.User + ConfigDirs string + NodeConfigToRun string +) + // ConfigCmd represents the node config command var ConfigCmd = &cobra.Command{ Use: "config", @@ -20,8 +30,15 @@ This command provides utilities for configuring your Quilibrium node, such as: `, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Check if the config directory exists - if utils.FileExists(clientNode.ConfigDirs) { - utils.ValidateAndCreateDir(clientNode.ConfigDirs, clientNode.NodeUser) + user, err := utils.GetCurrentSudoUser() + if err != nil { + fmt.Println("Error getting current user:", err) + os.Exit(1) + } + ConfigDirs = filepath.Join(user.HomeDir, ".quilibrium", "configs") + NodeConfigToRun = filepath.Join(user.HomeDir, ".quilibrium", "configs", "default") + if utils.FileExists(ConfigDirs) { + utils.ValidateAndCreateDir(ConfigDirs, user) } }, Run: func(cmd *cobra.Command, args []string) { @@ -29,36 +46,8 @@ This command provides utilities for configuring your Quilibrium node, such as: }, } -// GetConfigSubCommands returns all the configuration subcommands -func GetConfigSubCommands() []*cobra.Command { - // This function can be used by other packages to get all config subcommands - // It can be expanded in the future to return additional subcommands as they are added - - return []*cobra.Command{ - importCmd, - setCmd, - setDefaultCmd, - createDefaultCmd, - } -} - func init() { - // Add subcommands to the config command - // These subcommands will register themselves in their own init() functions - - // Register the config command to the node command - clientNode.NodeCmd.AddCommand(ConfigCmd) -} - -// GetRootConfigCmd returns the root config command -// This is a utility function that can be used by other packages to get the config command -// and its subcommands -func GetRootConfigCmd() *cobra.Command { - // Return the config command that is defined in import.go - return ConfigCmd -} - -// RegisterConfigCommand registers a subcommand to the root config command -func RegisterConfigCommand(cmd *cobra.Command) { - ConfigCmd.AddCommand(cmd) + ConfigCmd.AddCommand(importCmd) + ConfigCmd.AddCommand(SwitchConfigCmd) + ConfigCmd.AddCommand(createCmd) } diff --git a/client/cmd/node/config/create-default.go b/client/cmd/node/config/create-default.go deleted file mode 100644 index 80235ab..0000000 --- a/client/cmd/node/config/create-default.go +++ /dev/null @@ -1,91 +0,0 @@ -package config - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/spf13/cobra" - clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" - "source.quilibrium.com/quilibrium/monorepo/client/utils" -) - -var createDefaultCmd = &cobra.Command{ - Use: "create-default [name]", - Short: "Create a default configuration", - Long: `Create a default configuration by running quilibrium-node with --peer-id and ---config flags, then symlink it to the default configuration. - -Example: - qclient node config create-default - qclient node config create-default myconfig - -The first example will create a new configuration at ConfigsDir/default-config and symlink it to ConfigsDir/default. -The second example will create a new configuration at ConfigsDir/myconfig and symlink it to ConfigsDir/default.`, - Args: cobra.MaximumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - // Check if running as root - if os.Geteuid() != 0 { - fmt.Println("Error: This command requires root privileges.") - fmt.Println("Please run with sudo or as root.") - os.Exit(1) - } - // Determine the config name (default-config or user-provided) - configName := "default-config" - if len(args) > 0 { - configName = args[0] - - // Check if trying to use "default" which is reserved for the symlink - if configName == "default" { - fmt.Println("Error: 'default' is reserved for the symlink. Please use a different name.") - os.Exit(1) - } - } - - // Construct the configuration directory path - configDir := filepath.Join(clientNode.ConfigDirs, configName) - - // Create directory if it doesn't exist - if err := os.MkdirAll(configDir, 0755); err != nil { - fmt.Printf("Failed to create config directory: %s\n", err) - os.Exit(1) - } - - // Run quilibrium-node command to generate config - // this is a hack to get the config files to be created - // TODO: fix this - // to fix this, we need to extrapolate the Node's config and keystore construction - // and reuse it for this command - nodeCmd := exec.Command("sudo", "quilibrium-node", "--peer-id", "--config", configDir) - nodeCmd.Stdout = os.Stdout - nodeCmd.Stderr = os.Stderr - - fmt.Printf("Running quilibrium-node to generate configuration in %s...\n", configName) - if err := nodeCmd.Run(); err != nil { - fmt.Printf("Failed to run quilibrium-node: %s\n", err) - os.Exit(1) - } - - // Check if the configuration was created successfully - if !HasConfigFiles(configDir) { - fmt.Printf("Failed to generate configuration files in: %s\n", configDir) - os.Exit(1) - } - - // Construct the default directory path - defaultDir := filepath.Join(clientNode.ConfigDirs, "default") - - // Create the symlink - if err := utils.CreateSymlink(configDir, defaultDir); err != nil { - fmt.Printf("Failed to create symlink: %s\n", err) - os.Exit(1) - } - - fmt.Printf("Successfully created %s configuration and symlinked to default\n", configName) - }, -} - -func init() { - ConfigCmd.AddCommand(createDefaultCmd) -} diff --git a/client/cmd/node/config/create.go b/client/cmd/node/config/create.go new file mode 100644 index 0000000..079d106 --- /dev/null +++ b/client/cmd/node/config/create.go @@ -0,0 +1,107 @@ +package config + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/spf13/cobra" + "source.quilibrium.com/quilibrium/monorepo/client/utils" +) + +var setDefault bool + +var createCmd = &cobra.Command{ + Use: "create [name]", + Short: "Create a default configuration file set for a node", + Long: fmt.Sprintf(`Create a default configuration by running quilibrium-node with --peer-id and --config flags, then symlink it to the default configuration. + +Example: + qclient node config create + qclient node config create myconfig + + qclient node config create myconfig --default + +The first example will create a new configuration at %s/default-config. +The second example will create a new configuration at %s/myconfig. +The third example will create a new configuration at %s/myconfig and symlink it so the node will use it.`, + ConfigDirs, ConfigDirs, ConfigDirs), + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // Determine the config name (default-config or user-provided) + var configName string + if len(args) > 0 { + configName = args[0] + } else { + // Prompt for a name if none provided + fmt.Print("Enter a name for the configuration (cannot be 'default'): ") + fmt.Scanln(&configName) + + if configName == "" { + configName = "default-config" + } + } + + // Check if trying to use "default" which is reserved for the symlink + if configName == "default" { + fmt.Println("Error: 'default' is reserved for the symlink. Please use a different name.") + os.Exit(1) + } + + // Construct the configuration directory path + configDir := filepath.Join(ConfigDirs, configName) + + // Create directory if it doesn't exist + if err := utils.ValidateAndCreateDir(configDir, NodeUser); err != nil { + fmt.Printf("Failed to create config directory: %s\n", err) + os.Exit(1) + } + + // Run quilibrium-node command to generate config + // this is a hack to get the config files to be created + // TODO: fix this + // to fix this, we need to extrapolate the Node's config and keystore construction + // and reuse it for this command + nodeCmd := exec.Command("quilibrium-node", "--peer-id", "--config", configDir) + nodeCmd.Stdout = os.Stdout + nodeCmd.Stderr = os.Stderr + + fmt.Printf("Running quilibrium-node to generate configuration in %s...\n", configName) + if err := nodeCmd.Run(); err != nil { + // Check if the error is due to the command not being found + if exitErr, ok := err.(*exec.ExitError); ok { + fmt.Printf("Error running quilibrium-node: %s\n", exitErr) + } else if _, ok := err.(*exec.Error); ok { + fmt.Printf("Error: quilibrium-node command not found. Please ensure it is installed and in your PATH.\n") + } else { + fmt.Printf("Error: %s\n", err) + } + os.RemoveAll(configDir) + os.Exit(1) + } + + // Check if the configuration was created successfully + if !HasConfigFiles(configDir) { + fmt.Printf("Failed to generate configuration files in: %s\n", configDir) + os.Exit(1) + } + + if setDefault { + // Create the symlink + if err := utils.CreateSymlink(configDir, NodeConfigToRun); err != nil { + fmt.Printf("Failed to create symlink: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully created %s configuration and symlinked to default\n", configName) + } else { + fmt.Printf("Successfully created %s configuration\n", configName) + } + fmt.Println("The keys.yml file will only contain 'null:' until the node is started.") + }, +} + +func init() { + createCmd.Flags().BoolVarP(&setDefault, "default", "d", false, "Select this config as the default") +} diff --git a/client/cmd/node/config/import.go b/client/cmd/node/config/import.go index 90195b7..a1fdeba 100644 --- a/client/cmd/node/config/import.go +++ b/client/cmd/node/config/import.go @@ -6,7 +6,6 @@ import ( "path/filepath" "github.com/spf13/cobra" - clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" "source.quilibrium.com/quilibrium/monorepo/client/utils" ) @@ -36,8 +35,8 @@ This will copy config.yml and keys.yml from /path/to/source to /home/quilibrium/ } // Create target directory in the standard location - targetDir := filepath.Join(clientNode.ConfigDirs, name) - if err := os.MkdirAll(targetDir, 0755); err != nil { + targetDir := filepath.Join(ConfigDirs, name) + if err := utils.ValidateAndCreateDir(targetDir, NodeUser); err != nil { fmt.Printf("Failed to create target directory: %s\n", err) os.Exit(1) } @@ -63,8 +62,3 @@ This will copy config.yml and keys.yml from /path/to/source to /home/quilibrium/ fmt.Printf("Successfully imported config files to %s\n", targetDir) }, } - -func init() { - // Add the import command to the config command - ConfigCmd.AddCommand(importCmd) -} diff --git a/client/cmd/node/config/set-default.go b/client/cmd/node/config/set-default.go deleted file mode 100644 index 962128c..0000000 --- a/client/cmd/node/config/set-default.go +++ /dev/null @@ -1,56 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/spf13/cobra" - clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" - "source.quilibrium.com/quilibrium/monorepo/client/utils" -) - -var setDefaultCmd = &cobra.Command{ - Use: "set-default [name]", - Short: "Set a configuration as the default", - Long: `Set a configuration as the default by creating a symlink. - -Example: - qclient node config set-default mynode - -This will symlink /home/quilibrium/configs/mynode to /home/quilibrium/configs/default`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - name := args[0] - - // Construct the source directory path - sourceDir := filepath.Join(clientNode.ConfigDirs, name) - - // Check if source directory exists - if _, err := os.Stat(sourceDir); os.IsNotExist(err) { - fmt.Printf("Config directory does not exist: %s\n", sourceDir) - os.Exit(1) - } - - // Check if the source directory has both config.yml and keys.yml files - if !HasConfigFiles(sourceDir) { - fmt.Printf("Source directory does not contain both config.yml and keys.yml files: %s\n", sourceDir) - os.Exit(1) - } - - // Construct the default directory path - defaultDir := filepath.Join(clientNode.ConfigDirs, "default") - - // Create the symlink - if err := utils.CreateSymlink(sourceDir, defaultDir); err != nil { - fmt.Printf("Failed to create symlink: %s\n", err) - os.Exit(1) - } - - fmt.Printf("Successfully set %s as the default configuration\n", name) - }, -} - -func init() { - ConfigCmd.AddCommand(setDefaultCmd) -} diff --git a/client/cmd/node/config/set.go b/client/cmd/node/config/set.go index 1671284..fc0fd06 100644 --- a/client/cmd/node/config/set.go +++ b/client/cmd/node/config/set.go @@ -6,7 +6,6 @@ import ( "path/filepath" "github.com/spf13/cobra" - clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" "source.quilibrium.com/quilibrium/monorepo/node/config" ) @@ -25,7 +24,7 @@ Example: value := args[2] // Construct the config directory path - configDir := filepath.Join(clientNode.ConfigDirs, name) + configDir := filepath.Join(ConfigDirs, name) configFile := filepath.Join(configDir, "config.yml") // Check if config directory exists @@ -41,7 +40,7 @@ Example: } // Load the config - cfg, err := config.LoadConfig(configFile, "", false) + cfg, err := config.LoadConfig(configDir, "", false) if err != nil { fmt.Printf("Failed to load config: %s\n", err) os.Exit(1) @@ -73,7 +72,7 @@ Example: } // Save the updated config - if err := config.SaveConfig(configFile, cfg); err != nil { + if err := config.SaveConfig(configDir, cfg); err != nil { fmt.Printf("Failed to save config: %s\n", err) os.Exit(1) } diff --git a/client/cmd/node/config/switch.go b/client/cmd/node/config/switch.go new file mode 100644 index 0000000..ed7bfc4 --- /dev/null +++ b/client/cmd/node/config/switch.go @@ -0,0 +1,82 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "source.quilibrium.com/quilibrium/monorepo/client/utils" +) + +var SwitchConfigCmd = &cobra.Command{ + Use: "switch [name]", + Short: "Switch the config to be run by the node", + Long: fmt.Sprintf(`Switch the configuration to be run by the node by creating a symlink. + +Example: + qclient node config switch mynode + +This will symlink %s/mynode to %s`, ConfigDirs, NodeConfigToRun), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var name string + if len(args) > 0 { + name = args[0] + } else { + // List available configurations + configs, err := ListConfigurations() + if err != nil { + fmt.Printf("Error listing configurations: %s\n", err) + os.Exit(1) + } + + if len(configs) == 0 { + fmt.Println("No configurations found. Create one with 'qclient node config create'") + os.Exit(1) + } + + fmt.Println("Available configurations:") + for i, config := range configs { + fmt.Printf("%d. %s\n", i+1, config) + } + + // Prompt for choice + var choice int + fmt.Print("Enter the number of the configuration to set as default: ") + _, err = fmt.Scanf("%d", &choice) + if err != nil || choice < 1 || choice > len(configs) { + fmt.Println("Invalid choice. Please enter a valid number.") + os.Exit(1) + } + + name = configs[choice-1] + } + + // Construct the source directory path + sourceDir := filepath.Join(ConfigDirs, name) + + // Check if source directory exists + if _, err := os.Stat(sourceDir); os.IsNotExist(err) { + fmt.Printf("Config directory does not exist: %s\n", sourceDir) + os.Exit(1) + } + + // Check if the source directory has both config.yml and keys.yml files + if !HasConfigFiles(sourceDir) { + fmt.Printf("Source directory does not contain both config.yml and keys.yml files: %s\n", sourceDir) + os.Exit(1) + } + + // Construct the default directory path + defaultDir := filepath.Join(ConfigDirs, "default") + + // Create the symlink + if err := utils.CreateSymlink(sourceDir, defaultDir); err != nil { + fmt.Printf("Failed to create symlink: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully set %s as the default configuration\n", name) + }, +} diff --git a/client/cmd/node/config/utils.go b/client/cmd/node/config/utils.go index fd3b296..45f7839 100644 --- a/client/cmd/node/config/utils.go +++ b/client/cmd/node/config/utils.go @@ -24,3 +24,19 @@ func HasConfigFiles(dirPath string) bool { return configExists && keysExists } + +func ListConfigurations() ([]string, error) { + files, err := os.ReadDir(ConfigDirs) + if err != nil { + return nil, err + } + + configs := make([]string, 0) + for _, file := range files { + if file.IsDir() { + configs = append(configs, file.Name()) + } + } + + return configs, nil +} diff --git a/client/cmd/node/install.go b/client/cmd/node/install.go index c2f17b1..4c6a761 100644 --- a/client/cmd/node/install.go +++ b/client/cmd/node/install.go @@ -3,7 +3,6 @@ package node import ( "fmt" "os" - "os/user" "path/filepath" "github.com/spf13/cobra" @@ -14,19 +13,87 @@ import ( var installCmd = &cobra.Command{ Use: "install [version]", Short: "Install Quilibrium node", - Long: `Install Quilibrium node binary. -If no version is specified, the latest version will be installed. + Long: `Install Quilibrium node binary and create a service to run it. + ## Service Management + + You can start, stop, and restart the service with: + + qclient node service start + qclient node service stop + qclient node service restart + qclient node service status + qclient node service enable + qclient node service disable + + ### Mac OS Notes + + On Mac OS, the service is managed by launchd (installed by default) + + ### Linux Notes + + On Linux, the service is managed by systemd (will be installed by this command). + + ## Config Management + + A config directory will be created in the user's home directory at .quilibrium/configs/. + + To create a default config, run the following command: + + qclient node config create-default [name-for-config] + + or, you can import existing configs one at a timefrom a directory: + + qclient node config import [name-for-config] /path/to/src/config/dir/ + + You can then select the config to use when running the node with: + + qclient node set-default [name-for-config] + + ## Binary Management + Binaries and signatures are installed to /var/quilibrium/bin/node/[version]/ + + You can update the node binary with: + + qclient node update [version] + + ### Auto-update + + You can enable auto-update with: + + qclient node auto-update enable + + You can disable auto-update with: + + qclient node auto-update disable + + You can check the auto-update status with: + + qclient node auto-update status + + ## Log Management + Logging uses system logging with logrotate installed by default. + + Logs are installed to /var/log/quilibrium + + The logrotate config is installed to /etc/logrotate.d/quilibrium + + You can view the logs with: + + qclient node logs [version] + +When installing with this command, if no version is specified, the latest version will be installed. + +Sudo is required to install the node to install the node binary, logging,systemd (on Linux), and create the config directory. Examples: - # Install the latest version - qclient node install - # Install a specific version - qclient node install 2.1.0 + # Install the latest version + qclient node install - # Install without creating a dedicated user - qclient node install --no-create-user`, + # Install a specific version + qclient node install 2.1.0 +`, Args: cobra.RangeArgs(0, 1), Run: func(cmd *cobra.Command, args []string) { // Get system information and validate @@ -38,12 +105,34 @@ Examples: if !utils.IsSudo() { fmt.Println("This command must be run with sudo: sudo qclient node install") + fmt.Println("Sudo is required to install the node binary, logging, systemd (on Linux) service, and create the config directory.") os.Exit(1) } // Determine version to install version := determineVersion(args) + // Download and install the node + if version == "latest" { + latestVersion, err := utils.GetLatestVersion(utils.ReleaseTypeNode) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting latest version: %v\n", err) + return + } + + version = latestVersion + fmt.Fprintf(os.Stdout, "Found latest version: %s\n", version) + } + + // do a pre-flight check to ensure the release file exists + fileName := fmt.Sprintf("%s-%s-%s-%s", utils.ReleaseTypeNode, version, osType, arch) + url := fmt.Sprintf("%s/%s", utils.BaseReleaseURL, fileName) + + if !utils.DoesRemoteFileExist(url) { + fmt.Printf("The release file %s does not exist on the release server\n", fileName) + os.Exit(1) + } + fmt.Fprintf(os.Stdout, "Installing Quilibrium node for %s-%s, version: %s\n", osType, arch, version) // Install the node @@ -64,18 +153,6 @@ func installNode(version string) { return } - // Download and install the node - if version == "latest" { - latestVersion, err := utils.GetLatestVersion(utils.ReleaseTypeNode) - if err != nil { - fmt.Fprintf(os.Stderr, "Error getting latest version: %v\n", err) - return - } - - version = latestVersion - fmt.Fprintf(os.Stdout, "Found latest version: %s\n", version) - } - if IsExistingNodeVersion(version) { fmt.Fprintf(os.Stderr, "Error: Node version %s already exists\n", version) os.Exit(1) @@ -93,13 +170,9 @@ func installNode(version string) { // installByVersion installs a specific version of the Quilibrium node func installByVersion(version string) error { - // Create version-specific directory - user, err := user.Lookup(utils.DefaultNodeUser) - if err != nil { - os.Exit(1) - } + versionDir := filepath.Join(utils.NodeDataPath, version) - if err := utils.ValidateAndCreateDir(versionDir, user); err != nil { + if err := utils.ValidateAndCreateDir(versionDir, NodeUser); err != nil { return fmt.Errorf("failed to create version directory: %w", err) } diff --git a/client/cmd/node/log/clean.go b/client/cmd/node/log/clean.go new file mode 100644 index 0000000..101e149 --- /dev/null +++ b/client/cmd/node/log/clean.go @@ -0,0 +1,3 @@ +package log + +// TODO: Implement a log subcommand to clean the logs diff --git a/client/cmd/node/log/log.go b/client/cmd/node/log/log.go new file mode 100644 index 0000000..9310e83 --- /dev/null +++ b/client/cmd/node/log/log.go @@ -0,0 +1,3 @@ +package log + +// TODO: Implement a command to view and manage the logs diff --git a/client/cmd/node/node.go b/client/cmd/node/node.go index 6eebae3..120f610 100644 --- a/client/cmd/node/node.go +++ b/client/cmd/node/node.go @@ -4,8 +4,10 @@ import ( "fmt" "os" "os/user" + "path/filepath" "github.com/spf13/cobra" + clientNodeConfig "source.quilibrium.com/quilibrium/monorepo/client/cmd/node/config" "source.quilibrium.com/quilibrium/monorepo/client/utils" "source.quilibrium.com/quilibrium/monorepo/node/config" ) @@ -18,23 +20,17 @@ var ( defaultSymlinkPath = "/usr/local/bin/quilibrium-node" logPath = "/var/log/quilibrium" - // Default user to run the node - nodeUser = "quilibrium" - ServiceName = "quilibrium-node" - ConfigDirs = "/home/quilibrium/configs" - NodeConfigToRun = "/home/quilibrium/configs/default" - - // Default config file name - defaultConfigFileName = "node.yaml" - - osType string - arch string + OsType string + Arch string configDirectory string NodeConfig *config.Config + NodeUser *user.User + ConfigDirs string + NodeConfigToRun string ) // NodeCmd represents the node command @@ -43,24 +39,26 @@ var NodeCmd = &cobra.Command{ Short: "Quilibrium node commands", Long: `Run Quilibrium node commands.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Store reference to parent's PersistentPreRun to call it first + parent := cmd.Parent() + if parent != nil && parent.PersistentPreRun != nil { + parent.PersistentPreRun(parent, args) + } + + // Then run the node-specific initialization var userLookup *user.User var err error - userLookup, err = user.Lookup(nodeUser) + userLookup, err = utils.GetCurrentSudoUser() if err != nil { - if !utils.IsSudo() { - fmt.Printf("need to be sudo to install quilibrium user, retry with sudo: %s\n", err) - os.Exit(1) - } - userLookup, err = InstallQuilibriumUser() - if err != nil { - fmt.Printf("error installing quilibrium user: %s\n", err) - os.Exit(1) - } + fmt.Fprintf(os.Stderr, "Error getting current user: %v\n", err) + os.Exit(1) } NodeUser = userLookup + ConfigDirs = filepath.Join(userLookup.HomeDir, ".quilibrium", "configs") + NodeConfigToRun = filepath.Join(userLookup.HomeDir, ".quilibrium", "configs", "default") }, Run: func(cmd *cobra.Command, args []string) { - + cmd.Help() }, } @@ -69,14 +67,16 @@ func init() { // Add subcommands NodeCmd.AddCommand(installCmd) + NodeCmd.AddCommand(clientNodeConfig.ConfigCmd) NodeCmd.AddCommand(updateNodeCmd) NodeCmd.AddCommand(nodeServiceCmd) + localOsType, localArch, err := utils.GetSystemInfo() if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return } - osType = localOsType - arch = localArch + OsType = localOsType + Arch = localArch } diff --git a/client/cmd/node/service.go b/client/cmd/node/service.go index d4a27e7..dfbff21 100644 --- a/client/cmd/node/service.go +++ b/client/cmd/node/service.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "os/exec" - "os/user" "path/filepath" "text/template" @@ -71,12 +70,12 @@ func installService() { return } - fmt.Fprintf(os.Stdout, "Installing Quilibrium node service for %s...\n", osType) + fmt.Fprintf(os.Stdout, "Installing Quilibrium node service for %s...\n", OsType) - if osType == "darwin" { + if OsType == "darwin" { // launchctl is already installed on macOS by default, so no need to check for it installMacOSService() - } else if osType == "linux" { + } else if OsType == "linux" { // systemd is not installed on linux by default, so we need to check for it if !CheckForSystemd() { // install systemd if not found @@ -87,7 +86,7 @@ func installService() { return } } else { - fmt.Fprintf(os.Stderr, "Error: Unsupported operating system: %s\n", osType) + fmt.Fprintf(os.Stderr, "Error: Unsupported operating system: %s\n", OsType) return } @@ -120,7 +119,7 @@ func startService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command cmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := cmd.Run(); err != nil { @@ -146,7 +145,7 @@ func stopService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command cmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := cmd.Run(); err != nil { @@ -172,7 +171,7 @@ func restartService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command - stop then start stopCmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := stopCmd.Run(); err != nil { @@ -204,7 +203,7 @@ func reloadService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command - unload then load plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) unloadCmd := exec.Command("sudo", "launchctl", "unload", plistPath) @@ -239,7 +238,7 @@ func checkServiceStatus() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command cmd := exec.Command("sudo", "launchctl", "list", fmt.Sprintf("com.quilibrium.%s", ServiceName)) cmd.Stdout = os.Stdout @@ -265,7 +264,7 @@ func enableService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command - load with -w flag to enable at boot plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) cmd := exec.Command("sudo", "launchctl", "load", "-w", plistPath) @@ -292,7 +291,7 @@ func disableService() { return } - if osType == "darwin" { + if OsType == "darwin" { // MacOS launchd command - unload with -w flag to disable at boot plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) cmd := exec.Command("sudo", "launchctl", "unload", "-w", plistPath) @@ -314,14 +313,14 @@ func disableService() { func createService() { // Create systemd service file - if osType == "linux" { + if OsType == "linux" { if err := createSystemdServiceFile(); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to create systemd service file: %v\n", err) } - } else if osType == "darwin" { + } else if OsType == "darwin" { installMacOSService() } else { - fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", osType) + fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", OsType) return } } @@ -337,11 +336,6 @@ func createSystemdServiceFile() error { return fmt.Errorf("failed to get sudo privileges: %w", err) } - userLookup, err := user.Lookup(nodeUser) - if err != nil { - return fmt.Errorf("failed to lookup user: %w", err) - } - // Create environment file content envContent := `# Quilibrium Node Environment` @@ -352,7 +346,7 @@ func createSystemdServiceFile() error { } // Set ownership of environment file - chownCmd := utils.ChownPath(envPath, userLookup, false) + chownCmd := utils.ChownPath(envPath, NodeUser, false) if chownCmd != nil { return fmt.Errorf("failed to set environment file ownership: %w", chownCmd) } diff --git a/client/cmd/node/shared.go b/client/cmd/node/shared.go index b23003e..4fe6a89 100644 --- a/client/cmd/node/shared.go +++ b/client/cmd/node/shared.go @@ -83,7 +83,7 @@ func setupLogRotation() error { func finishInstallation(version string) { setOwnership() - normalizedBinaryName := "node-" + version + "-" + osType + "-" + arch + normalizedBinaryName := "node-" + version + "-" + OsType + "-" + Arch // Finish installation nodeBinaryPath := filepath.Join(utils.NodeDataPath, version, normalizedBinaryName) @@ -118,16 +118,33 @@ func finishInstallation(version string) { // printSuccessMessage prints a success message after installation func printSuccessMessage(version string) { fmt.Fprintf(os.Stdout, "\nSuccessfully installed Quilibrium node %s\n", version) - fmt.Fprintf(os.Stdout, "Binary download directory: %s\n", utils.NodeDataPath+"/"+version) + fmt.Fprintf(os.Stdout, "Binary download directory: %s\n", filepath.Join(utils.NodeDataPath, version)) fmt.Fprintf(os.Stdout, "Binary symlinked to %s\n", defaultSymlinkPath) fmt.Fprintf(os.Stdout, "Log directory: %s\n", logPath) fmt.Fprintf(os.Stdout, "Environment file: /etc/default/quilibrium-node\n") fmt.Fprintf(os.Stdout, "Service file: /etc/systemd/system/quilibrium-node.service\n") - fmt.Fprintf(os.Stdout, "\nTo start the node, you can run:\n") - fmt.Fprintf(os.Stdout, " %s --config %s/config/config.yaml\n", + fmt.Fprintf(os.Stdout, "\nConfiguration:\n") + fmt.Fprintf(os.Stdout, " To create a new configuration:\n") + fmt.Fprintf(os.Stdout, " qclient node config create [name] --default\n") + fmt.Fprintf(os.Stdout, " quilibrium-node --peer-id %s/default-config\n", ConfigDirs) + + fmt.Fprintf(os.Stdout, "\n To use an existing configuration:\n") + fmt.Fprintf(os.Stdout, " cp -r /path/to/your/existing/config %s/default-config\n", ConfigDirs) + fmt.Fprintf(os.Stdout, " # Or modify the service file to point to your existing config:\n") + fmt.Fprintf(os.Stdout, " sudo nano /etc/systemd/system/quilibrium-node.service\n") + fmt.Fprintf(os.Stdout, " # Then reload systemd:\n") + fmt.Fprintf(os.Stdout, " sudo systemctl daemon-reload\n") + + fmt.Fprintf(os.Stdout, "\nTo select a configuration:\n") + fmt.Fprintf(os.Stdout, " qclient node config switch \n") + fmt.Fprintf(os.Stdout, " # Or use the --default flag when creating a config to automatically select it:\n") + fmt.Fprintf(os.Stdout, " qclient node config create --default\n") + + fmt.Fprintf(os.Stdout, "\nTo manually start the node (must create a config first), you can run:\n") + fmt.Fprintf(os.Stdout, " %s --config %s/myconfig/\n", ServiceName, ConfigDirs) - fmt.Fprintf(os.Stdout, " # Or use systemd service:\n") + fmt.Fprintf(os.Stdout, " # Or use systemd service using the default config:\n") fmt.Fprintf(os.Stdout, " sudo systemctl start quilibrium-node\n") fmt.Fprintf(os.Stdout, "\nFor more options, run:\n") diff --git a/client/cmd/node/uninstall.go b/client/cmd/node/uninstall.go new file mode 100644 index 0000000..9c93775 --- /dev/null +++ b/client/cmd/node/uninstall.go @@ -0,0 +1,10 @@ +package node + +// TODO: Implement a command to uninstall the current and previous versions +// of the node and all files, not including user data +// this should NEVER delete the user data, only the node files and logs + +// prompt the user for confirmation or a --force flag to skip the confirmation + +// qlient node uninstall +// qlient node uninstall --force (skip the confirmation) diff --git a/client/cmd/node/update.go b/client/cmd/node/update.go index f464fbb..5929e34 100644 --- a/client/cmd/node/update.go +++ b/client/cmd/node/update.go @@ -30,7 +30,7 @@ Examples: // Determine version to install version := determineVersion(args) - fmt.Fprintf(os.Stdout, "Updating Quilibrium node for %s-%s, version: %s\n", osType, arch, version) + fmt.Fprintf(os.Stdout, "Updating Quilibrium node for %s-%s, version: %s\n", OsType, Arch, version) // Update the node updateNode(version) @@ -69,7 +69,7 @@ func updateNode(version string) { err := utils.DownloadRelease(utils.ReleaseTypeNode, versionWithoutV) if err != nil { fmt.Fprintf(os.Stderr, "Error downloading version %s: %v\n", version, err) - fmt.Fprintf(os.Stderr, "The specified version %s does not exist for %s-%s\n", version, osType, arch) + fmt.Fprintf(os.Stderr, "The specified version %s does not exist for %s-%s\n", version, OsType, Arch) // Clean up the created directories since installation failed os.RemoveAll(versionDataDir) return diff --git a/client/cmd/node/user.go b/client/cmd/node/user.go deleted file mode 100644 index 672e085..0000000 --- a/client/cmd/node/user.go +++ /dev/null @@ -1,112 +0,0 @@ -package node - -import ( - "fmt" - "os" - "os/exec" - "os/user" - "strings" - - "source.quilibrium.com/quilibrium/monorepo/client/utils" -) - -// createNodeUser creates a dedicated user for running the node -func createNodeUser(nodeUser string) error { - fmt.Fprintf(os.Stdout, "Creating dedicated user '%s' for running the node...\n", nodeUser) - - // Check for sudo privileges - if err := utils.CheckAndRequestSudo("Creating system user requires root privileges"); err != nil { - return fmt.Errorf("failed to get sudo privileges: %w", err) - } - - if osType == "linux" { - return createLinuxNodeUser(nodeUser) - } else if osType == "darwin" { - return createMacNodeUser(nodeUser) - } else { - return fmt.Errorf("User creation not supported on %s", osType) - } -} - -func createLinuxNodeUser(username string) error { - var cmd *exec.Cmd - - // Check if user already exists - _, err := user.Lookup(username) - if err == nil { - fmt.Fprintf(os.Stdout, "User '%s' already exists\n", username) - return nil - } - - // Create user on Linux - cmd = exec.Command("useradd", "-r", "-s", "/bin/false", "-m", "-c", "Quilibrium Node User", username) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - - fmt.Fprintf(os.Stdout, "User '%s' created successfully\n", username) - return nil -} - -func createMacNodeUser(username string) error { - var cmd *exec.Cmd - - // Check if user already exists - _, err := user.Lookup(username) - if err == nil { - fmt.Fprintf(os.Stdout, "User '%s' already exists\n", username) - return nil - } - - // Create user on macOS - // Get next available user ID - uidCmd := exec.Command("dscl", ".", "-list", "/Users", "UniqueID") - uidOutput, err := uidCmd.Output() - if err != nil { - return fmt.Errorf("failed to get user IDs: %v", err) - } - - // Find the highest UID and add 1 - var maxUID int = 500 // Start with a reasonable system UID - for _, line := range strings.Split(string(uidOutput), "\n") { - fields := strings.Fields(line) - if len(fields) >= 2 { - var uid int - fmt.Sscanf(fields[len(fields)-1], "%d", &uid) - if uid > maxUID && uid < 65000 { // Avoid system UIDs - maxUID = uid - } - } - } - nextUID := maxUID + 1 - - // Create the user - cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser) - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create user: %v", err) - } - - // Set the user's properties - commands := [][]string{ - {"-create", "/Users/" + nodeUser, "UniqueID", fmt.Sprintf("%d", nextUID)}, - {"-create", "/Users/" + nodeUser, "PrimaryGroupID", "20"}, // staff group - {"-create", "/Users/" + nodeUser, "UserShell", "/bin/false"}, - {"-create", "/Users/" + nodeUser, "NFSHomeDirectory", "/var/empty"}, - {"-create", "/Users/" + nodeUser, "RealName", "Quilibrium Node User"}, - } - - for _, args := range commands { - cmd = exec.Command("dscl", append([]string{"."}, args...)...) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to set user property %s: %v", args[1], err) - } - } - - // Disable the user account - cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser, "Password", "*") - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to disable user account: %v", err) - } - - return nil -} diff --git a/client/cmd/node/utils.go b/client/cmd/node/utils.go index f17597b..d2dd2bc 100644 --- a/client/cmd/node/utils.go +++ b/client/cmd/node/utils.go @@ -2,10 +2,7 @@ package node import ( "encoding/hex" - "fmt" - "os" "os/exec" - "os/user" "path/filepath" "github.com/pkg/errors" @@ -55,26 +52,3 @@ func CheckForSystemd() bool { _, err := exec.LookPath("systemctl") return err == nil } - -func checkForQuilibriumUser() (*user.User, error) { - user, err := user.Lookup(nodeUser) - if err != nil { - return nil, err - } - return user, nil -} - -func InstallQuilibriumUser() (*user.User, error) { - var user *user.User - var err error - user, err = checkForQuilibriumUser() - if err != nil { - if err := createNodeUser(nodeUser); err != nil { - fmt.Fprintf(os.Stderr, "Error creating user: %v\n", err) - os.Exit(1) - } - user, err = checkForQuilibriumUser() - } - - return user, err -} diff --git a/client/cmd/root.go b/client/cmd/root.go index 9b82c54..6548bef 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -23,6 +23,7 @@ import ( var configDirectory string var signatureCheck bool = true +var byPassSignatureCheck bool = false var NodeConfig *config.Config var simulateFail bool var DryRun bool = false @@ -52,7 +53,7 @@ It provides commands for installing, updating, and managing Quilibrium nodes.`, os.Exit(1) } - if !clientConfig.SignatureCheck { + if !clientConfig.SignatureCheck || byPassSignatureCheck { signatureCheck = false } @@ -90,7 +91,7 @@ It provides commands for installing, updating, and managing Quilibrium nodes.`, reader := bufio.NewReader(os.Stdin) response, _ := reader.ReadString('\n') - response = strings.TrimSpace(strings.ToLower(response)) + response = strings.ToLower(strings.TrimSpace(response)) if response != "y" && response != "yes" { fmt.Println("Exiting due to missing digest file") @@ -199,27 +200,14 @@ func init() { "bypass signature check (not recommended for binaries) (default true or value of QUILIBRIUM_SIGNATURE_CHECK env var)", ) - rootCmd.PersistentFlags().BoolP( + rootCmd.PersistentFlags().BoolVarP( + &byPassSignatureCheck, "yes", "y", false, "auto-approve and bypass signature check (sets signature-check=false)", ) - // Store original PersistentPreRun - originalPersistentPreRun := rootCmd.PersistentPreRun - - // Override PersistentPreRun to check for -y flag first - rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - // Check if -y flag is set and update signatureCheck - if yes, _ := cmd.Flags().GetBool("yes"); yes { - signatureCheck = false - } - - // Call the original PersistentPreRun - originalPersistentPreRun(cmd, args) - } - // Add the node command rootCmd.AddCommand(node.NodeCmd) rootCmd.AddCommand(clientConfig.ConfigCmd) diff --git a/client/utils/download.go b/client/utils/download.go index 9845539..b4cd51d 100644 --- a/client/utils/download.go +++ b/client/utils/download.go @@ -15,6 +15,15 @@ var BaseReleaseURL = "https://releases.quilibrium.com" // DownloadRelease downloads a specific release file func DownloadRelease(releaseType ReleaseType, version string) error { fileName := fmt.Sprintf("%s-%s-%s-%s", releaseType, version, osType, arch) + fmt.Printf("Getting binary %s...\n", fileName) + fmt.Println("Will save to", filepath.Join(BinaryPath, string(releaseType), version)) + url := fmt.Sprintf("%s/%s", BaseReleaseURL, fileName) + + if !DoesRemoteFileExist(url) { + fmt.Printf("the release file %s does not exist on the release server\n", fileName) + os.Exit(1) + } + return DownloadReleaseFile(releaseType, fileName, version, true) } @@ -25,6 +34,7 @@ func GetLatestVersion(releaseType ReleaseType) (string, error) { if releaseType == ReleaseTypeQClient { releaseURL = fmt.Sprintf("%s/qclient-release", BaseReleaseURL) } + resp, err := http.Get(releaseURL) if err != nil { return "", fmt.Errorf("failed to fetch latest version: %w", err) @@ -57,7 +67,7 @@ func DownloadReleaseFile(releaseType ReleaseType, fileName string, version strin os.MkdirAll(destDir, 0755) destPath := filepath.Join(destDir, fileName) - fmt.Printf("Downloading %s to %s\n", url, destPath) + fmt.Printf("Downloading %s...", fileName) resp, err := http.Get(url) if err != nil { @@ -83,7 +93,7 @@ func DownloadReleaseFile(releaseType ReleaseType, fileName string, version strin if err != nil { return err } - fmt.Fprintf(os.Stdout, "Downloaded %s to %s\n", fileName, destPath) + fmt.Print(" done\n") return nil } @@ -91,7 +101,8 @@ func DownloadReleaseFile(releaseType ReleaseType, fileName string, version strin func DownloadReleaseSignatures(releaseType ReleaseType, version string) error { var files []string baseName := fmt.Sprintf("%s-%s-%s-%s", releaseType, version, osType, arch) - fmt.Printf("Downloading signatures for %s\n", baseName) + fmt.Printf("Searching for signatures for %s from %s\n", baseName, BaseReleaseURL) + fmt.Println("Will save to", filepath.Join(BinaryPath, string(releaseType), version)) // Add digest file URL files = append(files, baseName+".dgst") @@ -103,19 +114,11 @@ func DownloadReleaseSignatures(releaseType ReleaseType, version string) error { sigFile := fmt.Sprintf("%s.dgst.sig.%d", baseName, num) remoteURL := fmt.Sprintf("%s/%s", BaseReleaseURL, sigFile) - // Make a HEAD request to check if the file exists - resp, err := http.Head(remoteURL) - if err != nil || resp.StatusCode != http.StatusOK { - // Skip this file if it doesn't exist on the server - fmt.Printf("Signature file %s not found on server, skipping\n", sigFile) + if !DoesRemoteFileExist(remoteURL) { continue - } else { - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - fmt.Printf("Signature file %s found on server, adding to download list\n", sigFile) - files = append(files, fmt.Sprintf("%s.dgst.sig.%d", baseName, num)) } + fmt.Printf("Found signature file %s\n", sigFile) + files = append(files, fmt.Sprintf("%s.dgst.sig.%d", baseName, num)) } if len(files) == 0 { @@ -192,3 +195,14 @@ func DownloadAllReleaseFiles(releaseType ReleaseType, fileNames []string, instal } return true } + +func DoesRemoteFileExist(url string) bool { + resp, err := http.Head(url) + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + return true +} diff --git a/client/utils/signatures.go b/client/utils/signatures.go deleted file mode 100644 index b929fb6..0000000 --- a/client/utils/signatures.go +++ /dev/null @@ -1,34 +0,0 @@ -package utils - -func OfferSignatureDownload(ClientConfig *ClientConfig, version string) { - // varDataPath := filepath.Join(ClientConfig.DataDir, version) - // digestPath := filepath.Join(varDataPath, StandardizedQClientFileName+".dgst") - // if FileExists(digestPath) { - // // Fall back to checking next to executable - // digestPath = ClientDataPath + "/" + version + "/" + StandardizedQClientFileName + ".dgst" - // if !FileExists(digestPath) { - // fmt.Println("Signature file not found. Would you like to download it? (y/n)") - // reader := bufio.NewReader(os.Stdin) - // response, _ := reader.ReadString('\n') - // response = strings.TrimSpace(strings.ToLower(response)) - - // if response == "y" || response == "yes" { - // fmt.Println("Downloading signature files...") - // if version == "" { - // fmt.Println("Could not determine version from executable name") - // return fmt.Errorf("could not determine version from executable name") - // } - - // // Download signature files - // if err := utils.DownloadReleaseSignatures(utils.ReleaseTypeQClient, version); err != nil { - // fmt.Printf("Error downloading signature files: %v\n", err) - // return fmt.Errorf("failed to download signature files: %v", err) - // } - // fmt.Println("Successfully downloaded signature files") - // } else { - // fmt.Println("Continuing without signature verification") - // signatureCheck = false - // } - // } - // } -} diff --git a/client/utils/types.go b/client/utils/types.go index a8410e0..9e0e605 100644 --- a/client/utils/types.go +++ b/client/utils/types.go @@ -8,10 +8,14 @@ type ClientConfig struct { type NodeConfig struct { ClientConfig - DataDir string - User string + RewardsAddress string `yaml:"rewardsAddress"` + AutoUpdateInterval string `yaml:"autoUpdateInterval"` } +const ( + DefaultAutoUpdateInterval = "*/10 * * * *" +) + type ReleaseType string const (