From df47bcc2d2f307e478165b97f1336c0737deeb35 Mon Sep 17 00:00:00 2001 From: Tyler Sturos <55340199+tjsturos@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:00:36 -0800 Subject: [PATCH] update install and node commands --- client/cmd/config/signatureCheck.go | 58 ++++ client/cmd/config/toggleSignatureCheck.go | 42 --- client/cmd/link.go | 106 +----- client/cmd/node/autoUpdate.go | 141 ++++++++ client/cmd/node/config/config.go | 64 ++++ client/cmd/node/config/create-default.go | 91 +++++ client/cmd/node/config/import.go | 70 ++++ client/cmd/node/config/set-default.go | 56 +++ client/cmd/node/config/set.go | 87 +++++ client/cmd/node/config/utils.go | 26 ++ client/cmd/node/install.go | 42 +-- client/cmd/node/node.go | 42 ++- client/cmd/node/service.go | 243 ++++++++++++- client/cmd/node/shared.go | 326 ++---------------- client/cmd/node/update.go | 25 +- client/cmd/node/user.go | 112 ++++++ client/cmd/node/utils.go | 39 +++ client/cmd/root.go | 41 ++- client/test/README.md | 9 + .../.quilibrium/config/qclient.yaml" | 3 + client/test/test_install.sh | 13 + client/utils/config.go | 33 +- client/utils/download.go | 12 +- client/utils/fileUtils.go | 111 +++++- client/utils/system.go | 38 ++ 25 files changed, 1293 insertions(+), 537 deletions(-) create mode 100644 client/cmd/config/signatureCheck.go delete mode 100644 client/cmd/config/toggleSignatureCheck.go create mode 100644 client/cmd/node/autoUpdate.go create mode 100644 client/cmd/node/config/config.go create mode 100644 client/cmd/node/config/create-default.go create mode 100644 client/cmd/node/config/import.go create mode 100644 client/cmd/node/config/set-default.go create mode 100644 client/cmd/node/config/set.go create mode 100644 client/cmd/node/config/utils.go create mode 100644 client/cmd/node/user.go create mode 100644 "client/test/home/testuser\n/.quilibrium/config/qclient.yaml" diff --git a/client/cmd/config/signatureCheck.go b/client/cmd/config/signatureCheck.go new file mode 100644 index 0000000..794a159 --- /dev/null +++ b/client/cmd/config/signatureCheck.go @@ -0,0 +1,58 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "source.quilibrium.com/quilibrium/monorepo/client/utils" +) + +var signatureCheckCmd = &cobra.Command{ + Use: "signature-check [true|false]", + Short: "Set signature check setting", + Long: `Set the signature check setting in the client configuration. +When disabled, signature verification will be bypassed (not recommended for production use). +Use 'true' to enable or 'false' to disable. If no argument is provided, it toggles the current setting.`, + Run: func(cmd *cobra.Command, args []string) { + config, err := utils.LoadClientConfig() + if err != nil { + fmt.Printf("Error loading config: %v\n", err) + os.Exit(1) + } + + if len(args) > 0 { + // Set the signature check based on the provided argument + value := strings.ToLower(args[0]) + if value == "true" { + config.SignatureCheck = true + } else if value == "false" { + config.SignatureCheck = false + } else { + // If the value is not true or false, print error message and exit + fmt.Printf("Error: Invalid value '%s'. Please use 'true' or 'false'.\n", value) + os.Exit(1) + } + } else { + // Toggle the signature check setting if no arguments are provided + config.SignatureCheck = !config.SignatureCheck + } + + // Save the updated config + if err := utils.SaveClientConfig(config); err != nil { + fmt.Printf("Error saving config: %v\n", err) + os.Exit(1) + } + + status := "enabled" + if !config.SignatureCheck { + status = "disabled" + } + fmt.Printf("Signature check has been set to %s and will be persisted across future commands unless reset.\n", status) + }, +} + +func init() { + ConfigCmd.AddCommand(signatureCheckCmd) +} diff --git a/client/cmd/config/toggleSignatureCheck.go b/client/cmd/config/toggleSignatureCheck.go deleted file mode 100644 index 863920b..0000000 --- a/client/cmd/config/toggleSignatureCheck.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "source.quilibrium.com/quilibrium/monorepo/client/utils" -) - -var toggleSignatureCheckCmd = &cobra.Command{ - Use: "toggle-signature-check", - Short: "Toggle signature check setting", - Long: `Toggle the signature check setting in the client configuration. -When disabled, signature verification will be bypassed (not recommended for production use).`, - Run: func(cmd *cobra.Command, args []string) { - config, err := utils.LoadClientConfig() - if err != nil { - fmt.Printf("Error loading config: %v\n", err) - os.Exit(1) - } - - // Toggle the signature check setting - config.SignatureCheck = !config.SignatureCheck - - // Save the updated config - if err := utils.SaveClientConfig(config); err != nil { - fmt.Printf("Error saving config: %v\n", err) - os.Exit(1) - } - - status := "enabled" - if !config.SignatureCheck { - status = "disabled" - } - fmt.Printf("Signature check has been %s\n", status) - }, -} - -func init() { - ConfigCmd.AddCommand(toggleSignatureCheckCmd) -} diff --git a/client/cmd/link.go b/client/cmd/link.go index 1183f0c..9fb1388 100644 --- a/client/cmd/link.go +++ b/client/cmd/link.go @@ -3,25 +3,19 @@ package cmd import ( "fmt" "os" - "path/filepath" - "strings" "github.com/spf13/cobra" "source.quilibrium.com/quilibrium/monorepo/client/utils" ) -var symlinkPath string +var symlinkPath = "/usr/local/bin/qclient" var linkCmd = &cobra.Command{ Use: "link", Short: "Create a symlink to qclient in PATH", - Long: fmt.Sprintf(`Create a symlink to the qclient binary in a suitable directory in your PATH. -This allows you to run qclient from anywhere without specifying the full path. + Long: `Create a symlink to the qclient binary in the directory /usr/local/bin/. -By default it will create the symlink in the current directory /usr/local/bin. -You can also specify a custom directory using the --path flag. - -Example: qclient link --path /usr/local/bin`), +Example: qclient link`, RunE: func(cmd *cobra.Command, args []string) error { // Get the path to the current executable execPath, err := os.Executable() @@ -29,102 +23,24 @@ Example: qclient link --path /usr/local/bin`), return fmt.Errorf("failed to get executable path: %w", err) } - // Determine the target directory and path for the symlink - targetDir, targetPath, err := determineSymlinkLocation() - if err != nil { - return err - } - - // If operation was cancelled by the user - if targetDir == "" && targetPath == "" { - return nil + IsSudo := utils.IsSudo() + if IsSudo { + fmt.Println("Running as sudo, creating symlink at /usr/local/bin/qclient") + } else { + fmt.Println("Cannot create symlink at /usr/local/bin/qclient, please run this command with sudo") + os.Exit(1) } // Create the symlink (handles existing symlinks) - if err := utils.CreateSymlink(execPath, targetPath); err != nil { + if err := utils.CreateSymlink(execPath, symlinkPath); err != nil { return err } - fmt.Printf("Symlink created at %s\n", targetPath) + fmt.Printf("Symlink created at %s\n", symlinkPath) return nil }, } -// determineSymlinkLocation finds the appropriate location for the symlink -// Returns the target directory, the full path for the symlink, and any error -func determineSymlinkLocation() (string, string, error) { - // If user provided a custom path - if symlinkPath != "" { - return validateUserProvidedPath(symlinkPath) - } - - // Otherwise, find a suitable directory in PATH - return utils.DefaultSymlinkDir, utils.DefaultQClientSymlinkPath, nil -} - -// isDirectoryInPath checks if a directory is in the PATH environment variable -func isDirectoryInPath(dir string) bool { - pathEnv := os.Getenv("PATH") - pathDirs := strings.Split(pathEnv, string(os.PathListSeparator)) - - // Normalize paths for comparison - absDir, err := filepath.Abs(dir) - if err != nil { - return false - } - - for _, pathDir := range pathDirs { - absPathDir, err := filepath.Abs(pathDir) - if err != nil { - continue - } - - if absDir == absPathDir { - return true - } - } - - return false -} - -// validateUserProvidedPath checks if the provided path is a valid directory -func validateUserProvidedPath(path string) (string, string, error) { - // Check if the provided path is a directory - info, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return "", "", fmt.Errorf("directory does not exist: %s", path) - } - return "", "", fmt.Errorf("error checking directory: %w", err) - } - - if !info.IsDir() { - return "", "", fmt.Errorf("the specified path is not a directory: %s", path) - } - - // Check if the directory is in PATH - if !isDirectoryInPath(path) { - // Ask user for confirmation to proceed with a directory not in PATH - fmt.Printf("Warning: The directory '%s' is not in your PATH environment variable.\n", path) - fmt.Println("The symlink will be created, but you may not be able to run 'qclient' from anywhere.") - fmt.Print("Do you want to continue? [y/N]: ") - - var response string - fmt.Scanln(&response) - if strings.ToLower(response) != "y" { - fmt.Println("Operation cancelled.") - return "", "", nil - } - } - - // Use the provided directory - targetDir := path - targetPath := filepath.Join(targetDir, "qclient") - - return targetDir, targetPath, nil -} - func init() { rootCmd.AddCommand(linkCmd) - linkCmd.Flags().StringVar(&symlinkPath, "path", "", "Specify a custom directory for the symlink") } diff --git a/client/cmd/node/autoUpdate.go b/client/cmd/node/autoUpdate.go new file mode 100644 index 0000000..8952702 --- /dev/null +++ b/client/cmd/node/autoUpdate.go @@ -0,0 +1,141 @@ +package node + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +// autoUpdateCmd represents the command to setup automatic updates +var autoUpdateCmd = &cobra.Command{ + Use: "auto-update", + Short: "Setup automatic update checks", + Long: `Setup a cron job to automatically check for Quilibrium node updates every 10 minutes. + +This command will create a cron entry that runs 'qclient node update' every 10 minutes +to check for and apply any available updates. + +Example: + # Setup automatic update checks + qclient node auto-update`, + Run: func(cmd *cobra.Command, args []string) { + setupCronJob() + }, +} + +func init() { + NodeCmd.AddCommand(autoUpdateCmd) +} + +func setupCronJob() { + // Get full path to qclient executable + qclientPath, err := exec.LookPath("qclient") + if err != nil { + fmt.Fprintf(os.Stderr, "Error: qclient executable not found in PATH: %v\n", err) + fmt.Fprintf(os.Stderr, "Please ensure qclient is properly installed and in your PATH (use 'sudo qclient link')\n") + return + } + + // Absolute path for qclient + qclientAbsPath, err := filepath.Abs(qclientPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting absolute path for qclient: %v\n", err) + return + } + + // OS-specific setup + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { + setupUnixCron(qclientAbsPath) + } else { + fmt.Fprintf(os.Stderr, "Error: auto-update is only supported on macOS and Linux\n") + return + } +} + +func isCrontabInstalled() bool { + // Check if crontab is installed + _, err := exec.LookPath("crontab") + return err == nil +} + +func installCrontab() { + fmt.Fprintf(os.Stdout, "Installing cron package...\n") + // Install crontab + installCmd := exec.Command("sudo", "apt-get", "update", "&&", "sudo", "apt-get", "install", "-y", "cron") + if err := installCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error installing cron package: %v\n", err) + return + } + + if isCrontabInstalled() { + fmt.Fprintf(os.Stdout, "Cron package installed\n") + } else { + fmt.Fprintf(os.Stderr, "Error: cron package not installed\n") + os.Exit(1) + } +} + +func setupUnixCron(qclientPath string) { + if !isCrontabInstalled() { + fmt.Fprintf(os.Stdout, "Crontab command not found, attempting to install cron package...\n") + installCrontab() + } + + fmt.Fprintf(os.Stdout, "Setting up cron job...\n") + // Create cron expression: run every 10 minutes + cronExpression := fmt.Sprintf("*/10 * * * * %s node update > /dev/null 2>&1", qclientPath) + + // Check existing crontab + checkCmd := exec.Command("crontab", "-l") + checkOutput, err := checkCmd.CombinedOutput() + + var currentCrontab string + if err != nil { + // If there's no crontab yet, this is fine, start with empty crontab + currentCrontab = "" + } else { + currentCrontab = string(checkOutput) + } + + // Check if our update command is already in crontab + if strings.Contains(currentCrontab, "qclient node update") { + fmt.Fprintf(os.Stdout, "Automatic update check is already configured in crontab\n") + return + } + + // Add new cron entry + newCrontab := currentCrontab + if strings.TrimSpace(newCrontab) != "" && !strings.HasSuffix(newCrontab, "\n") { + newCrontab += "\n" + } + newCrontab += cronExpression + "\n" + + // Write to temporary file + tempFile, err := os.CreateTemp("", "qclient-crontab") + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating temporary file: %v\n", err) + return + } + defer os.Remove(tempFile.Name()) + + if _, err := tempFile.WriteString(newCrontab); err != nil { + fmt.Fprintf(os.Stderr, "Error writing to temporary file: %v\n", err) + return + } + tempFile.Close() + + // Install new crontab + installCmd := exec.Command("crontab", tempFile.Name()) + if err := installCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error installing crontab: %v\n", err) + return + } + + fmt.Fprintf(os.Stdout, "Successfully configured cron job to check for updates every 10 minutes\n") + fmt.Fprintf(os.Stdout, "Added: %s\n", cronExpression) +} diff --git a/client/cmd/node/config/config.go b/client/cmd/node/config/config.go new file mode 100644 index 0000000..9768aa8 --- /dev/null +++ b/client/cmd/node/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "github.com/spf13/cobra" + clientNode "source.quilibrium.com/quilibrium/monorepo/client/cmd/node" + "source.quilibrium.com/quilibrium/monorepo/client/utils" +) + +// ConfigCmd represents the node config command +var ConfigCmd = &cobra.Command{ + Use: "config", + Short: "Manage node configuration", + Long: `Manage Quilibrium node configuration. + +This command provides utilities for configuring your Quilibrium node, such as: +- Setting configuration values +- Setting default configuration +- Creating default configuration +- Importing configuration +`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Check if the config directory exists + if utils.FileExists(clientNode.ConfigDirs) { + utils.ValidateAndCreateDir(clientNode.ConfigDirs, clientNode.NodeUser) + } + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +// 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) +} diff --git a/client/cmd/node/config/create-default.go b/client/cmd/node/config/create-default.go new file mode 100644 index 0000000..80235ab --- /dev/null +++ b/client/cmd/node/config/create-default.go @@ -0,0 +1,91 @@ +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/import.go b/client/cmd/node/config/import.go new file mode 100644 index 0000000..90195b7 --- /dev/null +++ b/client/cmd/node/config/import.go @@ -0,0 +1,70 @@ +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 importCmd = &cobra.Command{ + Use: "import [name] [source_directory]", + Short: "Import config.yml and keys.yml from a source directory", + Long: `Import config.yml and keys.yml from a source directory to the QuilibriumRoot config folder. + +Example: + qclient node config import mynode /path/to/source + +This will copy config.yml and keys.yml from /path/to/source to /home/quilibrium/configs/mynode/`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + sourceDir := args[1] + + // Check if source directory exists + if _, err := os.Stat(sourceDir); os.IsNotExist(err) { + fmt.Printf("Source directory does not exist: %s\n", sourceDir) + os.Exit(1) + } + + if !HasConfigFiles(sourceDir) { + fmt.Printf("Source directory does not contain both config.yml and keys.yml files: %s\n", sourceDir) + os.Exit(1) + } + + // Create target directory in the standard location + targetDir := filepath.Join(clientNode.ConfigDirs, name) + if err := os.MkdirAll(targetDir, 0755); err != nil { + fmt.Printf("Failed to create target directory: %s\n", err) + os.Exit(1) + } + + // Define source file paths + sourceConfigPath := filepath.Join(sourceDir, "config.yml") + sourceKeysPath := filepath.Join(sourceDir, "keys.yml") + + // Copy config.yml + targetConfigPath := filepath.Join(targetDir, "config.yml") + if err := utils.CopyFile(sourceConfigPath, targetConfigPath); err != nil { + fmt.Printf("Failed to copy config.yml: %s\n", err) + os.Exit(1) + } + + // Copy keys.yml + targetKeysPath := filepath.Join(targetDir, "keys.yml") + if err := utils.CopyFile(sourceKeysPath, targetKeysPath); err != nil { + fmt.Printf("Failed to copy keys.yml: %s\n", err) + os.Exit(1) + } + + 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 new file mode 100644 index 0000000..962128c --- /dev/null +++ b/client/cmd/node/config/set-default.go @@ -0,0 +1,56 @@ +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 new file mode 100644 index 0000000..1671284 --- /dev/null +++ b/client/cmd/node/config/set.go @@ -0,0 +1,87 @@ +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/node/config" +) + +var setCmd = &cobra.Command{ + Use: "set [name] [key] [value]", + Short: "Set a configuration value", + Long: `Set a configuration value in the node config.yml file. + +Example: + qclient node config set mynode engine.statsMultiaddr /dns/stats.quilibrium.com/tcp/443 +`, + Args: cobra.ExactArgs(3), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + key := args[1] + value := args[2] + + // Construct the config directory path + configDir := filepath.Join(clientNode.ConfigDirs, name) + configFile := filepath.Join(configDir, "config.yml") + + // Check if config directory exists + if _, err := os.Stat(configDir); os.IsNotExist(err) { + fmt.Printf("Config directory does not exist: %s\n", configDir) + os.Exit(1) + } + + // Check if config file exists + if _, err := os.Stat(configFile); os.IsNotExist(err) { + fmt.Printf("Config file does not exist: %s\n", configFile) + os.Exit(1) + } + + // Load the config + cfg, err := config.LoadConfig(configFile, "", false) + if err != nil { + fmt.Printf("Failed to load config: %s\n", err) + os.Exit(1) + } + + // Update the config based on the key + switch key { + case "engine.statsMultiaddr": + cfg.Engine.StatsMultiaddr = value + case "p2p.listenMultiaddr": + cfg.P2P.ListenMultiaddr = value + case "listenGrpcMultiaddr": + cfg.ListenGRPCMultiaddr = value + case "listenRestMultiaddr": + cfg.ListenRestMultiaddr = value + case "engine.autoMergeCoins": + if value == "true" { + cfg.Engine.AutoMergeCoins = true + } else if value == "false" { + cfg.Engine.AutoMergeCoins = false + } else { + fmt.Printf("Invalid value for %s: must be 'true' or 'false'\n", key) + os.Exit(1) + } + default: + fmt.Printf("Unsupported configuration key: %s\n", key) + fmt.Println("Supported keys: engine.statsMultiaddr, p2p.listenMultiaddr, listenGrpcMultiaddr, listenRestMultiaddr, engine.autoMergeCoins") + os.Exit(1) + } + + // Save the updated config + if err := config.SaveConfig(configFile, cfg); err != nil { + fmt.Printf("Failed to save config: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully updated %s to %s in %s\n", key, value, configFile) + }, +} + +func init() { + ConfigCmd.AddCommand(setCmd) +} diff --git a/client/cmd/node/config/utils.go b/client/cmd/node/config/utils.go new file mode 100644 index 0000000..fd3b296 --- /dev/null +++ b/client/cmd/node/config/utils.go @@ -0,0 +1,26 @@ +package config + +import ( + "os" + "path/filepath" +) + +// HasConfigFiles checks if a directory contains both config.yml and keys.yml files +func HasConfigFiles(dirPath string) bool { + configPath := filepath.Join(dirPath, "config.yml") + keysPath := filepath.Join(dirPath, "keys.yml") + + // Check if both files exist + configExists := false + keysExists := false + + if _, err := os.Stat(configPath); !os.IsNotExist(err) { + configExists = true + } + + if _, err := os.Stat(keysPath); !os.IsNotExist(err) { + keysExists = true + } + + return configExists && keysExists +} diff --git a/client/cmd/node/install.go b/client/cmd/node/install.go index fa90da5..c2f17b1 100644 --- a/client/cmd/node/install.go +++ b/client/cmd/node/install.go @@ -3,6 +3,7 @@ package node import ( "fmt" "os" + "os/user" "path/filepath" "github.com/spf13/cobra" @@ -35,18 +36,16 @@ Examples: return } + if !utils.IsSudo() { + fmt.Println("This command must be run with sudo: sudo qclient node install") + os.Exit(1) + } + // Determine version to install version := determineVersion(args) fmt.Fprintf(os.Stdout, "Installing Quilibrium node for %s-%s, version: %s\n", osType, arch, version) - // Check if we need to create a dedicated user (opt-out) - - if err := createNodeUser(); err != nil { - fmt.Fprintf(os.Stderr, "Error creating user: %v\n", err) - fmt.Fprintf(os.Stdout, "Continuing with installation as current user...\n") - } - // Install the node installNode(version) }, @@ -60,17 +59,11 @@ func init() { // installNode installs the Quilibrium node func installNode(version string) { // Create installation directory - if err := utils.ValidateAndCreateDir(installPath); err != nil { + if err := utils.ValidateAndCreateDir(utils.NodeDataPath, NodeUser); err != nil { fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err) return } - // Create data directory - if err := utils.ValidateAndCreateDir(dataPath); err != nil { - fmt.Fprintf(os.Stderr, "Error creating data directory: %v\n", err) - return - } - // Download and install the node if version == "latest" { latestVersion, err := utils.GetLatestVersion(utils.ReleaseTypeNode) @@ -80,7 +73,12 @@ func installNode(version string) { } version = latestVersion - fmt.Fprintf(os.Stdout, "Installing latest version: %s\n", version) + 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) } if err := installByVersion(version); err != nil { @@ -88,16 +86,20 @@ func installNode(version string) { os.Exit(1) } - // Finish installation - nodeBinaryPath := filepath.Join(installPath, string(utils.ReleaseTypeNode), version) - finishInstallation(nodeBinaryPath, version) + createService() + + finishInstallation(version) } // installByVersion installs a specific version of the Quilibrium node func installByVersion(version string) error { // Create version-specific directory - versionDir := filepath.Join(installPath, version) - if err := utils.ValidateAndCreateDir(versionDir); err != nil { + 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 { return fmt.Errorf("failed to create version directory: %w", err) } diff --git a/client/cmd/node/node.go b/client/cmd/node/node.go index e88b4a8..995d1e7 100644 --- a/client/cmd/node/node.go +++ b/client/cmd/node/node.go @@ -3,6 +3,7 @@ package node import ( "fmt" "os" + "os/user" "github.com/spf13/cobra" "source.quilibrium.com/quilibrium/monorepo/client/utils" @@ -15,19 +16,15 @@ var ( // Default symlink path for the node binary defaultSymlinkPath = "/usr/local/bin/quilibrium-node" - - // Default installation directory base path - installPath = "/opt/quilibrium" - - // Default data directory paths - dataPath = "/var/lib/quilibrium" - - logPath = "/var/log/quilibrium" + logPath = "/var/log/quilibrium" // Default user to run the node nodeUser = "quilibrium" - serviceName = "quilibrium-node" + ServiceName = "quilibrium-node" + + ConfigDirs = "/home/quilibrium/configs" + NodeConfigToRun = "/home/quilibrium/configs/default" // Default config file name defaultConfigFileName = "node.yaml" @@ -37,8 +34,7 @@ var ( configDirectory string NodeConfig *config.Config - publicRPC bool = false - LightNode bool = false + NodeUser *user.User ) // NodeCmd represents the node command @@ -46,6 +42,19 @@ var NodeCmd = &cobra.Command{ Use: "node", Short: "Quilibrium node commands", Long: `Run Quilibrium node commands.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + var userLookup *user.User + var err error + userLookup, err = user.Lookup(nodeUser) + if err != nil { + userLookup, err = InstallQuilibriumUser() + if err != nil { + fmt.Printf("error installing quilibrium user: %s\n", err) + os.Exit(1) + } + } + NodeUser = userLookup + }, Run: func(cmd *cobra.Command, args []string) { // These commands handle their own configuration _, err := os.Stat(configDirectory) @@ -59,22 +68,11 @@ var NodeCmd = &cobra.Command{ fmt.Printf("invalid config directory: %s\n", configDirectory) os.Exit(1) } - - if publicRPC { - fmt.Println("Public RPC enabled, using light node") - LightNode = true - } - - if !LightNode && NodeConfig.ListenGRPCMultiaddr == "" { - fmt.Println("No ListenGRPCMultiaddr found in config, using light node") - LightNode = true - } }, } func init() { NodeCmd.PersistentFlags().StringVar(&configDirectory, "config", ".config", "config directory (default is .config/)") - NodeCmd.PersistentFlags().BoolVar(&publicRPC, "public-rpc", false, "Use public RPC for node operations") // Add subcommands NodeCmd.AddCommand(installCmd) diff --git a/client/cmd/node/service.go b/client/cmd/node/service.go index d689d15..d4a27e7 100644 --- a/client/cmd/node/service.go +++ b/client/cmd/node/service.go @@ -4,6 +4,9 @@ import ( "fmt" "os" "os/exec" + "os/user" + "path/filepath" + "text/template" "github.com/spf13/cobra" "source.quilibrium.com/quilibrium/monorepo/client/utils" @@ -62,6 +65,7 @@ Examples: // installService installs the appropriate service configuration for the current OS func installService() { + if err := utils.CheckAndRequestSudo("Installing service requires root privileges"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return @@ -70,8 +74,14 @@ func installService() { fmt.Fprintf(os.Stdout, "Installing Quilibrium node service for %s...\n", osType) if osType == "darwin" { + // launchctl is already installed on macOS by default, so no need to check for it installMacOSService() } 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 + installSystemd() + } if err := createSystemdServiceFile(); err != nil { fmt.Fprintf(os.Stderr, "Error creating systemd service file: %v\n", err) return @@ -84,6 +94,25 @@ func installService() { fmt.Fprintf(os.Stdout, "Quilibrium node service installed successfully\n") } +func installSystemd() { + fmt.Fprintf(os.Stdout, "Installing systemd...\n") + updateCmd := exec.Command("sudo", "apt-get", "update") + updateCmd.Stdout = nil + updateCmd.Stderr = nil + if err := updateCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error updating package lists: %v\n", err) + return + } + + installCmd := exec.Command("sudo", "apt-get", "install", "-y", "systemd") + installCmd.Stdout = nil + installCmd.Stderr = nil + if err := installCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error installing systemd: %v\n", err) + return + } +} + // startService starts the Quilibrium node service func startService() { if err := utils.CheckAndRequestSudo("Starting service requires root privileges"); err != nil { @@ -93,14 +122,14 @@ func startService() { if osType == "darwin" { // MacOS launchd command - cmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", serviceName)) + cmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err) return } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "start", serviceName) + cmd := exec.Command("sudo", "systemctl", "start", ServiceName) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err) return @@ -119,14 +148,14 @@ func stopService() { if osType == "darwin" { // MacOS launchd command - cmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", serviceName)) + cmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err) return } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "stop", serviceName) + cmd := exec.Command("sudo", "systemctl", "stop", ServiceName) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err) return @@ -145,20 +174,20 @@ func restartService() { if osType == "darwin" { // MacOS launchd command - stop then start - stopCmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", serviceName)) + stopCmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := stopCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err) return } - startCmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", serviceName)) + startCmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", ServiceName)) if err := startCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err) return } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "restart", serviceName) + cmd := exec.Command("sudo", "systemctl", "restart", ServiceName) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err) return @@ -177,7 +206,7 @@ func reloadService() { if osType == "darwin" { // MacOS launchd command - unload then load - plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName) + plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) unloadCmd := exec.Command("sudo", "launchctl", "unload", plistPath) if err := unloadCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error unloading service: %v\n", err) @@ -212,7 +241,7 @@ func checkServiceStatus() { if osType == "darwin" { // MacOS launchd command - cmd := exec.Command("sudo", "launchctl", "list", fmt.Sprintf("com.quilibrium.%s", serviceName)) + cmd := exec.Command("sudo", "launchctl", "list", fmt.Sprintf("com.quilibrium.%s", ServiceName)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { @@ -220,7 +249,7 @@ func checkServiceStatus() { } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "status", serviceName) + cmd := exec.Command("sudo", "systemctl", "status", ServiceName) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { @@ -238,7 +267,7 @@ func enableService() { if osType == "darwin" { // MacOS launchd command - load with -w flag to enable at boot - plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName) + plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) cmd := exec.Command("sudo", "launchctl", "load", "-w", plistPath) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error enabling service: %v\n", err) @@ -246,7 +275,7 @@ func enableService() { } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "enable", serviceName) + cmd := exec.Command("sudo", "systemctl", "enable", ServiceName) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error enabling service: %v\n", err) return @@ -265,7 +294,7 @@ func disableService() { if osType == "darwin" { // MacOS launchd command - unload with -w flag to disable at boot - plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName) + plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", ServiceName) cmd := exec.Command("sudo", "launchctl", "unload", "-w", plistPath) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error disabling service: %v\n", err) @@ -273,7 +302,7 @@ func disableService() { } } else { // Linux systemd command - cmd := exec.Command("sudo", "systemctl", "disable", serviceName) + cmd := exec.Command("sudo", "systemctl", "disable", ServiceName) if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error disabling service: %v\n", err) return @@ -282,3 +311,189 @@ func disableService() { fmt.Fprintf(os.Stdout, "Disabled Quilibrium node service from starting on boot\n") } + +func createService() { + // Create systemd service file + 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" { + installMacOSService() + } else { + fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", osType) + return + } +} + +// createSystemdServiceFile creates the systemd service file with environment file support +func createSystemdServiceFile() error { + if !CheckForSystemd() { + installSystemd() + } + + // Check if we need sudo privileges + if err := utils.CheckAndRequestSudo("Creating systemd service file requires root privileges"); err != nil { + 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` + + // Write environment file + envPath := filepath.Join(utils.RootQuilibriumPath, "quilibrium.env") + if err := os.WriteFile(envPath, []byte(envContent), 0640); err != nil { + return fmt.Errorf("failed to create environment file: %w", err) + } + + // Set ownership of environment file + chownCmd := utils.ChownPath(envPath, userLookup, false) + if chownCmd != nil { + return fmt.Errorf("failed to set environment file ownership: %w", chownCmd) + } + + // Create systemd service file content + serviceContent := fmt.Sprintf(`[Unit] +Description=Quilibrium Node Service +After=network.target + +[Service] +Type=simple +User=quilibrium +EnvironmentFile=/opt/quilibrium/config/quilibrium.env +ExecStart=/usr/local/bin/quilibrium-node --config %s +Restart=on-failure +RestartSec=10 +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target +`, ConfigDirs+"/default") + + // Write service file + servicePath := "/etc/systemd/system/quilibrium-node.service" + if err := utils.WriteFileAuto(servicePath, serviceContent); err != nil { + return fmt.Errorf("failed to create service file: %w", err) + } + + // Reload systemd daemon + reloadCmd := exec.Command("sudo", "systemctl", "daemon-reload") + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("failed to reload systemd daemon: %w", err) + } + + fmt.Fprintf(os.Stdout, "Created systemd service file at %s\n", servicePath) + fmt.Fprintf(os.Stdout, "Created environment file at %s\n", envPath) + return nil +} + +// installMacOSService installs a launchd service on macOS +func installMacOSService() { + fmt.Println("Installing launchd service for Quilibrium node...") + + // Create plist file content + plistTemplate := ` + + + + Label + {{.Label}} + ProgramArguments + + /usr/local/bin/quilibrium-node + --config + /opt/quilibrium/config/ + + EnvironmentVariables + + QUILIBRIUM_DATA_DIR + {{.DataPath}} + QUILIBRIUM_LOG_LEVEL + info + QUILIBRIUM_LISTEN_GRPC_MULTIADDR + /ip4/127.0.0.1/tcp/8337 + QUILIBRIUM_LISTEN_REST_MULTIADDR + /ip4/127.0.0.1/tcp/8338 + QUILIBRIUM_STATS_MULTIADDR + /dns/stats.quilibrium.com/tcp/443 + QUILIBRIUM_NETWORK_ID + 0 + QUILIBRIUM_DEBUG + false + QUILIBRIUM_SIGNATURE_CHECK + true + + RunAtLoad + + KeepAlive + + StandardErrorPath + {{.LogPath}}/node.err + StandardOutPath + {{.LogPath}}/node.log + +` + + // Prepare template data + data := struct { + Label string + DataPath string + ServiceName string + LogPath string + }{ + Label: fmt.Sprintf("com.quilibrium.node"), + DataPath: utils.NodeDataPath, + ServiceName: "node", + LogPath: logPath, + } + + // Parse and execute template + tmpl, err := template.New("plist").Parse(plistTemplate) + if err != nil { + fmt.Printf("Error creating plist template: %v\n", err) + return + } + + // Determine plist file path + var plistPath = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", data.Label) + + // Write plist file + file, err := os.Create(plistPath) + if err != nil { + fmt.Printf("Error creating plist file: %v\n", err) + return + } + defer file.Close() + + if err := tmpl.Execute(file, data); err != nil { + fmt.Printf("Error writing plist file: %v\n", err) + return + } + + // Set correct permissions + chownCmd := exec.Command("chown", "root:wheel", plistPath) + if err := chownCmd.Run(); err != nil { + fmt.Printf("Warning: Failed to change ownership of plist file: %v\n", err) + } + + // Load the service + var loadCmd = exec.Command("launchctl", "load", "-w", plistPath) + + if err := loadCmd.Run(); err != nil { + fmt.Printf("Error loading service: %v\n", err) + fmt.Println("You may need to load the service manually.") + } + + fmt.Printf("Launchd service installed successfully as %s\n", plistPath) + fmt.Println("\nTo start the service:") + fmt.Printf(" sudo launchctl start %s\n", data.Label) + fmt.Println("\nTo stop the service:") + fmt.Printf(" sudo launchctl stop %s\n", data.Label) + fmt.Println("\nTo view service logs:") + fmt.Printf(" cat %s/%s.log\n", data.LogPath, data.ServiceName) +} diff --git a/client/cmd/node/shared.go b/client/cmd/node/shared.go index 568e032..b23003e 100644 --- a/client/cmd/node/shared.go +++ b/client/cmd/node/shared.go @@ -1,13 +1,10 @@ package node import ( - "bufio" "fmt" "os" - "os/exec" "path/filepath" "strings" - "text/template" "source.quilibrium.com/quilibrium/monorepo/client/utils" ) @@ -21,122 +18,22 @@ func determineVersion(args []string) string { } // confirmPaths asks the user to confirm the installation and data paths -func confirmPaths(installPath, dataPath string) bool { - fmt.Print("Do you want to continue with these paths? [Y/n]: ") - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - response = strings.TrimSpace(strings.ToLower(response)) +// func confirmPaths(installPath, dataPath string) bool { +// fmt.Print("Do you want to continue with these paths? [Y/n]: ") +// reader := bufio.NewReader(os.Stdin) +// response, _ := reader.ReadString('\n') +// response = strings.TrimSpace(strings.ToLower(response)) - return response == "" || response == "y" || response == "yes" -} - -// createNodeUser creates a dedicated user for running the node -func createNodeUser() 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) - } - - var cmd *exec.Cmd - - if osType == "linux" { - // Check if user already exists - checkCmd := exec.Command("id", nodeUser) - if checkCmd.Run() == nil { - fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser) - return nil - } - - // Create user on Linux - cmd = exec.Command("useradd", "-r", "-s", "/bin/false", "-m", "-c", "Quilibrium Node User", nodeUser) - } else if osType == "darwin" { - // Check if user already exists on macOS - checkCmd := exec.Command("dscl", ".", "-read", "/Users/"+nodeUser) - if checkCmd.Run() == nil { - fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser) - 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 - } else { - return fmt.Errorf("user creation not supported on %s", osType) - } - - // Run the command - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - - fmt.Fprintf(os.Stdout, "User '%s' created successfully\n", nodeUser) - return nil -} +// return response == "" || response == "y" || response == "yes" +// } // setOwnership sets the ownership of directories to the node user func setOwnership() { - fmt.Fprintf(os.Stdout, "Setting ownership of %s and %s to %s...\n", installPath, dataPath, nodeUser) // Change ownership of installation directory - chownCmd := exec.Command("chown", "-R", nodeUser+":"+nodeUser, installPath) - if err := chownCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", installPath, err) - } - - // Change ownership of data directory - chownCmd = exec.Command("chown", "-R", nodeUser+":"+nodeUser, dataPath) - if err := chownCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", dataPath, err) + err := utils.ChownPath(utils.NodeDataPath, NodeUser, true) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", utils.NodeDataPath, err) } } @@ -159,22 +56,22 @@ func setupLogRotation() error { postrotate systemctl reload quilibrium-node >/dev/null 2>&1 || true endscript -}`, logPath, nodeUser, nodeUser) +}`, logPath, NodeUser.Username, NodeUser.Username) // Write the configuration file configPath := "/etc/logrotate.d/quilibrium-node" - if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + if err := utils.WriteFile(configPath, configContent); err != nil { return fmt.Errorf("failed to create logrotate configuration: %w", err) } // Create log directory with proper permissions - if err := os.MkdirAll(logPath, 0750); err != nil { + if err := utils.ValidateAndCreateDir(logPath, NodeUser); err != nil { return fmt.Errorf("failed to create log directory: %w", err) } // Set ownership of log directory - chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, logPath) - if err := chownCmd.Run(); err != nil { + err := utils.ChownPath(logPath, NodeUser, true) + if err != nil { return fmt.Errorf("failed to set log directory ownership: %w", err) } @@ -183,14 +80,17 @@ func setupLogRotation() error { } // finishInstallation completes the installation process by making the binary executable and creating a symlink -func finishInstallation(nodeBinaryPath string, version string) { - +func finishInstallation(version string) { setOwnership() + normalizedBinaryName := "node-" + version + "-" + osType + "-" + arch + + // Finish installation + nodeBinaryPath := filepath.Join(utils.NodeDataPath, version, normalizedBinaryName) + fmt.Printf("Making binary executable: %s\n", nodeBinaryPath) // Make the binary executable - if err := os.Chmod(nodeBinaryPath, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Error making binary executable: %v\n", err) - return + if err := utils.ChmodPath(nodeBinaryPath, 0755, "executable"); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to make binary executable: %v\n", err) } // Check if we need sudo privileges for creating symlink in system directory @@ -211,18 +111,6 @@ func finishInstallation(nodeBinaryPath string, version string) { fmt.Fprintf(os.Stderr, "Warning: Failed to set up log rotation: %v\n", err) } - // Create systemd service file - 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" { - installMacOSService() - } else { - fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", osType) - return - } - // Print success message printSuccessMessage(version) } @@ -230,8 +118,7 @@ func finishInstallation(nodeBinaryPath string, 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, "Installation directory: %s\n", installPath) - fmt.Fprintf(os.Stdout, "Data directory: %s\n", dataPath) + fmt.Fprintf(os.Stdout, "Binary download directory: %s\n", 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") @@ -239,173 +126,10 @@ func printSuccessMessage(version string) { fmt.Fprintf(os.Stdout, "\nTo start the node, you can run:\n") fmt.Fprintf(os.Stdout, " %s --config %s/config/config.yaml\n", - defaultSymlinkPath, dataPath) + ServiceName, ConfigDirs) fmt.Fprintf(os.Stdout, " # Or use systemd service:\n") fmt.Fprintf(os.Stdout, " sudo systemctl start quilibrium-node\n") fmt.Fprintf(os.Stdout, "\nFor more options, run:\n") fmt.Fprintf(os.Stdout, " quilibrium-node --help\n") } - -// createSystemdServiceFile creates the systemd service file with environment file support -func createSystemdServiceFile() error { - // Check if we need sudo privileges - if err := utils.CheckAndRequestSudo("Creating systemd service file requires root privileges"); err != nil { - return fmt.Errorf("failed to get sudo privileges: %w", err) - } - - // Create environment file content - envContent := fmt.Sprintf(`# Quilibrium Node Environment`, dataPath) - - // Write environment file - envPath := filepath.Join(dataPath, "config", "quilibrium.env") - if err := os.WriteFile(envPath, []byte(envContent), 0640); err != nil { - return fmt.Errorf("failed to create environment file: %w", err) - } - - // Set ownership of environment file - chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, envPath) - if err := chownCmd.Run(); err != nil { - return fmt.Errorf("failed to set environment file ownership: %w", err) - } - - // Create systemd service file content - serviceContent := fmt.Sprintf(`[Unit] -Description=Quilibrium Node Service -After=network.target - -[Service] -Type=simple -User=quilibrium -EnvironmentFile=/opt/quilibrium/config/quilibrium.env -ExecStart=/usr/local/bin/quilibrium-node --config /opt/quilibrium/config -Restart=on-failure -RestartSec=10 -LimitNOFILE=65535 - -[Install] -WantedBy=multi-user.target -`, nodeUser, defaultSymlinkPath, dataPath) - - // Write service file - servicePath := "/etc/systemd/system/quilibrium-node.service" - if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil { - return fmt.Errorf("failed to create service file: %w", err) - } - - // Reload systemd daemon - reloadCmd := exec.Command("systemctl", "daemon-reload") - if err := reloadCmd.Run(); err != nil { - return fmt.Errorf("failed to reload systemd daemon: %w", err) - } - - fmt.Fprintf(os.Stdout, "Created systemd service file at %s\n", servicePath) - fmt.Fprintf(os.Stdout, "Created environment file at %s\n", envPath) - return nil -} - -// installMacOSService installs a launchd service on macOS -func installMacOSService() { - fmt.Println("Installing launchd service for Quilibrium node...") - - // Create plist file content - plistTemplate := ` - - - - Label - {{.Label}} - ProgramArguments - - /usr/local/bin/quilibrium-node - --config - /opt/quilibrium/config/ - - EnvironmentVariables - - QUILIBRIUM_DATA_DIR - {{.DataPath}} - QUILIBRIUM_LOG_LEVEL - info - QUILIBRIUM_LISTEN_GRPC_MULTIADDR - /ip4/127.0.0.1/tcp/8337 - QUILIBRIUM_LISTEN_REST_MULTIADDR - /ip4/127.0.0.1/tcp/8338 - QUILIBRIUM_STATS_MULTIADDR - /dns/stats.quilibrium.com/tcp/443 - QUILIBRIUM_NETWORK_ID - 0 - QUILIBRIUM_DEBUG - false - QUILIBRIUM_SIGNATURE_CHECK - true - - RunAtLoad - - KeepAlive - - StandardErrorPath - {{.LogPath}}/node.err - StandardOutPath - {{.LogPath}}/node.log - -` - - // Prepare template data - data := struct { - Label string - DataPath string - ServiceName string - LogPath string - }{ - Label: fmt.Sprintf("com.quilibrium.node"), - DataPath: dataPath, - ServiceName: "node", - LogPath: logPath, - } - - // Parse and execute template - tmpl, err := template.New("plist").Parse(plistTemplate) - if err != nil { - fmt.Printf("Error creating plist template: %v\n", err) - return - } - - // Determine plist file path - var plistPath = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", data.Label) - - // Write plist file - file, err := os.Create(plistPath) - if err != nil { - fmt.Printf("Error creating plist file: %v\n", err) - return - } - defer file.Close() - - if err := tmpl.Execute(file, data); err != nil { - fmt.Printf("Error writing plist file: %v\n", err) - return - } - - // Set correct permissions - chownCmd := exec.Command("chown", "root:wheel", plistPath) - if err := chownCmd.Run(); err != nil { - fmt.Printf("Warning: Failed to change ownership of plist file: %v\n", err) - } - - // Load the service - var loadCmd = exec.Command("launchctl", "load", "-w", plistPath) - - if err := loadCmd.Run(); err != nil { - fmt.Printf("Error loading service: %v\n", err) - fmt.Println("You may need to load the service manually.") - } - - fmt.Printf("Launchd service installed successfully as %s\n", plistPath) - fmt.Println("\nTo start the service:") - fmt.Printf(" sudo launchctl start %s\n", data.Label) - fmt.Println("\nTo stop the service:") - fmt.Printf(" sudo launchctl stop %s\n", data.Label) - fmt.Println("\nTo view service logs:") - fmt.Printf(" cat %s/%s.log\n", data.LogPath, data.ServiceName) -} diff --git a/client/cmd/node/update.go b/client/cmd/node/update.go index 01e602c..f464fbb 100644 --- a/client/cmd/node/update.go +++ b/client/cmd/node/update.go @@ -44,21 +44,14 @@ func init() { // updateNode handles the node update process func updateNode(version string) { // Check if we need sudo privileges - if err := utils.CheckAndRequestSudo(fmt.Sprintf("Updating node at %s requires root privileges", installPath)); err != nil { + if err := utils.CheckAndRequestSudo(fmt.Sprintf("Updating node at %s requires root privileges", utils.NodeDataPath)); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) return } - // Create version-specific installation directory - versionDir := filepath.Join(installPath, "node", version) - if err := os.MkdirAll(versionDir, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err) - return - } - - // Create data directory - versionDataDir := filepath.Join(dataPath, "node", version) - if err := os.MkdirAll(versionDataDir, 0755); err != nil { + // Create new binary version directory + versionDataDir := filepath.Join(utils.NodeDataPath, version) + if err := utils.ValidateAndCreateDir(versionDataDir, nil); err != nil { fmt.Fprintf(os.Stderr, "Error creating data directory: %v\n", err) return } @@ -66,16 +59,18 @@ func updateNode(version string) { // Construct the expected filename for the specified version // Remove 'v' prefix if present for filename construction versionWithoutV := strings.TrimPrefix(version, "v") - fileName := fmt.Sprintf("node-%s-%s-%s", versionWithoutV, osType, arch) + + if IsExistingNodeVersion(versionWithoutV) { + fmt.Fprintf(os.Stderr, "Error: Node version %s already exists\n", versionWithoutV) + os.Exit(1) + } // Download the release directly - nodeBinaryPath := filepath.Join(dataPath, version, fileName) 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) // Clean up the created directories since installation failed - os.RemoveAll(versionDir) os.RemoveAll(versionDataDir) return } @@ -92,5 +87,5 @@ func updateNode(version string) { } // Successfully downloaded the specific version - finishInstallation(nodeBinaryPath, version) + finishInstallation(version) } diff --git a/client/cmd/node/user.go b/client/cmd/node/user.go new file mode 100644 index 0000000..672e085 --- /dev/null +++ b/client/cmd/node/user.go @@ -0,0 +1,112 @@ +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 073211d..f17597b 100644 --- a/client/cmd/node/utils.go +++ b/client/cmd/node/utils.go @@ -2,11 +2,17 @@ package node import ( "encoding/hex" + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" "github.com/pkg/errors" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "source.quilibrium.com/quilibrium/monorepo/client/utils" "source.quilibrium.com/quilibrium/monorepo/node/config" ) @@ -39,3 +45,36 @@ func GetPrivKeyFromConfig(cfg *config.Config) (crypto.PrivKey, error) { privKey, err := crypto.UnmarshalEd448PrivateKey(peerPrivKey) return privKey, err } + +func IsExistingNodeVersion(version string) bool { + return utils.FileExists(filepath.Join(utils.NodeDataPath, version)) +} + +func CheckForSystemd() bool { + // Check if systemctl command exists + _, 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 dc79b47..9b82c54 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -41,7 +41,7 @@ It provides commands for installing, updating, and managing Quilibrium nodes.`, return } - if !utils.FileExists(utils.ClientConfigPath) { + if !utils.FileExists(utils.GetConfigPath()) { fmt.Println("Client config not found, creating default config") utils.CreateDefaultConfig() } @@ -85,9 +85,8 @@ It provides commands for installing, updating, and managing Quilibrium nodes.`, // Fall back to checking next to executable digest, err = os.ReadFile(ex + ".dgst") if err != nil { + fmt.Println("") fmt.Println("The digest file was not found. Do you want to continue without signature verification? (y/n)") - fmt.Println("The signature files (if they exist)can be downloaded with the 'qclient download-signatures' command") - fmt.Println("You can also use --signature-check=false in your command to skip this prompt") reader := bufio.NewReader(os.Stdin) response, _ := reader.ReadString('\n') @@ -95,10 +94,19 @@ It provides commands for installing, updating, and managing Quilibrium nodes.`, if response != "y" && response != "yes" { fmt.Println("Exiting due to missing digest file") + fmt.Println("The signature files (if they exist) can be downloaded with the 'qclient download-signatures' command") + fmt.Println("You can also skip this prompt manually by using the --signature-check=false flag or to permanently disable signature checks running 'qclient config signature-check false'") + os.Exit(1) } + // Check if the user is trying to run the config command to disable/enable signature checks + if len(os.Args) >= 3 && os.Args[1] == "config" && os.Args[2] != "signature-check" { + fmt.Println("The signature files (if they exist) can be downloaded with the 'qclient download-signatures' command") + fmt.Println("You can also skip this prompt manually by using the --signature-check=false flag or to permanently disable signature checks running 'qclient config signature-check false'") + } fmt.Println("Continuing without signature verification") + signatureCheck = false } } @@ -184,12 +192,6 @@ func signatureCheckDefault() bool { } func init() { - rootCmd.PersistentFlags().BoolVar( - &DryRun, - "dry-run", - false, - "runs the command (if applicable) without actually mutating state (printing effect output)", - ) rootCmd.PersistentFlags().BoolVar( &signatureCheck, "signature-check", @@ -197,6 +199,27 @@ func init() { "bypass signature check (not recommended for binaries) (default true or value of QUILIBRIUM_SIGNATURE_CHECK env var)", ) + rootCmd.PersistentFlags().BoolP( + "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/test/README.md b/client/test/README.md index 0e1a56f..18aaac4 100644 --- a/client/test/README.md +++ b/client/test/README.md @@ -146,6 +146,15 @@ sudo task build_qclient_arm64_linux # for mac, you will need to build on a mac ``` +## Run a test container + +``` +sudo docker run -it \ + -v "/home/user/ceremonyclient/client/test/:/app" \ + -v "/home/user/ceremenoyclient/client/build/amd64_linux/qclient:/opt/quilibrium/bin/qclient" \ + quil-test /bin/bash + + This command builds the Docker image with the qclient binary according to the specifications in `Dockerfile.source`. The resulting image will be tagged as `qclient`. ## Contributing diff --git "a/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" "b/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" new file mode 100644 index 0000000..2beed71 --- /dev/null +++ "b/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" @@ -0,0 +1,3 @@ +dataDir: /var/quilibrium/data/qclient +symlinkPath: /usr/local/bin/qclient +signatureCheck: true diff --git a/client/test/test_install.sh b/client/test/test_install.sh index aed7cc1..955694a 100755 --- a/client/test/test_install.sh +++ b/client/test/test_install.sh @@ -20,6 +20,19 @@ else exit 1 fi +# Test: Link the qclient binary to the system PATH +echo "Testing qclient link command..." +run_test_with_format "sudo /opt/quilibrium/bin/qclient link" + +# Verify qclient is now in PATH +echo "Verifying qclient is in PATH after link command..." +run_test_with_format "which qclient" | grep -q "/usr/local/bin/qclient" && echo "SUCCESS: qclient found in PATH" || echo "FAILURE: qclient not found in PATH" + +# Test qclient can be executed directly +echo "Testing qclient can be executed directly..." +run_test_with_format "qclient --help" | grep -q "Usage:" && echo "SUCCESS: qclient executable works" || echo "FAILURE: qclient executable not working properly" + + # Test: Ensure no config file exists initially echo "Testing no config file exists initially..." run_test_with_format "test ! -f /etc/quilibrium/config/qclient.yaml" diff --git a/client/utils/config.go b/client/utils/config.go index 4b2b346..93be350 100644 --- a/client/utils/config.go +++ b/client/utils/config.go @@ -8,25 +8,34 @@ import ( "gopkg.in/yaml.v2" ) -var ClientConfigDir = filepath.Join("/etc/quilibrium/", "config") -var ClientConfigFile = string(ReleaseTypeQClient) + ".yaml" +var ClientConfigDir = filepath.Join(os.Getenv("HOME"), ".quilibrium") +var ClientConfigFile = string(ReleaseTypeQClient) + "-config.yaml" var ClientConfigPath = filepath.Join(ClientConfigDir, ClientConfigFile) -// var clientConfig = &ClientConfig{} - func CreateDefaultConfig() { - fmt.Printf("Creating default config: %s\n", ClientConfigPath) + configPath := GetConfigPath() + + fmt.Printf("Creating default config: %s\n", configPath) SaveClientConfig(&ClientConfig{ DataDir: ClientDataPath, SymlinkPath: DefaultQClientSymlinkPath, SignatureCheck: true, }) + + sudoUser, err := GetCurrentSudoUser() + if err != nil { + fmt.Println("Error getting current sudo user") + os.Exit(1) + } + ChownPath(GetUserQuilibriumDir(), sudoUser, true) } // LoadClientConfig loads the client configuration from the config file func LoadClientConfig() (*ClientConfig, error) { + configPath := GetConfigPath() + // Create default config if it doesn't exist - if _, err := os.Stat(ClientConfigPath); os.IsNotExist(err) { + if _, err := os.Stat(configPath); os.IsNotExist(err) { config := &ClientConfig{ DataDir: ClientDataPath, SymlinkPath: filepath.Join(ClientDataPath, "current"), @@ -39,7 +48,7 @@ func LoadClientConfig() (*ClientConfig, error) { } // Read existing config - data, err := os.ReadFile(ClientConfigPath) + data, err := os.ReadFile(configPath) if err != nil { return nil, err } @@ -60,16 +69,20 @@ func SaveClientConfig(config *ClientConfig) error { } // Ensure the config directory exists - if err := os.MkdirAll(ClientConfigDir, 0755); err != nil { + if err := os.MkdirAll(GetConfigDir(), 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } - return os.WriteFile(ClientConfigPath, data, 0644) + return os.WriteFile(GetConfigPath(), data, 0644) } // GetConfigPath returns the path to the client configuration file func GetConfigPath() string { - return ClientConfigPath + return filepath.Join(GetConfigDir(), ClientConfigFile) +} + +func GetConfigDir() string { + return filepath.Join(GetUserQuilibriumDir()) } // IsClientConfigured checks if the client is configured diff --git a/client/utils/download.go b/client/utils/download.go index 7f77274..9845539 100644 --- a/client/utils/download.go +++ b/client/utils/download.go @@ -53,7 +53,7 @@ func GetLatestVersion(releaseType ReleaseType) (string, error) { // DownloadReleaseFile downloads a release file from the Quilibrium releases server func DownloadReleaseFile(releaseType ReleaseType, fileName string, version string, showError bool) error { url := fmt.Sprintf("%s/%s", BaseReleaseURL, fileName) - destDir := filepath.Join(DataPath, string(releaseType), version) + destDir := filepath.Join(BinaryPath, string(releaseType), version) os.MkdirAll(destDir, 0755) destPath := filepath.Join(destDir, fileName) @@ -109,11 +109,13 @@ func DownloadReleaseSignatures(releaseType ReleaseType, version string) error { // Skip this file if it doesn't exist on the server fmt.Printf("Signature file %s not found on server, skipping\n", sigFile) 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)) } - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - files = append(files, fmt.Sprintf("%s.dgst.sig.%d", baseName, num)) } if len(files) == 0 { diff --git a/client/utils/fileUtils.go b/client/utils/fileUtils.go index 2f92f79..61c076d 100644 --- a/client/utils/fileUtils.go +++ b/client/utils/fileUtils.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "os" + "os/exec" + "os/user" "path/filepath" "runtime" ) @@ -15,9 +17,10 @@ import ( var DefaultNodeUser = "quilibrium" var ClientInstallPath = filepath.Join("/opt/quilibrium/", string(ReleaseTypeQClient)) -var DataPath = filepath.Join("/var/quilibrium/", "data") -var ClientDataPath = filepath.Join(DataPath, string(ReleaseTypeQClient)) -var NodeDataPath = filepath.Join(DataPath, string(ReleaseTypeNode)) +var RootQuilibriumPath = filepath.Join("/var/quilibrium/") +var BinaryPath = filepath.Join(RootQuilibriumPath, "bin") +var ClientDataPath = filepath.Join(BinaryPath, string(ReleaseTypeQClient)) +var NodeDataPath = filepath.Join(BinaryPath, string(ReleaseTypeNode)) var DefaultSymlinkDir = "/usr/local/bin" var DefaultNodeSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeNode)) var DefaultQClientSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeQClient)) @@ -68,6 +71,8 @@ func CreateSymlink(execPath, targetPath string) error { } } + fmt.Printf("Creating symlink %s -> %s\n", targetPath, execPath) + // Create the symlink if err := os.Symlink(execPath, targetPath); err != nil { return fmt.Errorf("failed to create symlink: %w", err) @@ -77,7 +82,7 @@ func CreateSymlink(execPath, targetPath string) error { } // ValidateAndCreateDir validates a directory path and creates it if it doesn't exist -func ValidateAndCreateDir(path string) error { +func ValidateAndCreateDir(path string, user *user.User) error { // Check if the directory exists info, err := os.Stat(path) if err == nil { @@ -90,9 +95,13 @@ func ValidateAndCreateDir(path string) error { // Directory doesn't exist, try to create it if os.IsNotExist(err) { + fmt.Printf("Creating directory %s\n", path) if err := os.MkdirAll(path, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %v", path, err) } + if user != nil { + ChownPath(path, user, false) + } return nil } @@ -135,3 +144,97 @@ func FileExists(path string) bool { _, err := os.Stat(path) return !os.IsNotExist(err) } + +func IsSudo() bool { + user, err := user.Current() + if err != nil { + return false + } + return user.Username == "root" +} + +// ChownPath changes the owner of a file or directory to the specified user +func ChownPath(path string, user *user.User, isRecursive bool) error { + // Change ownership of the path + if isRecursive { + fmt.Printf("Changing ownership of %s (recursive) to %s\n", path, user.Username) + if err := exec.Command("chown", "-R", user.Uid+":"+user.Gid, path).Run(); err != nil { + return fmt.Errorf("failed to change ownership of %s to %s (requires sudo): %v", path, user.Uid, err) + } + } else { + fmt.Printf("Changing ownership of %s to %s\n", path, user.Username) + if err := exec.Command("chown", user.Uid+":"+user.Gid, path).Run(); err != nil { + return fmt.Errorf("failed to change ownership of %s to %s (requires sudo): %v", path, user.Uid, err) + } + } + + return nil +} + +func ChmodPath(path string, mode os.FileMode, description string) error { + fmt.Printf("Changing path: %s to %s (%s)\n", path, mode, description) + return os.Chmod(path, mode) +} + +func WriteFile(path string, content string) error { + return os.WriteFile(path, []byte(content), 0644) +} + +// WriteFileAuto writes content to a file, automatically using sudo only if necessary +func WriteFileAuto(path string, content string) error { + // First check if file exists and is writable + if FileExists(path) { + // Try to open the file for writing to check permissions + file, err := os.OpenFile(path, os.O_WRONLY, 0) + if err == nil { + // File is writable, close it and write normally + file.Close() + fmt.Printf("Writing to file %s using normal permissions\n", path) + return os.WriteFile(path, []byte(content), 0644) + } + } else { + // Check if parent directory is writable + dir := filepath.Dir(path) + if IsWritable(dir) { + fmt.Printf("Writing to file %s using normal permissions\n", path) + return os.WriteFile(path, []byte(content), 0644) + } + } + + // If we reach here, sudo is needed + fmt.Printf("Writing to file %s using sudo\n", path) + cmd := exec.Command("sudo", "tee", path) + stdin, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("failed to get stdin pipe: %w", err) + } + + // Start the command + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start sudo command: %w", err) + } + + // Write content to stdin + if _, err := io.WriteString(stdin, content); err != nil { + return fmt.Errorf("failed to write to stdin: %w", err) + } + stdin.Close() + + // Wait for the command to finish + if err := cmd.Wait(); err != nil { + return fmt.Errorf("sudo tee command failed: %w", err) + } + + return nil +} + +// CopyFile copies a file from src to dst +func CopyFile(src, dst string) error { + fmt.Printf("Copying file from %s to %s\n", src, dst) + sourceData, err := os.ReadFile(src) + if err != nil { + return err + } + + return os.WriteFile(dst, sourceData, 0600) +} diff --git a/client/utils/system.go b/client/utils/system.go index b76289d..a87ee6b 100644 --- a/client/utils/system.go +++ b/client/utils/system.go @@ -1,8 +1,14 @@ package utils import ( + "bytes" "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" "runtime" + "strings" ) // GetSystemInfo determines and validates the OS and architecture @@ -26,3 +32,35 @@ func GetSystemInfo() (string, string, error) { return osType, arch, nil } + +func GetCurrentSudoUser() (*user.User, error) { + if os.Geteuid() != 0 { + return user.Current() + } + + cmd := exec.Command("sh", "-c", "env | grep SUDO_USER | cut -d= -f2 | cut -d\\n -f1") + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("failed to get current sudo user: %v", err) + } + + userLookup, err := user.Lookup(strings.TrimSpace(out.String())) + if err != nil { + return nil, fmt.Errorf("failed to get current sudo user: %v", err) + } + return userLookup, nil +} + +func GetUserQuilibriumDir() string { + sudoUser, err := GetCurrentSudoUser() + if err != nil { + fmt.Println("Error getting current sudo user") + os.Exit(1) + } + + return filepath.Join(sudoUser.HomeDir, ".quilibrium") +}