diff --git a/cmd/admin/v2/machine.go b/cmd/admin/v2/machine.go index c8cb0a3..9b742e3 100644 --- a/cmd/admin/v2/machine.go +++ b/cmd/admin/v2/machine.go @@ -452,7 +452,7 @@ func (c *machine) console(ctx context.Context, args []string) error { return err } - err = sshClient(id, viper.GetString("sshidentity"), parsedurl.Host, viper.GetInt("metal-console-port"), &c.c.Context.Token, true) + err = helpers.SShClient(id, viper.GetString("sshidentity"), parsedurl.Host, viper.GetInt("metal-console-port"), c.c.Context.Token, c.c.GetProject()) if err != nil { return fmt.Errorf("machine console error:%w", err) } @@ -592,40 +592,3 @@ func (c *machine) firewallSSH(ctx context.Context, args []string) (err error) { } return s.Connect(nil) } - -// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key. -func sshClient(user, keyfile, host string, port int, idToken *string, passwordAuth bool) error { - var opts []metalssh.ConnectOpt - - if passwordAuth { - opts = append(opts, metalssh.ConnectOptOutputPassword(*idToken)) - } else { - if keyfile == "" { - var err error - keyfile, err = helpers.SearchSSHKey() - if err != nil { - return err - } - } - - privateKey, err := os.ReadFile(keyfile) - if err != nil { - return err - } - - opts = append(opts, metalssh.ConnectOptOutputPrivateKey(privateKey)) - } - - s, err := metalssh.NewClient(user, host, port, opts...) - if err != nil { - return err - } - - var env *metalssh.Env - - if idToken != nil { - env = &metalssh.Env{"LC_METAL_STACK_OIDC_TOKEN": *idToken} - } - - return s.Connect(env) -} diff --git a/cmd/api/v2/machine.go b/cmd/api/v2/machine.go index cc7845e..3a148da 100644 --- a/cmd/api/v2/machine.go +++ b/cmd/api/v2/machine.go @@ -1,6 +1,10 @@ package v2 import ( + "context" + "fmt" + "net/url" + "github.com/metal-stack/api/go/errorutil" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/cmd/config" @@ -8,6 +12,7 @@ import ( "github.com/metal-stack/cli/pkg/helpers" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -72,7 +77,21 @@ If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picke ValidArgsFn: c.Completion.Machine, } - return genericcli.NewCmds(cmdsConfig) + consoleCmd := &cobra.Command{ + Use: "console", + Short: "establishes a connection to the serial console of a machine. for authentication at the metal-console it uses the token such that no machine ssh key is required for access (unlike the corresponding user API command).", + RunE: func(cmd *cobra.Command, args []string) error { + return w.console(cmd.Context(), args) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + consoleCmd.Flags().Bool("ipmi", false, "if set to true, the serial console will be opened using ipmitool (requires ipmitool to be present)") + consoleCmd.Flags().Int("metal-console-port", 5222, "port open on our control-plane to connect via ssh to get machine console access") + consoleCmd.Flags().StringP("project", "p", "", "project of the machine") + genericcli.Must(consoleCmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + genericcli.Must(consoleCmd.MarkFlagRequired("project")) + + return genericcli.NewCmds(cmdsConfig, consoleCmd) } func (c *machine) Create(rq *apiv2.MachineServiceCreateRequest) (*apiv2.Machine, error) { @@ -176,3 +195,22 @@ func (c *machine) Convert(r *apiv2.Machine) (string, *apiv2.MachineServiceCreate return helpers.EncodeProject(r.Uuid, r.Allocation.Project), create, update, err } + +func (c *machine) console(ctx context.Context, args []string) error { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + parsedurl, err := url.Parse(pointer.SafeDeref(c.c.Context.ApiURL)) + if err != nil { + return err + } + + err = helpers.SShClient(id, viper.GetString("sshidentity"), parsedurl.Host, viper.GetInt("metal-console-port"), c.c.Context.Token, viper.GetString("project")) + if err != nil { + return fmt.Errorf("machine console error:%w", err) + } + + return nil +} diff --git a/pkg/helpers/ssh.go b/pkg/helpers/ssh.go new file mode 100644 index 0000000..36fd964 --- /dev/null +++ b/pkg/helpers/ssh.go @@ -0,0 +1,43 @@ +package helpers + +import ( + "os" + + metalssh "github.com/metal-stack/metal-lib/pkg/ssh" +) + +// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key. +func SShClient(user, keyfile, host string, port int, idToken, project string) error { + var opts []metalssh.ConnectOpt + + opts = append(opts, metalssh.ConnectOptOutputPassword(idToken)) + + if keyfile == "" { + var err error + keyfile, err = SearchSSHKey() + if err != nil { + return err + } + } + + privateKey, err := os.ReadFile(keyfile) + if err != nil { + return err + } + + opts = append(opts, metalssh.ConnectOptOutputPrivateKey(privateKey)) + + s, err := metalssh.NewClient(user, host, port, opts...) + if err != nil { + return err + } + + env := map[string]string{ + "LC_METAL_STACK_OIDC_TOKEN": idToken, + "LC_METAL_STACK_PROJECT": project, + } + + sshEnv := metalssh.Env(env) + + return s.Connect(&sshEnv) +}