The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/7642
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) ===
From e84b6de416ddcb685fd30d556ecfd2432e70e9b5 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 6 Jul 2020 11:34:30 +0200 Subject: [PATCH 01/11] shared/version: Add console_vga_type API extension Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- shared/version/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/version/api.go b/shared/version/api.go index a4b1431b59..8fb696c459 100644 --- a/shared/version/api.go +++ b/shared/version/api.go @@ -218,6 +218,7 @@ var APIExtensions = []string{ "custom_block_volumes", "clustering_failure_domains", "resources_gpu_mdev", + "console_vga_type", } // APIExtensionsCount returns the number of available API extensions. From aa29ff9041c6c359a9d379abda4ef06912742954 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 6 Jul 2020 11:42:27 +0200 Subject: [PATCH 02/11] doc: Document console_vga_type extension Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- doc/api-extensions.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index 97945491d1..1a2b81ed67 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -1109,3 +1109,12 @@ A number of new syscalls related container configuration keys were updated. ## resources\_gpu\_mdev Expose available mediated device profiles and devices in /1.0/resources. + +## console\_vga\_type + +This extends the `/1.0/console` endpoint to take a `?type=` argument, which can +be set to `console` (default) or `vga` (the new type added by this extension). + +When POST'ing to `/1.0/<instance name>/console?type=vga` the data websocket +returned by the operation in the metadata field will be a bidirectional proxy +attached to a SPICE unix socket of the target virtual machine. From 66ebc8647db4b7fd0066b5dec35fe675b18737b5 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 6 Jul 2020 12:11:54 +0200 Subject: [PATCH 03/11] shared/api: Add Type field to InstanceConsolePost Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- shared/api/instance_console.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared/api/instance_console.go b/shared/api/instance_console.go index 614beb1245..a7a903c355 100644 --- a/shared/api/instance_console.go +++ b/shared/api/instance_console.go @@ -14,4 +14,7 @@ type InstanceConsoleControl struct { type InstanceConsolePost struct { Width int `json:"width" yaml:"width"` Height int `json:"height" yaml:"height"` + + // API extension: console_vga_type + Type string `json:"type" yaml:"type"` } From 85c079b79a6f007780af1f7323158f2d9cfc1b22 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 6 Jul 2020 12:16:11 +0200 Subject: [PATCH 04/11] lxd: Set "console" as default POST /1.0/<instance>/console type parameter Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/instance/instance_interface.go | 6 ++++++ lxd/instance_console.go | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/lxd/instance/instance_interface.go b/lxd/instance/instance_interface.go index 0652e87227..405523bd48 100644 --- a/lxd/instance/instance_interface.go +++ b/lxd/instance/instance_interface.go @@ -25,6 +25,12 @@ const HookStopNS = "onstopns" // HookStop hook used when instance has stopped. const HookStop = "onstop" +// Possible values for the protocol argument of the Instance.Console() method. +const ( + ConsoleTypeConsole = "console" + ConsoleTypeVGA = "vga" +) + // ConfigReader is used to read instance config. type ConfigReader interface { Type() instancetype.Type diff --git a/lxd/instance_console.go b/lxd/instance_console.go index 54600fe33f..c66d3d655c 100644 --- a/lxd/instance_console.go +++ b/lxd/instance_console.go @@ -277,6 +277,15 @@ func containerConsolePost(d *Daemon, r *http.Request) response.Response { return operations.ForwardedOperationResponse(project, &opAPI) } + if post.Type == "" { + post.Type = instance.ConsoleTypeConsole + } + + // Basic parameter validation. + if !shared.StringInSlice(post.Type, []string{instance.ConsoleTypeConsole, instance.ConsoleTypeVGA}) { + return response.BadRequest(fmt.Errorf("Unknown console type %q", post.Type)) + } + inst, err := instance.LoadByProjectAndName(d.State(), project, name) if err != nil { return response.SmartError(err) From 1c5377987bd1d34b14356eeaa874b20198ea75dc Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Tue, 7 Jul 2020 08:27:19 +0200 Subject: [PATCH 05/11] lxd/instance: Add protocol argument to Instance.Console() Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/instance/drivers/driver_lxc.go | 6 +++++- lxd/instance/drivers/driver_qemu.go | 2 +- lxd/instance/instance_interface.go | 4 ++-- lxd/instance_console.go | 6 +++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lxd/instance/drivers/driver_lxc.go b/lxd/instance/drivers/driver_lxc.go index 6481b1a066..256cc7573a 100644 --- a/lxd/instance/drivers/driver_lxc.go +++ b/lxd/instance/drivers/driver_lxc.go @@ -5534,7 +5534,11 @@ func (c *lxc) FileRemove(path string) error { } // Console attaches to the instance console. -func (c *lxc) Console() (*os.File, chan error, error) { +func (c *lxc) Console(protocol string) (*os.File, chan error, error) { + if protocol != instance.ConsoleTypeConsole { + return nil, nil, fmt.Errorf("Container instances don't support %q output", protocol) + } + chDisconnect := make(chan error, 1) args := []string{ diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index b0194652f8..81d11f96b6 100644 --- a/lxd/instance/drivers/driver_qemu.go +++ b/lxd/instance/drivers/driver_qemu.go @@ -3753,7 +3753,7 @@ func (vm *qemu) FileRemove(path string) error { } // Console gets access to the instance's console. -func (vm *qemu) Console() (*os.File, chan error, error) { +func (vm *qemu) Console(protocol string) (*os.File, chan error, error) { chDisconnect := make(chan error, 1) // Avoid duplicate connects. diff --git a/lxd/instance/instance_interface.go b/lxd/instance/instance_interface.go index 405523bd48..efad8bea9b 100644 --- a/lxd/instance/instance_interface.go +++ b/lxd/instance/instance_interface.go @@ -78,8 +78,8 @@ type Instance interface { FilePush(fileType string, srcpath string, dstpath string, uid int64, gid int64, mode int, write string) error FileRemove(path string) error - // Console - Allocate and run a console tty. - Console() (*os.File, chan error, error) + // Console - Allocate and run a console tty or a spice Unix socket. + Console(protocol string) (*os.File, chan error, error) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, stderr *os.File) (Cmd, error) // Status diff --git a/lxd/instance_console.go b/lxd/instance_console.go index c66d3d655c..3514425fe1 100644 --- a/lxd/instance_console.go +++ b/lxd/instance_console.go @@ -51,6 +51,9 @@ type consoleWs struct { // terminal height height int + + // channel type (either console or vga) + protocol string } func (s *consoleWs) Metadata() interface{} { @@ -112,7 +115,7 @@ func (s *consoleWs) Do(op *operations.Operation) error { <-s.allConnected // Get console from instance. - console, consoleDisconnectCh, err := s.instance.Console() + console, consoleDisconnectCh, err := s.instance.Console(s.protocol) if err != nil { return err } @@ -318,6 +321,7 @@ func containerConsolePost(d *Daemon, r *http.Request) response.Response { ws.instance = inst ws.width = post.Width ws.height = post.Height + ws.protocol = post.Type resources := map[string][]string{} resources["instances"] = []string{ws.instance.Name()} From 604ff42ae50c412fe4016e01020b81d7dd7b6d98 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Tue, 7 Jul 2020 09:36:00 +0200 Subject: [PATCH 06/11] lxd/instance/drivers: Support VGA output in qemu.Console() Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/instance/drivers/driver_qemu.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index 81d11f96b6..31c4bec940 100644 --- a/lxd/instance/drivers/driver_qemu.go +++ b/lxd/instance/drivers/driver_qemu.go @@ -3754,6 +3754,17 @@ func (vm *qemu) FileRemove(path string) error { // Console gets access to the instance's console. func (vm *qemu) Console(protocol string) (*os.File, chan error, error) { + switch protocol { + case instance.ConsoleTypeConsole: + return vm.console() + case instance.ConsoleTypeVGA: + return vm.vga() + default: + return nil, nil, fmt.Errorf("Unknown protocol %q", protocol) + } +} + +func (vm *qemu) console() (*os.File, chan error, error) { chDisconnect := make(chan error, 1) // Avoid duplicate connects. @@ -3793,6 +3804,22 @@ func (vm *qemu) Console(protocol string) (*os.File, chan error, error) { return console, chDisconnect, nil } +func (vm *qemu) vga() (*os.File, chan error, error) { + // Open the spice socket + conn, err := net.Dial("unix", vm.spicePath()) + if err != nil { + return nil, nil, errors.Wrapf(err, "Connect to SPICE socket %q", vm.spicePath()) + } + + file, err := (conn.(*net.UnixConn)).File() + if err != nil { + return nil, nil, errors.Wrap(err, "Get socket file") + } + conn.Close() + + return file, nil, nil +} + // Exec a command inside the instance. func (vm *qemu) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, stderr *os.File) (instance.Cmd, error) { revert := revert.New() From 3dc551aaa8091dda593f96a48512a94197672dc9 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Fri, 10 Jul 2020 12:43:20 +0200 Subject: [PATCH 07/11] lxd: Handle "vga" type in console API handler Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/instance_console.go | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/lxd/instance_console.go b/lxd/instance_console.go index 3514425fe1..217bf1e88a 100644 --- a/lxd/instance_console.go +++ b/lxd/instance_console.go @@ -34,6 +34,9 @@ type consoleWs struct { // websocket connections to bridge pty fds to conns map[int]*websocket.Conn + // map dynamic websocket connections to their associated console file + dynamic map[*websocket.Conn]*os.File + // locks needed to access the "conns" member connsLock sync.Mutex @@ -70,6 +73,17 @@ func (s *consoleWs) Metadata() interface{} { } func (s *consoleWs) Connect(op *operations.Operation, r *http.Request, w http.ResponseWriter) error { + switch s.protocol { + case instance.ConsoleTypeConsole: + return s.connectConsole(op, r, w) + case instance.ConsoleTypeVGA: + return s.connectVGA(op, r, w) + default: + return fmt.Errorf("Unknown protocol %q", s.protocol) + } +} + +func (s *consoleWs) connectConsole(op *operations.Operation, r *http.Request, w http.ResponseWriter) error { secret := r.FormValue("secret") if secret == "" { return fmt.Errorf("missing secret") @@ -110,7 +124,70 @@ func (s *consoleWs) Connect(op *operations.Operation, r *http.Request, w http.Re return os.ErrPermission } +func (s *consoleWs) connectVGA(op *operations.Operation, r *http.Request, w http.ResponseWriter) error { + secret := r.FormValue("secret") + if secret == "" { + return fmt.Errorf("missing secret") + } + + for fd, fdSecret := range s.fds { + if secret != fdSecret { + continue + } + + conn, err := shared.WebsocketUpgrader.Upgrade(w, r, nil) + if err != nil { + return err + } + + if fd == -1 { + logger.Debug("VGA control websocket connected") + + s.connsLock.Lock() + s.conns[fd] = conn + s.connsLock.Unlock() + + s.controlConnected <- true + return nil + } + + logger.Debug("VGA dynamic websocket connected") + + console, _, err := s.instance.Console("vga") + if err != nil { + conn.Close() + return err + } + + // Mirror the console and websocket. + go func() { + shared.WebsocketConsoleMirror(conn, console, console) + }() + + s.connsLock.Lock() + s.dynamic[conn] = console + s.connsLock.Unlock() + + return nil + } + + // If we didn't find the right secret, the user provided a bad one, + // which 403, not 404, since this operation actually exists. + return os.ErrPermission +} + func (s *consoleWs) Do(op *operations.Operation) error { + switch s.protocol { + case instance.ConsoleTypeConsole: + return s.doConsole(op) + case instance.ConsoleTypeVGA: + return s.doVGA(op) + default: + return fmt.Errorf("Unknown protocol %q", s.protocol) + } +} + +func (s *consoleWs) doConsole(op *operations.Operation) error { defer logger.Debug("Console websocket finished") <-s.allConnected @@ -242,6 +319,52 @@ func (s *consoleWs) Do(op *operations.Operation) error { return nil } +func (s *consoleWs) doVGA(op *operations.Operation) error { + defer logger.Debug("VGA websocket finished") + + consoleDoneCh := make(chan struct{}) + + // The control socket is used to terminate the operation. + go func() { + defer logger.Debugf("VGA control websocket finished") + res := <-s.controlConnected + if !res { + return + } + + for { + s.connsLock.Lock() + conn := s.conns[-1] + s.connsLock.Unlock() + + _, _, err := conn.NextReader() + if err != nil { + logger.Debugf("Got error getting next reader %s", err) + close(consoleDoneCh) + return + } + } + }() + + // Wait until the control channel is done. + <-consoleDoneCh + s.connsLock.Lock() + control := s.conns[-1] + s.connsLock.Unlock() + control.Close() + + // Close all dynamic connections. + for conn, console := range s.dynamic { + conn.Close() + console.Close() + } + + // Indicate to the control socket go routine to end if not already. + close(s.controlConnected) + + return nil +} + func containerConsolePost(d *Daemon, r *http.Request) response.Response { instanceType, err := urlInstanceTypeDetect(r) if err != nil { @@ -309,6 +432,7 @@ func containerConsolePost(d *Daemon, r *http.Request) response.Response { ws.conns = map[int]*websocket.Conn{} ws.conns[-1] = nil ws.conns[0] = nil + ws.dynamic = map[*websocket.Conn]*os.File{} for i := -1; i < len(ws.conns)-1; i++ { ws.fds[i], err = shared.RandomCryptoString() if err != nil { From 3070e74a9af2de59f8aa59a3a5eba74ef39eb7f9 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Tue, 7 Jul 2020 08:40:04 +0200 Subject: [PATCH 08/11] client: Check that server has console_vga_type extension If InstanceConsolePost.Type is set to "vga" the server must be able to handle it. Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- client/lxd_instances.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/lxd_instances.go b/client/lxd_instances.go index 87e87e22b0..43d29905bf 100644 --- a/client/lxd_instances.go +++ b/client/lxd_instances.go @@ -1788,6 +1788,14 @@ func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceC return nil, fmt.Errorf("The server is missing the required \"console\" API extension") } + if console.Type == "" { + console.Type = "console" + } + + if console.Type == "vga" && !r.HasExtension("console_vga_type") { + return nil, fmt.Errorf("The server is missing the required \"console_vga_type\" API extension") + } + // Send the request op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "") if err != nil { From e87e1e5e1ca127217be32ceba0752785293574ef Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Fri, 10 Jul 2020 10:20:48 +0200 Subject: [PATCH 09/11] client: Add ConsoleInstanceDynamic() to support multiple websocket connections Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- client/interfaces.go | 2 + client/lxd_instances.go | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/client/interfaces.go b/client/interfaces.go index 583e97f9b8..e0dfdd59cf 100644 --- a/client/interfaces.go +++ b/client/interfaces.go @@ -156,6 +156,8 @@ type InstanceServer interface { ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (op Operation, err error) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (op Operation, err error) + ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error) + GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (content io.ReadCloser, err error) DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (err error) diff --git a/client/lxd_instances.go b/client/lxd_instances.go index 43d29905bf..9b94f545de 100644 --- a/client/lxd_instances.go +++ b/client/lxd_instances.go @@ -1860,6 +1860,96 @@ func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceC return op, nil } +// ConsoleInstanceDynamic requests that LXD attaches to the console device of a +// instance with the possibility of opening multiple connections to it. +// +// Every time the returned 'console' function is called, a new connection will +// be established and proxied to the given io.ReadWriteCloser. +func (r *ProtocolLXD) ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error) { + path, _, err := r.instanceTypeToPath(api.InstanceTypeAny) + if err != nil { + return nil, nil, err + } + + if !r.HasExtension("console") { + return nil, nil, fmt.Errorf("The server is missing the required \"console\" API extension") + } + + if console.Type == "" { + console.Type = "console" + } + + if console.Type == "vga" && !r.HasExtension("console_vga_type") { + return nil, nil, fmt.Errorf("The server is missing the required \"console_vga_type\" API extension") + } + + // Send the request + op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "") + if err != nil { + return nil, nil, err + } + opAPI := op.Get() + + if args == nil { + return nil, nil, fmt.Errorf("No arguments provided") + } + + if args.Control == nil { + return nil, nil, fmt.Errorf("A control channel must be set") + } + + // Parse the fds + fds := map[string]string{} + + value, ok := opAPI.Metadata["fds"] + if ok { + values := value.(map[string]interface{}) + for k, v := range values { + fds[k] = v.(string) + } + } + + // Call the control handler with a connection to the control socket + if fds["control"] == "" { + return nil, nil, fmt.Errorf("Did not receive a file descriptor for the control channel") + } + + controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"]) + if err != nil { + return nil, nil, err + } + + go args.Control(controlConn) + + f := func(rwc io.ReadWriteCloser) error { + // Connect to the websocket + conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"]) + if err != nil { + return err + } + + // Detach. + go func(consoleDisconnect <-chan bool) { + <-consoleDisconnect + msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console") + // We don't care if this fails. This is just for convenience. + controlConn.WriteMessage(websocket.CloseMessage, msg) + controlConn.Close() + }(args.ConsoleDisconnect) + + // Attach reader/writer + go func() { + shared.WebsocketSendStream(conn, rwc, -1) + <-shared.WebsocketRecvStream(rwc, conn) + conn.Close() + }() + + return nil + } + + return op, f, nil +} + // GetInstanceConsoleLog requests that LXD attaches to the console device of a instance. // // Note that it's the caller's responsibility to close the returned ReadCloser From 7303d22ca0ce4a02d649442749ebf1563566c939 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Tue, 7 Jul 2020 08:35:42 +0200 Subject: [PATCH 10/11] lxc: Add --type flag to "lxc console" Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxc/console.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lxc/console.go b/lxc/console.go index 02b8e1dbf2..a1308d0060 100644 --- a/lxc/console.go +++ b/lxc/console.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/lxc/lxd/client" + "github.com/lxc/lxd/shared" "github.com/lxc/lxd/shared/api" cli "github.com/lxc/lxd/shared/cmd" "github.com/lxc/lxd/shared/i18n" @@ -22,6 +23,7 @@ type cmdConsole struct { global *cmdGlobal flagShowLog bool + flagType string } func (c *cmdConsole) Command() *cobra.Command { @@ -36,6 +38,7 @@ as well as retrieve past log entries from it.`)) cmd.RunE = c.Run cmd.Flags().BoolVar(&c.flagShowLog, "show-log", false, i18n.G("Retrieve the instance's console log")) + cmd.Flags().StringVar(&c.flagType, "type", "console", i18n.G("Type of connection to establish: 'console' for serial console, 'vga' for SPICE graphical output")) return cmd } @@ -101,6 +104,11 @@ func (c *cmdConsole) Run(cmd *cobra.Command, args []string) error { return err } + // Validate flags. + if !shared.StringInSlice(c.flagType, []string{"console", "vga"}) { + return fmt.Errorf("Unknown output type %q", c.flagType) + } + // Connect to LXD remote, name, err := conf.ParseRemote(args[0]) if err != nil { @@ -114,6 +122,10 @@ func (c *cmdConsole) Run(cmd *cobra.Command, args []string) error { // Show the current log if requested if c.flagShowLog { + if c.flagType != "console" { + return fmt.Errorf("The --show-log flag is only supported for by 'console' output type") + } + console := &lxd.InstanceConsoleLogArgs{} log, err := d.GetInstanceConsoleLog(name, console) if err != nil { @@ -154,6 +166,7 @@ func (c *cmdConsole) Console(d lxd.InstanceServer, name string) error { req := api.InstanceConsolePost{ Width: width, Height: height, + Type: c.flagType, } consoleDisconnect := make(chan bool) From 92d242763baed3e4da81a64341061234ce950d4e Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Fri, 10 Jul 2020 10:25:15 +0200 Subject: [PATCH 11/11] lxc: Handle "--type vga" option in "lxc console" Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxc/console.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/lxc/console.go b/lxc/console.go index a1308d0060..98af6ff94f 100644 --- a/lxc/console.go +++ b/lxc/console.go @@ -4,11 +4,15 @@ import ( "fmt" "io" "io/ioutil" + "net" "os" + "os/exec" + "os/signal" "strconv" "github.com/gorilla/websocket" "github.com/spf13/cobra" + "golang.org/x/sys/unix" "github.com/lxc/lxd/client" "github.com/lxc/lxd/shared" @@ -145,6 +149,16 @@ func (c *cmdConsole) Run(cmd *cobra.Command, args []string) error { } func (c *cmdConsole) Console(d lxd.InstanceServer, name string) error { + switch c.flagType { + case "console": + return c.console(d, name) + case "vga": + return c.vga(d, name) + } + return fmt.Errorf("Unknown console type %q", c.flagType) +} + +func (c *cmdConsole) console(d lxd.InstanceServer, name string) error { // Configure the terminal cfd := int(os.Stdin.Fd()) @@ -166,7 +180,7 @@ func (c *cmdConsole) Console(d lxd.InstanceServer, name string) error { req := api.InstanceConsolePost{ Width: width, Height: height, - Type: c.flagType, + Type: "console", } consoleDisconnect := make(chan bool) @@ -201,3 +215,110 @@ func (c *cmdConsole) Console(d lxd.InstanceServer, name string) error { return nil } + +func (c *cmdConsole) vga(d lxd.InstanceServer, name string) error { + // We currently use the control websocket just to abort in case of + // errors. + controlDone := make(chan struct{}, 1) + handler := func(control *websocket.Conn) { + <-controlDone + closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + control.WriteMessage(websocket.CloseMessage, closeMsg) + } + + // Prepare the remote console + req := api.InstanceConsolePost{ + Type: "vga", + } + + consoleDisconnect := make(chan bool) + sendDisconnect := make(chan bool) + defer close(sendDisconnect) + + consoleArgs := lxd.InstanceConsoleArgs{ + Control: handler, + ConsoleDisconnect: consoleDisconnect, + } + + go func() { + <-sendDisconnect + close(consoleDisconnect) + }() + + // Create a temporary unix socket mirroring the instance's spice + // socket. + path, err := ioutil.TempFile("", "*.spice") + if err != nil { + return err + } + err = os.Remove(path.Name()) + if err != nil { + return err + } + path.Close() + + socket := path.Name() + + listener, err := net.Listen("unix", socket) + if err != nil { + return err + } + defer listener.Close() + + op, connect, err := d.ConsoleInstanceDynamic(name, req, &consoleArgs) + if err != nil { + return err + } + + go func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func(conn io.ReadWriteCloser) { + err = connect(conn) + if err != nil { + select { + case controlDone <- struct{}{}: + } + } + }(conn) + } + }() + + // Use either spicy or remote-viewer if available. + var cmd *exec.Cmd + + spicy, err := exec.LookPath("spicy") + if err == nil { + cmd = exec.Command(spicy, fmt.Sprintf("--uri=spice+unix://%s", socket)) + } else { + remoteViewer, err := exec.LookPath("remote-viewer") + if err == nil { + cmd = exec.Command(remoteViewer, fmt.Sprintf("spice+unix://%s", socket)) + } + } + + if cmd != nil { + cmd.Output() + } else { + fmt.Printf("\n"+i18n.G("No remote desktop client found, raw SPICE socket available at:")+"\n\n%s\n", socket) + + ch := make(chan os.Signal, 10) + signal.Notify(ch, unix.SIGINT) + <-ch + } + + select { + case controlDone <- struct{}{}: + } + + // Wait for the operation to complete + err = op.Wait() + if err != nil { + return err + } + + return nil +}
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel