Skip to content

Commit

Permalink
Image export over devlxd for virtual machines (canonical#13878)
Browse files Browse the repository at this point in the history
Adds an endpoint to the LXD agent `/dev/lxd` socket listener that
proxies requests to `/1.0/images/{fingerprint}/export` on the host LXD
devlxd listener.

Additionally cleans up `lxd-agent/devlxd.go` and moves the
`security.devlxd.images` config key from the container specific config
key map into the shared container/VM config key map and regenerates the
metadata.

I just worked on this to speed up my local test runs :)

Closes canonical#12589
  • Loading branch information
tomponline authored Aug 6, 2024
2 parents da2f17b + 6e8512f commit da24dfd
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 29 deletions.
1 change: 0 additions & 1 deletion doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2112,7 +2112,6 @@ See {ref}`dev-lxd` for more information.
```

```{config:option} security.devlxd.images instance-security
:condition: "container"
:defaultdesc: "`false`"
:liveupdate: "no"
:shortdesc: "Controls the availability of the `/1.0/images` API over `devlxd`"
Expand Down
124 changes: 107 additions & 17 deletions lxd-agent/devlxd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"io"
"net"
"net/http"
"net/url"
Expand All @@ -18,8 +19,11 @@ import (
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/logger"
"github.com/canonical/lxd/shared/version"
)

type devLXDHandlerFunc func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse

// DevLxdServer creates an http.Server capable of handling requests against the
// /dev/lxd Unix socket endpoint created inside VMs.
func devLxdServer(d *Daemon) *http.Server {
Expand All @@ -37,7 +41,7 @@ type devLxdHandler struct {
* server side right now either, I went the simple route to avoid
* needless noise.
*/
f func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse
handlerFunc devLXDHandlerFunc
}

func getVsockClient(d *Daemon) (lxd.InstanceServer, error) {
Expand All @@ -55,7 +59,12 @@ func getVsockClient(d *Daemon) (lxd.InstanceServer, error) {
return server, nil
}

var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
var devlxdConfigGet = devLxdHandler{
path: "/1.0/config",
handlerFunc: devlxdConfigGetHandler,
}

func devlxdConfigGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err))
Expand All @@ -82,9 +91,14 @@ var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, w http.Respon
}
}
return okResponse(filtered, "json")
}}
}

var devlxdConfigKeyGet = devLxdHandler{
path: "/1.0/config/{key}",
handlerFunc: devlxdConfigKeyGetHandler,
}

var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
func devlxdConfigKeyGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
key, err := url.PathUnescape(mux.Vars(r)["key"])
if err != nil {
return &devLxdResponse{"bad request", http.StatusBadRequest, "raw"}
Expand Down Expand Up @@ -114,9 +128,14 @@ var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, w ht
}

return okResponse(value, "raw")
}}
}

var devlxdMetadataGet = devLxdHandler{
path: "/1.0/meta-data",
handlerFunc: devlxdMetadataGetHandler,
}

var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
func devlxdMetadataGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
var client lxd.InstanceServer
var err error

Expand Down Expand Up @@ -148,18 +167,28 @@ var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, w http.R
}

return okResponse(metaData, "raw")
}}
}

var devLxdEventsGet = devLxdHandler{
path: "/1.0/events",
handlerFunc: devlxdEventsGetHandler,
}

var devLxdEventsGet = devLxdHandler{"/1.0/events", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
func devlxdEventsGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
err := eventsGet(d, r).Render(w)
if err != nil {
return smartResponse(err)
}

return okResponse("", "raw")
}}
}

var devlxdAPIGet = devLxdHandler{
path: "/1.0",
handlerFunc: devlxdAPIGetHandler,
}

var devlxdAPIGet = devLxdHandler{"/1.0", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
func devlxdAPIGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err))
Expand Down Expand Up @@ -191,9 +220,14 @@ var devlxdAPIGet = devLxdHandler{"/1.0", func(d *Daemon, w http.ResponseWriter,
}

return &devLxdResponse{fmt.Sprintf("method %q not allowed", r.Method), http.StatusBadRequest, "raw"}
}}
}

var devlxdDevicesGet = devLxdHandler{
path: "/1.0/devices",
handlerFunc: devlxdDevicesGetHandler,
}

