1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 09:46:19 +01:00
Files
incus/cmd/incus-agent/dev_incus.go
2025-07-07 17:30:12 +02:00

345 lines
9.0 KiB
Go

package main
import (
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
incus "github.com/lxc/incus/v6/client"
"github.com/lxc/incus/v6/internal/server/daemon"
"github.com/lxc/incus/v6/internal/server/device/config"
localUtil "github.com/lxc/incus/v6/internal/server/util"
api "github.com/lxc/incus/v6/shared/api/guest"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/util"
)
// DevIncusServer creates an http.Server capable of handling requests against the
// /dev/incus Unix socket endpoint created inside VMs.
func devIncusServer(d *Daemon) *http.Server {
return &http.Server{
Handler: devIncusAPI(d),
}
}
type devIncusHandler struct {
path string
/*
* This API will have to be changed slightly when we decide to support
* websocket events upgrading, but since we don't have events on the
* server side right now either, I went the simple route to avoid
* needless noise.
*/
f func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse
}
func getVsockClient(d *Daemon) (incus.InstanceServer, error) {
// Try connecting to the host.
client, err := getClient(d.serverCID, int(d.serverPort), d.serverCertificate)
if err != nil {
return nil, err
}
server, err := incus.ConnectIncusHTTP(nil, client)
if err != nil {
return nil, err
}
return server, nil
}
var DevIncusConfigGet = devIncusHandler{"/1.0/config", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to the host over vsock: %w", err))
}
defer client.Disconnect()
resp, _, err := client.RawQuery("GET", "/1.0/config", nil, "")
if err != nil {
return smartResponse(err)
}
var config []string
err = resp.MetadataAsStruct(&config)
if err != nil {
return smartResponse(fmt.Errorf("Failed parsing response from host: %w", err))
}
filtered := []string{}
for _, k := range config {
if strings.HasPrefix(k, "/1.0/config/user.") || strings.HasPrefix(k, "/1.0/config/cloud-init.") {
filtered = append(filtered, k)
}
}
return okResponse(filtered, "json")
}}
var DevIncusConfigKeyGet = devIncusHandler{"/1.0/config/{key}", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
key := r.PathValue("key")
if key == "" {
return &devIncusResponse{"bad request", http.StatusBadRequest, "raw"}
}
if !strings.HasPrefix(key, "user.") && !strings.HasPrefix(key, "cloud-init.") {
return &devIncusResponse{"not authorized", http.StatusForbidden, "raw"}
}
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to host over vsock: %w", err))
}
defer client.Disconnect()
resp, _, err := client.RawQuery("GET", fmt.Sprintf("/1.0/config/%s", key), nil, "")
if err != nil {
return smartResponse(err)
}
var value string
err = resp.MetadataAsStruct(&value)
if err != nil {
return smartResponse(fmt.Errorf("Failed parsing response from host: %w", err))
}
return okResponse(value, "raw")
}}
var DevIncusMetadataGet = devIncusHandler{"/1.0/meta-data", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
var client incus.InstanceServer
var err error
for range 10 {
client, err = getVsockClient(d)
if err == nil {
break
}
time.Sleep(500 * time.Millisecond)
}
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to host over vsock: %w", err))
}
defer client.Disconnect()
resp, _, err := client.RawQuery("GET", "/1.0/meta-data", nil, "")
if err != nil {
return smartResponse(err)
}
var metaData string
err = resp.MetadataAsStruct(&metaData)
if err != nil {
return smartResponse(fmt.Errorf("Failed parsing response from host: %w", err))
}
return okResponse(metaData, "raw")
}}
var devIncusEventsGet = devIncusHandler{"/1.0/events", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
err := eventsGet(d, r).Render(w)
if err != nil {
return smartResponse(err)
}
return okResponse("", "raw")
}}
var DevIncusAPIGet = devIncusHandler{"/1.0", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to host over vsock: %w", err))
}
defer client.Disconnect()
if r.Method == "GET" {
resp, _, err := client.RawQuery(r.Method, "/1.0", nil, "")
if err != nil {
return smartResponse(err)
}
var instanceData api.DevIncusGet
err = resp.MetadataAsStruct(&instanceData)
if err != nil {
return smartResponse(fmt.Errorf("Failed parsing response from host: %w", err))
}
return okResponse(instanceData, "json")
} else if r.Method == "PATCH" {
_, _, err := client.RawQuery(r.Method, "/1.0", r.Body, "")
if err != nil {
return smartResponse(err)
}
return okResponse("", "raw")
}
return &devIncusResponse{fmt.Sprintf("method %q not allowed", r.Method), http.StatusBadRequest, "raw"}
}}
var DevIncusDevicesGet = devIncusHandler{"/1.0/devices", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
client, err := getVsockClient(d)
if err != nil {
return smartResponse(fmt.Errorf("Failed connecting to host over vsock: %w", err))
}
defer client.Disconnect()
resp, _, err := client.RawQuery("GET", "/1.0/devices", nil, "")
if err != nil {
return smartResponse(err)
}
var devices config.Devices
err = resp.MetadataAsStruct(&devices)
if err != nil {
return smartResponse(fmt.Errorf("Failed parsing response from host: %w", err))
}
return okResponse(devices, "json")
}}
var handlers = []devIncusHandler{
{"/", func(d *Daemon, w http.ResponseWriter, r *http.Request) *devIncusResponse {
return okResponse([]string{"/1.0"}, "json")
}},
DevIncusAPIGet,
DevIncusConfigGet,
DevIncusConfigKeyGet,
DevIncusMetadataGet,
devIncusEventsGet,
DevIncusDevicesGet,
}
func hoistReq(f func(*Daemon, http.ResponseWriter, *http.Request) *devIncusResponse, d *Daemon) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
resp := f(d, w, r)
if resp.code != http.StatusOK {
http.Error(w, fmt.Sprintf("%s", resp.content), resp.code)
} else if resp.ctype == "json" {
w.Header().Set("Content-Type", "application/json")
var debugLogger logger.Logger
if daemon.Debug {
debugLogger = logger.Logger(logger.Log)
}
_ = localUtil.WriteJSON(w, resp.content, debugLogger)
} else if resp.ctype != "websocket" {
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = fmt.Fprint(w, resp.content.(string))
}
}
}
func devIncusAPI(d *Daemon) http.Handler {
router := http.NewServeMux()
for _, handler := range handlers {
router.HandleFunc(handler.path, hoistReq(handler.f, d))
}
return router
}
// Create a new net.Listener bound to the unix socket of the DevIncus endpoint.
func createDevIncuslListener(dir string) (net.Listener, error) {
path := filepath.Join(dir, "incus", "sock")
err := os.MkdirAll(filepath.Dir(path), 0o755)
if err != nil {
return nil, err
}
// Add a symlink for legacy support.
err = os.Symlink(filepath.Join(dir, "incus"), filepath.Join(dir, "lxd"))
if err != nil && !os.IsExist(err) {
return nil, err
}
// If this socket exists, that means a previous agent instance died and
// didn't clean up. We assume that such agent instance is actually dead
// if we get this far, since localCreateListener() tries to connect to
// the actual incus socket to make sure that it is actually dead. So, it
// is safe to remove it here without any checks.
//
// Also, it would be nice to SO_REUSEADDR here so we don't have to
// delete the socket, but we can't:
// http://stackoverflow.com/questions/15716302/so-reuseaddr-and-af-unix
//
// Note that this will force clients to reconnect when the daemon is restarted.
err = socketUnixRemoveStale(path)
if err != nil {
return nil, err
}
listener, err := socketUnixListen(path)
if err != nil {
return nil, err
}
err = socketUnixSetPermissions(path, 0o600)
if err != nil {
_ = listener.Close()
return nil, err
}
return listener, nil
}
// Remove any stale socket file at the given path.
func socketUnixRemoveStale(path string) error {
// If there's no socket file at all, there's nothing to do.
if !util.PathExists(path) {
return nil
}
logger.Debugf("Detected stale unix socket, deleting")
err := os.Remove(path)
if err != nil {
return fmt.Errorf("could not delete stale local socket: %w", err)
}
return nil
}
// Change the file mode of the given unix socket file.
func socketUnixSetPermissions(path string, mode os.FileMode) error {
err := os.Chmod(path, mode)
if err != nil {
return fmt.Errorf("cannot set permissions on local socket: %w", err)
}
return nil
}
// Bind to the given unix socket path.
func socketUnixListen(path string) (net.Listener, error) {
addr, err := net.ResolveUnixAddr("unix", path)
if err != nil {
return nil, fmt.Errorf("cannot resolve socket address: %w", err)
}
listener, err := net.ListenUnix("unix", addr)
if err != nil {
return nil, fmt.Errorf("cannot bind socket: %w", err)
}
return listener, err
}