update install and node commands

This commit is contained in:
Tyler Sturos 2025-04-09 22:00:36 -08:00
parent 27ed7a775d
commit df47bcc2d2
25 changed files with 1293 additions and 537 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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 := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{{.Label}}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/quilibrium-node</string>
<string>--config</string>
<string>/opt/quilibrium/config/</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>QUILIBRIUM_DATA_DIR</key>
<string>{{.DataPath}}</string>
<key>QUILIBRIUM_LOG_LEVEL</key>
<string>info</string>
<key>QUILIBRIUM_LISTEN_GRPC_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8337</string>
<key>QUILIBRIUM_LISTEN_REST_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8338</string>
<key>QUILIBRIUM_STATS_MULTIADDR</key>
<string>/dns/stats.quilibrium.com/tcp/443</string>
<key>QUILIBRIUM_NETWORK_ID</key>
<string>0</string>
<key>QUILIBRIUM_DEBUG</key>
<string>false</string>
<key>QUILIBRIUM_SIGNATURE_CHECK</key>
<string>true</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>{{.LogPath}}/node.err</string>
<key>StandardOutPath</key>
<string>{{.LogPath}}/node.log</string>
</dict>
</plist>`
// 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)
}

View File

@ -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 := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{{.Label}}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/quilibrium-node</string>
<string>--config</string>
<string>/opt/quilibrium/config/</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>QUILIBRIUM_DATA_DIR</key>
<string>{{.DataPath}}</string>
<key>QUILIBRIUM_LOG_LEVEL</key>
<string>info</string>
<key>QUILIBRIUM_LISTEN_GRPC_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8337</string>
<key>QUILIBRIUM_LISTEN_REST_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8338</string>
<key>QUILIBRIUM_STATS_MULTIADDR</key>
<string>/dns/stats.quilibrium.com/tcp/443</string>
<key>QUILIBRIUM_NETWORK_ID</key>
<string>0</string>
<key>QUILIBRIUM_DEBUG</key>
<string>false</string>
<key>QUILIBRIUM_SIGNATURE_CHECK</key>
<string>true</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>{{.LogPath}}/node.err</string>
<key>StandardOutPath</key>
<string>{{.LogPath}}/node.log</string>
</dict>
</plist>`
// 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)
}

View File

@ -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)
}

112
client/cmd/node/user.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,3 @@
dataDir: /var/quilibrium/data/qclient
symlinkPath: /usr/local/bin/qclient
signatureCheck: true

View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

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