var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
func devlxdDevicesGetHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err))
Expand All @@ -214,23 +248,79 @@ var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, w http.Resp
}

return okResponse(devices, "json")
}}
}

var devlxdImageExport = devLxdHandler{
path: "/1.0/images/{fingerprint}/export",
handlerFunc: devlxdImageExportHandler,
}

func devlxdImageExportHandler(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
// Extract the fingerprint.
fingerprint, err := url.PathUnescape(mux.Vars(r)["fingerprint"])
if err != nil {
return smartResponse(err)
}

// Get a http.Client.
client, err := getClient(d.serverCID, int(d.serverPort), d.serverCertificate)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to LXD over vsock: %w", err))
}

// Remove the request URI, this cannot be set on requests.
r.RequestURI = ""

// Set up the request URL with the correct host.
r.URL = &api.NewURL().Scheme("https").Host("custom.socket").Path(version.APIVersion, "images", fingerprint, "export").URL

// Proxy the request.
resp, err := client.Do(r)
if err != nil {
return errorResponse(http.StatusInternalServerError, err.Error())
}

// Set headers from the host LXD.
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Set(k, v)
}
}

// Copy headers and response body.
w.WriteHeader(resp.StatusCode)
_, err = io.Copy(w, resp.Body)
if err != nil {
return smartResponse(err)
}

return nil
}

var handlers = []devLxdHandler{
{"/", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
return okResponse([]string{"/1.0"}, "json")
}},
{
path: "/",
handlerFunc: func(d *Daemon, w http.ResponseWriter, r *http.Request) *devLxdResponse {
return okResponse([]string{"/1.0"}, "json")
},
},
devlxdAPIGet,
devlxdConfigGet,
devlxdConfigKeyGet,
devlxdMetadataGet,
devLxdEventsGet,
devlxdDevicesGet,
devlxdImageExport,
}

func hoistReq(f func(*Daemon, http.ResponseWriter, *http.Request) *devLxdResponse, d *Daemon) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
resp := f(d, w, r)
if resp == nil {
// The handler has already written the response.
return
}

if resp.code != http.StatusOK {
http.Error(w, fmt.Sprintf("%s", resp.content), resp.code)
} else if resp.ctype == "json" {
Expand All @@ -249,7 +339,7 @@ func devLxdAPI(d *Daemon) http.Handler {
m.UseEncodedPath() // Allow encoded values in path segments.

for _, handler := range handlers {
m.HandleFunc(handler.path, hoistReq(handler.f, d))
m.HandleFunc(handler.path, hoistReq(handler.handlerFunc, d))
}

return m
Expand Down
19 changes: 9 additions & 10 deletions lxd/instance/instancetype/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,15 @@ var InstanceConfigKeysAny = map[string]func(value string) error{
// shortdesc: Whether `/dev/lxd` is present in the instance
"security.devlxd": validate.Optional(validate.IsBool),

// lxdmeta:generate(entities=instance; group=security; key=security.devlxd.images)
//
// ---
// type: bool
// defaultdesc: `false`
// liveupdate: no
// shortdesc: Controls the availability of the `/1.0/images` API over `devlxd`
"security.devlxd.images": validate.Optional(validate.IsBool),

// lxdmeta:generate(entities=instance; group=security; key=security.protection.delete)
//
// ---
Expand Down Expand Up @@ -676,16 +685,6 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
// shortdesc: Raw Seccomp configuration
"raw.seccomp": validate.IsAny,

// lxdmeta:generate(entities=instance; group=security; key=security.devlxd.images)
//
// ---
// type: bool
// defaultdesc: `false`
// liveupdate: no
// condition: container
// shortdesc: Controls the availability of the `/1.0/images` API over `devlxd`
"security.devlxd.images": validate.Optional(validate.IsBool),

// lxdmeta:generate(entities=instance; group=security; key=security.idmap.base)
// Setting this option overrides auto-detection.
// ---
Expand Down
1 change: 0 additions & 1 deletion lxd/metadata/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -2392,7 +2392,6 @@
},
{
"security.devlxd.images": {
"condition": "container",
"defaultdesc": "`false`",
"liveupdate": "no",
"longdesc": "",
Expand Down

0 comments on commit da24dfd

Please sign in to comment.