[lxc-devel] [lxd/master] Spice console

freeekanayaka on Github lxc-bot at linuxcontainers.org
Fri Jul 10 11:50:05 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200710/e300060b/attachment.bin>
-------------- next part --------------
From e84b6de416ddcb685fd30d556ecfd2432e70e9b5 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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.ekanayaka at 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
+}


More information about the lxc-devel mailing list