//go:build installer package main import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "text/template" "github.com/shellhub-io/shellhub/pkg/uuid" "github.com/spf13/cobra" ) const ( agentEnvFile = "/etc/shellhub-agent.env" agentServiceFile = "/etc/systemd/system/shellhub-agent.service" agentServiceName = "shellhub-agent" ) var agentServiceTemplate = `[Unit] Description=ShellHub Agent After=local-fs.target network-online.target time-sync.target Wants=network-online.target Requires=local-fs.target [Service] EnvironmentFile=/etc/shellhub-agent.env ExecStart={{.BinaryPath}} Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target ` type installerConfig struct { ServerAddress string TenantID string PrivateKey string ProvisioningKey string PreferredHostname string PreferredIdentity string KeepaliveInterval uint } func registerInstallerCommands(rootCmd *cobra.Command) { installCmd := &cobra.Command{ //nolint:exhaustruct Use: "install", Short: "Install ShellHub agent as a systemd service", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { flags := cmd.Flags() serverAddress, _ := flags.GetString("server-address") tenantID, _ := flags.GetString("tenant-id") privateKey, _ := flags.GetString("private-key") preferredHostname, _ := flags.GetString("preferred-hostname") preferredIdentity, _ := flags.GetString("preferred-identity") provisioningKey, _ := flags.GetString("provisioning-key") if provisioningKey != "" { if _, err := uuid.Parse(provisioningKey); err != nil { return fmt.Errorf("invalid provisioning key: %q is not a valid UUID", provisioningKey) } } var keepaliveInterval uint if flags.Changed("keepalive-interval") { keepaliveInterval, _ = flags.GetUint("keepalive-interval") } if err := agentInstall(installerConfig{ ServerAddress: serverAddress, TenantID: tenantID, PrivateKey: privateKey, ProvisioningKey: provisioningKey, PreferredHostname: preferredHostname, PreferredIdentity: preferredIdentity, KeepaliveInterval: keepaliveInterval, }); err != nil { return fmt.Errorf("installation failed: %w", err) } fmt.Println("ShellHub agent installed successfully.") fmt.Println("Check status: systemctl status shellhub-agent") fmt.Println("View logs: journalctl -u shellhub-agent -f") return nil }, } installCmd.Flags().String("server-address", "", "ShellHub server address") installCmd.Flags().String("tenant-id", "", "Namespace tenant ID") installCmd.Flags().String("private-key", "/etc/shellhub.key", "Path to the agent private key file") installCmd.Flags().String("provisioning-key", "", "Provisioning key used to enroll the device") installCmd.Flags().String("preferred-hostname", "", "Preferred device hostname") installCmd.Flags().String("preferred-identity", "", "Preferred device identity") installCmd.Flags().Uint("keepalive-interval", 30, "Keepalive interval in seconds") installCmd.MarkFlagRequired("server-address") //nolint:errcheck installCmd.MarkFlagsOneRequired("tenant-id", "provisioning-key") rootCmd.AddCommand(installCmd) rootCmd.AddCommand(&cobra.Command{ //nolint:exhaustruct Use: "uninstall", Short: "Uninstall ShellHub agent systemd service", RunE: func(_ *cobra.Command, _ []string) error { if err := agentUninstall(); err != nil { return fmt.Errorf("uninstallation failed: %w", err) } fmt.Println("ShellHub agent uninstalled successfully.") fmt.Println("Note: the binary and private key were not removed.") return nil }, }) } func agentInstall(cfg installerConfig) error { if os.Geteuid() != 0 { return fmt.Errorf("must be run as root") } if err := exec.Command("systemctl", "show-environment").Run(); err != nil { return fmt.Errorf("systemd is not available on this system") } exec.Command("systemctl", "disable", "--now", agentServiceName).Run() //nolint:errcheck exe, err := os.Executable() if err != nil { return fmt.Errorf("failed to resolve executable path: %w", err) } binaryPath, err := filepath.EvalSymlinks(exe) if err != nil { return fmt.Errorf("failed to resolve symlinks: %w", err) } if err := writeAgentEnvFile(cfg); err != nil { return fmt.Errorf("failed to write env file: %w", err) } if err := writeAgentServiceFile(binaryPath); err != nil { return fmt.Errorf("failed to write service file: %w", err) } if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { return fmt.Errorf("failed to reload systemd daemon: %w", err) } if err := exec.Command("systemctl", "enable", "--now", agentServiceName).Run(); err != nil { return fmt.Errorf("failed to enable service: %w", err) } return nil } func writeAgentEnvFile(cfg installerConfig) error { var buf bytes.Buffer fmt.Fprintf(&buf, "SHELLHUB_SERVER_ADDRESS=%s\n", cfg.ServerAddress) fmt.Fprintf(&buf, "SHELLHUB_PRIVATE_KEY=%s\n", cfg.PrivateKey) if cfg.TenantID != "" { fmt.Fprintf(&buf, "SHELLHUB_TENANT_ID=%s\n", cfg.TenantID) } if cfg.ProvisioningKey != "" { fmt.Fprintf(&buf, "SHELLHUB_PROVISIONING_KEY=%s\n", cfg.ProvisioningKey) } if cfg.PreferredHostname != "" { fmt.Fprintf(&buf, "SHELLHUB_PREFERRED_HOSTNAME=%s\n", cfg.PreferredHostname) } if cfg.PreferredIdentity != "" { fmt.Fprintf(&buf, "SHELLHUB_PREFERRED_IDENTITY=%s\n", cfg.PreferredIdentity) } if cfg.KeepaliveInterval != 0 { fmt.Fprintf(&buf, "SHELLHUB_KEEPALIVE_INTERVAL=%d\n", cfg.KeepaliveInterval) } return os.WriteFile(agentEnvFile, buf.Bytes(), 0o600) } func writeAgentServiceFile(binaryPath string) error { tmpl, err := template.New("service").Parse(agentServiceTemplate) if err != nil { return err } var buf bytes.Buffer if err := tmpl.Execute(&buf, struct{ BinaryPath string }{binaryPath}); err != nil { return err } return os.WriteFile(agentServiceFile, buf.Bytes(), 0o644) } func agentUninstall() error { if os.Geteuid() != 0 { return fmt.Errorf("must be run as root") } exec.Command("systemctl", "disable", "--now", agentServiceName).Run() //nolint:errcheck if err := os.Remove(agentServiceFile); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove service file: %w", err) } if err := os.Remove(agentEnvFile); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove env file: %w", err) } if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { return fmt.Errorf("failed to reload systemd daemon: %w", err) } return nil }