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

Reply via email to