Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-restart to service install and the .deb package #180

Merged
merged 7 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions hyperdrive-cli/client/global-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,18 +274,33 @@ func (c *GlobalConfig) GetChanges(oldConfig *GlobalConfig) ([]*config.ChangedSec

// Process all configs for changes
sectionList = getChanges(oldConfig.Hyperdrive, c.Hyperdrive, sectionList, changedContainers)
if c.StakeWise.Enabled.Value {
if c.StakeWise.Enabled.Value || oldConfig.StakeWise.Enabled.Value {
sectionList = getChanges(oldConfig.StakeWise, c.StakeWise, sectionList, changedContainers)
}
if c.Constellation.Enabled.Value {
if c.Constellation.Enabled.Value || oldConfig.Constellation.Enabled.Value {
sectionList = getChanges(oldConfig.Constellation, c.Constellation, sectionList, changedContainers)
}

// Add all VCs to the list of changed containers if any change requires a VC change
if changedContainers[config.ContainerID_ValidatorClient] {
delete(changedContainers, config.ContainerID_ValidatorClient)
oldConfigs := oldConfig.GetAllModuleConfigs()
for _, module := range c.GetAllModuleConfigs() {
includeVc := true
if !module.IsEnabled() {
// If it's not enabled, see if it was enabled before so it's been disabled now
for _, oldModule := range oldConfigs {
if oldModule.GetModuleName() != module.GetModuleName() {
continue
}
if !oldModule.IsEnabled() {
// If it's disabled now and was disabled before, ignore the VC
includeVc = false
}
break
}
}
if !includeVc {
continue
}
vcInfo := module.GetValidatorContainerTagInfo()
Expand Down
54 changes: 54 additions & 0 deletions hyperdrive-cli/client/package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package client

import (
"fmt"
"os"
"os/exec"
"os/user"
"strings"
)

// Starts Hyperdrive as the provided user via `su`
func StartServiceAsUser(uid uint32, hyperdriveBin string, configPath string) bool {
// Get the user from the uid
user, err := user.LookupId(fmt.Sprintf("%d", uid))
if err != nil {
fmt.Printf("ERROR: Could not find user with ID %d: %s\n", uid, err.Error())
return false
}
name := user.Username

// Prep the hyperdrive command
hyperdriveCmd := []string{hyperdriveBin}
if uid == 0 {
hyperdriveCmd = append(hyperdriveCmd, "--allow-root")
}
hyperdriveCmd = append(hyperdriveCmd, "--config-path", configPath, "service", "start", "-y")
hyperdriveCmdStr := strings.Join(hyperdriveCmd, " ")

// Run `su` to start Hyperdrive as the user
cmd := &command{
cmd: exec.Command("su", "-l", name, "-c", hyperdriveCmdStr),
}
err = runStartServiceCommand(cmd)
if err == nil {
return true
}

fmt.Printf("WARN: Error starting Hyperdrive as user '%s' (%d): %s\n", name, uid, err.Error())
return false
}

// Runs the given service start command
func runStartServiceCommand(cmd *command) error {
cmd.SetStdin(os.Stdin)
cmd.SetStdout(os.Stdout)
cmd.SetStderr(os.Stderr)

// Run the command
err := cmd.Start()
if err != nil {
return err
}
return cmd.Wait()
}
5 changes: 4 additions & 1 deletion hyperdrive-cli/commands/nodeset/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func CheckRegistrationStatus(c *cli.Context, hd *client.HyperdriveClient) (bool,
}

// Prompt for registration
if !(c.Bool(utils.YesFlag.Name) || utils.Confirm("Would you like to register your node now?")) {
if c.Bool(utils.YesFlag.Name) {
return false, nil
}
if !utils.Confirm("Would you like to register your node now?") {
fmt.Println("Cancelled.")
return false, nil
}
Expand Down
19 changes: 19 additions & 0 deletions hyperdrive-cli/commands/service/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func RegisterCommands(app *cli.App, name string, aliases []string) {
installVersionFlag,
installLocalScriptFlag,
installLocalPackageFlag,
installNoRestartFlag,
},
Action: func(c *cli.Context) error {
// Validate args
Expand Down Expand Up @@ -384,6 +385,24 @@ func RegisterCommands(app *cli.App, name string, aliases []string) {
return terminateService(c)
},
},

// Used by the package installers only
{
Name: "safe-start-after-install",
Aliases: []string{"ssaf"},
Usage: "Install the Hyperdrive service",
Hidden: true,
Flags: []cli.Flag{},
Action: func(c *cli.Context) error {
// Validate args
utils.ValidateArgCount(c, 1)
systemDir := c.Args().Get(0)

// Run command
safeStartAfterInstall(systemDir)
return nil
},
},
},
})
}
38 changes: 28 additions & 10 deletions hyperdrive-cli/commands/service/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,26 @@ var (
Aliases: []string{"lp"},
Usage: fmt.Sprintf("Path to a local installer package. If this is specified, Hyperdrive will use it instead of pulling the package down from the source repository. Requires -ls. %sMake sure you absolutely trust the script before using this flag.%s", terminal.ColorRed, terminal.ColorReset),
}
installNoRestartFlag *cli.BoolFlag = &cli.BoolFlag{
Name: "no-restart",
Aliases: []string{"nr"},
Usage: "Do not restart Hyperdrive services after installation",
}
)

// Install the Hyperdrive service
func installService(c *cli.Context) error {
fmt.Printf("Hyperdrive will be installed --Version: %s\n\n%sIf you're upgrading, your existing configuration will be backed up and preserved.\nAll of your previous settings will be migrated automatically.", c.String(installVersionFlag.Name), terminal.ColorGreen)
fmt.Println()

if !c.Bool(installNoRestartFlag.Name) {
fmt.Print("The service will restart to apply the update (including restarting your clients). If you have doppelganger detection enabled, any active validators will miss the next few attestations while it runs.")
}
fmt.Printf("%s\n", terminal.ColorReset)
fmt.Println()

// Prompt for confirmation
if !(c.Bool(utils.YesFlag.Name) || utils.Confirm(fmt.Sprintf(
"Hyperdrive will be installed --Version: %s\n\n%sIf you're upgrading, your existing configuration will be backed up and preserved.\nAll of your previous settings will be migrated automatically.%s\nAre you sure you want to continue?",
c.String(installVersionFlag.Name), terminal.ColorGreen, terminal.ColorReset,
))) {
if !(c.Bool(utils.YesFlag.Name) || utils.Confirm("Are you ready to continue?")) {
fmt.Println("Cancelled.")
return nil
}
Expand Down Expand Up @@ -89,18 +100,25 @@ func installService(c *cli.Context) error {
return fmt.Errorf("error generating daemon API keys: %w", err)
}

// Report next steps
fmt.Printf("%s\n=== Next Steps ===\n", terminal.ColorBlue)
fmt.Printf("Run 'hyperdrive service config' to review the settings changes for this update, or to continue setting up your node.%s\n", terminal.ColorReset)

// Print the docker permissions notice
if isNew {
fmt.Printf("%s\n=== Next Steps ===\n", terminal.ColorBlue)
fmt.Printf("Run 'hyperdrive service config' to review the settings changes for this update, or to continue setting up your node.%s\n", terminal.ColorReset)

// Print the docker permissions notice on first install
fmt.Printf("\n%sNOTE:\nSince this is your first time installing Hyperdrive, please start a new shell session by logging out and back in or restarting the machine.\n", terminal.ColorYellow)
fmt.Printf("This is necessary for your user account to have permissions to use Docker.%s", terminal.ColorReset)
} else if !c.Bool(installNoRestartFlag.Name) {
// Restart services
fmt.Println("Restarting Hyperdrive services...")
err = startService(c, true)
if err != nil {
return fmt.Errorf("error restarting services: %w", err)
}
} else {
fmt.Println("Remember to run `hyperdrive service start` to update to the new services!")
}

return nil

}

// Print the latest patch notes for this release
Expand Down
120 changes: 120 additions & 0 deletions hyperdrive-cli/commands/service/safe-start-after-install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package service

import (
"context"
"fmt"
"os"
"regexp"
"strings"
"syscall"

dtc "github.com/docker/docker/api/types/container"
dmount "github.com/docker/docker/api/types/mount"
docker "github.com/docker/docker/client"
"github.com/nodeset-org/hyperdrive/hyperdrive-cli/client"
)

const (
daemonBinLocation string = "/usr/bin/hyperdrive-daemon"
daemonImageRegexStr string = "nodeset/hyperdrive:v.*"
daemonUserDirFlag string = "--user-dir"
)

var (
daemonImageRegex *regexp.Regexp = regexp.MustCompile(daemonImageRegexStr)
)

// Called by package managers to restart the service after installation if it was already running
// Note this is going to run as a superuser, not necessarily the user running Hyperdrive, so we can't rely on the default context flags
func safeStartAfterInstall(systemDir string) {
hyperdriveBinPath := os.Args[0]

// Get a Docker client
d, err := docker.NewClientWithOpts(docker.WithAPIVersionNegotiation())
if err != nil {
fmt.Printf("Error creating Docker client: %s\n", err.Error())
return
}

// Get a list of all running images
cl, err := d.ContainerList(context.Background(), dtc.ListOptions{All: false})
if err != nil {
fmt.Printf("Error getting running Docker container list: %s\n", err.Error())
return
}

// Find the daemon containers
daemonIds := []string{}
for _, container := range cl {
isDaemon := daemonImageRegex.MatchString(container.Image)
if !isDaemon {
continue
}
name := strings.TrimPrefix(container.Names[0], "/")
fmt.Printf("Found running daemon container [%s] with image [%s]\n", name, container.Image)
daemonIds = append(daemonIds, container.ID)
}

// Run a service start for each daemon container
for _, id := range daemonIds {
containerInfo, err := d.ContainerInspect(context.Background(), id)
if err != nil {
fmt.Printf("Error inspecting container %s: %s\n", id, err.Error())
continue
}
containerName := strings.TrimPrefix(containerInfo.Name, "/")

// Make sure it has the system dir mounted
usesSystemDir := false
for _, mount := range containerInfo.Mounts {
if mount.Type != dmount.TypeBind {
continue
}
if !strings.HasPrefix(mount.Source, systemDir) {
continue
}
usesSystemDir = true
break
}
if !usesSystemDir {
fmt.Printf("Daemon container [%s] does not have the system dir mounted, skipping...\n", containerName)
continue
}

// Get the user dir from the args
userDir := ""
for i := 0; i < len(containerInfo.Args); i++ {
arg := containerInfo.Args[i]
if arg != daemonUserDirFlag {
continue
}
if i+1 >= len(containerInfo.Args) {
break
}
userDir = containerInfo.Args[i+1]
break
}
if userDir == "" {
// Couldn't get the user dir from the args, so skip this container
fmt.Printf("Daemon container [%s] does not have a user dir arg, skipping...\n", containerName)
continue
}

// Get the owner of the user dir
userDirStat := syscall.Stat_t{}
err = syscall.Stat(userDir, &userDirStat)
if err != nil {
fmt.Printf("Error getting user dir [%s] stat for [%s]: %s\n", userDir, containerName, err.Error())
continue
}
owner := userDirStat.Uid

// Start the service
success := client.StartServiceAsUser(owner, hyperdriveBinPath, userDir)
if !success {
fmt.Printf("WARN: starting service for container [%s] failed.\n", containerName)
fmt.Println("Please restart the service manually with `hyperdrive service start` to update the daemon services.")
continue
}
}
}
Loading
Loading