Skip to content
Draft
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
39 changes: 1 addition & 38 deletions cmd/admin/v2/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
40 changes: 39 additions & 1 deletion cmd/api/v2/machine.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
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"
"github.com/metal-stack/cli/cmd/sorters"
"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"
)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
43 changes: 43 additions & 0 deletions pkg/helpers/ssh.go
Original file line number Diff line number Diff line change
@@ -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)
}