Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image export over devlxd for virtual machines #13878

Merged
merged 5 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading