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

feat(instance): add console command #897

Merged
merged 11 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Connect to the serial console of an instance

USAGE:
scw instance server console <server-id> [arg=value ...]

ARGS:
server-id Server ID to connect to
[zone] Zone to target. If none is passed will use default zone from the config

FLAGS:
-h, --help help for console

GLOBAL FLAGS:
-D, --debug Enable debug mode
-o, --output string Output format: json or human
-p, --profile string The config profile to use
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ require (
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/containerd/console v1.0.0
github.com/dnaeon/go-vcr v1.0.1
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.9.0
github.com/getsentry/raven-go v0.2.0
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/go-version v1.2.0
github.com/kr/pretty v0.1.0 // indirect
Expand Down
11 changes: 7 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=
github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
Expand All @@ -30,6 +32,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
Expand All @@ -53,15 +57,12 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200422170208-cd3e8b9e038c h1:/aJXisEbIVbReS1ofNktWS8s1iT1TPP8eTu7b7Qvmxs=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200422170208-cd3e8b9e038c/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200428110639-7ec36ccf1cfc h1:zJICGw5p88KTYAzdDFjyZRBENt3kIuDsM+e89gj+5q4=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200428110639-7ec36ccf1cfc/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200430120055-da0e4c1879bc h1:xwb3X3F2jWqN/vPvFOo/QDK4IcIEhmcs83CBV5bihtk=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200430120055-da0e4c1879bc/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
Expand Down Expand Up @@ -92,6 +93,8 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
Expand Down
208 changes: 208 additions & 0 deletions internal/gotty/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package gotty

import (
"encoding/base64"
"fmt"
"net/url"
"os"
"os/signal"
"syscall"
"time"

"github.com/containerd/console"
"github.com/gorilla/websocket"
"github.com/scaleway/scaleway-sdk-go/scw"
)

// Scaleway GoTTY websocket endpoint
var wsURLs = map[scw.Zone]string{
Sh4d1 marked this conversation as resolved.
Show resolved Hide resolved
scw.ZoneFrPar1: "wss://tty-par1.scaleway.com/v2/ws",
scw.ZoneFrPar2: "wss://tty-par2.scaleway.com/v2/ws",
scw.ZoneNlAms1: "wss://tty-ams1.scaleway.com/v2/ws",
}

const (
// GoTTY ingress message code
outputCode = '0'
setWindowTitleCode = '2'

// GoTTY egress message code
inputCode = '0'
pingCode = '1'
resizeTerminalCode = '2'
)

type Client struct {
wsURL string
serverId string
secretKey string
}

// NewClient returns a GoTTY client.
func NewClient(zone scw.Zone, serverID string, secretKey string) (*Client, error) {

wsURL, zoneExist := wsURLs[zone]
if !zoneExist {
return nil, fmt.Errorf("gotty is not availabe in zone %s", zone)
}

return &Client{
wsURL: wsURL,
serverId: serverID,
secretKey: secretKey,
}, nil
}

func (c *Client) Connect() error {

wsDialer := websocket.Dialer{}
conn, _, err := wsDialer.Dial(c.wsURL, nil)
if err != nil {
return err
}
defer func() {
// Websocket protocol require the server to close the connection.
// This sent a close request.
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}()

// This is how scaleway implement gotty authentication
err = conn.WriteJSON(map[string]string{
"AuthToken": "",
"Arguments": "?" + url.Values{"arg": []string{c.secretKey, c.serverId}}.Encode(),
})
if err != nil {
return err
}

cns, err := console.ConsoleFromFile(os.Stdin)
if err != nil {
return fmt.Errorf("os.Stdin doesn't seems to be a valid terminal: %w", err)
}
err = cns.SetRaw()
if err != nil {
return fmt.Errorf("error setting raw terminal: %w", err)
}
defer cns.Reset()
defer cns.Close()

// Create a chanel that will receive all resizes signals
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
Sh4d1 marked this conversation as resolved.
Show resolved Hide resolved
defer signal.Stop(resizeChan)

wsChan, wsErrChan := websocketReader(conn)
cnsChan, cnsErrChan := consoleReader(cns)

// Force first resize
resizeChan <- syscall.SIGWINCH
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, need to be in another file


for {
select {

// Resize event: we send new terminal size to the server
case <-resizeChan:
size, err := cns.Size()
if err != nil {
return err
}
message := fmt.Sprintf(`%c{"columns":%d,"rows":%d}`, resizeTerminalCode, size.Width, size.Height)
err = conn.WriteMessage(websocket.TextMessage, []byte(message))

// We receive a message from the server
case message := <-wsChan:
// If message is empty, connection was closed
if len(message) == 0 {
return nil
}

switch message[0] {
// The message contain data that should be printed
case outputCode:
buf, err := base64.StdEncoding.DecodeString(string(message[1:]))
if err != nil {
return err
}
os.Stdout.Write(buf)
// The message contain a new terminal title
case setWindowTitleCode:
newTitle := string(message[1:])
fmt.Fprintf(os.Stdout, "\033]0;%s\007", newTitle)
// We ignore other type of events
default:
}

// We read something on the console (probably user input): we send it to the server.
case message := <-cnsChan:
// If message is empty the console has been closed
if len(message) == 0 {
return nil
}
err = conn.WriteMessage(websocket.TextMessage, append([]byte{inputCode}, message...))
if err != nil {
return err
}

// We make sure to send a ping every 30s to keep the connection alive.
case <-time.After(30 * time.Second):
err = conn.WriteMessage(websocket.TextMessage, []byte{pingCode})
if err != nil {
return err
}

// If we receive an error from one of the 2 reader we return it
case err := <-wsErrChan:
return err
case err := <-cnsErrChan:
return err
}
}
}

// websocketReader start a go routine to read incoming messages on the websocket.
// It return 2 channels one with read message and one in case of errors.
func websocketReader(conn *websocket.Conn) (chan []byte, chan error) {
readChan := make(chan []byte)
errChan := make(chan error)
go func() {
defer close(readChan)
defer close(errChan)
for {
_, data, err := conn.ReadMessage()
if err != nil {
// if a error is a close error exit properly
if _, isClose := err.(*websocket.CloseError); !isClose {
errChan <- err
}
return
}
readChan <- data
}
}()

return readChan, errChan
}

// consoleReader start a go routine to read user input on the console.
// It return 2 channels one with read input and one in case of errors.
func consoleReader(cns console.Console) (chan []byte, chan error) {
readChan := make(chan []byte)
errChan := make(chan error)

go func() {
defer close(readChan)
defer close(errChan)

for {
buff := make([]byte, 128)
size, err := cns.Read(buff)
if err != nil {
errChan <- err
return
}
readChan <- buff[:size]
}
}()

return readChan, errChan
}
11 changes: 6 additions & 5 deletions internal/namespaces/instance/v1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,18 @@ func GetCommands() *core.Commands {
cmds.MustFind("instance", "server", "update").Override(serverUpdateBuilder)

cmds.Merge(core.NewCommands(
serverAttachVolumeCommand(),
serverBackupCommand(),
serverConsoleCommand(),
serverCreateCommand(),
serverDeleteCommand(),
serverDetachVolumeCommand(),
serverSSHCommand(),
serverStartCommand(),
serverStopCommand(),
serverStandbyCommand(),
serverRebootCommand(),
serverBackupCommand(),
serverWaitCommand(),
serverDeleteCommand(),
serverAttachVolumeCommand(),
serverDetachVolumeCommand(),
serverSSHCommand(),
))

//
Expand Down
77 changes: 77 additions & 0 deletions internal/namespaces/instance/v1/custom_server_console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package instance

import (
"context"
"fmt"
"reflect"

"github.com/fatih/color"
"github.com/scaleway/scaleway-cli/internal/core"
"github.com/scaleway/scaleway-cli/internal/gotty"
"github.com/scaleway/scaleway-cli/internal/interactive"
"github.com/scaleway/scaleway-cli/internal/terminal"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type instanceConsoleServerArgs struct {
Zone scw.Zone
ServerID string
}

func serverConsoleCommand() *core.Command {
return &core.Command{
Short: `Connect to the serial console of an instance`,
Namespace: "instance",
Verb: "console",
Resource: "server",
ArgsType: reflect.TypeOf(instanceConsoleServerArgs{}),
ArgSpecs: core.ArgSpecs{
{
Name: "server-id",
Short: "Server ID to connect to",
Required: true,
Positional: true,
},
core.ZoneArgSpec(),
},
Run: instanceServerConsoleRun,
}
}

func instanceServerConsoleRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
args := argsI.(*instanceConsoleServerArgs)

client := core.ExtractClient(ctx)
apiInstance := instance.NewAPI(client)
serverResp, err := apiInstance.GetServer(&instance.GetServerRequest{
Zone: args.Zone,
ServerID: args.ServerID,
})
if err != nil {
return nil, err
}
server := serverResp.Server

secretKey, ok := client.GetSecretKey()
if !ok {
return nil, fmt.Errorf("could not get secret key")
}

ttyClient, err := gotty.NewClient(server.Zone, server.ID, secretKey)
if err != nil {
return nil, err
}

// Add hint on how to quit properly
fmt.Printf(terminal.Style("Open connection to %s (%s)\n", color.Bold), server.Name, server.ID)
fmt.Println(" - You may need to hit enter to start")
fmt.Println(" - Type Ctrl+q to quit.")
fmt.Println(interactive.Line("-"))

if err = ttyClient.Connect(); err != nil {
return nil, err
}

return nil, err
}