[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