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")
+}