diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 30e9030d905c..000000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @tomponline diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000000..97d91bae52a7 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,2 @@ +# +shell=bash diff --git a/Makefile b/Makefile index 61196f4b53d1..957995e7c994 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ GOPATH ?= $(shell go env GOPATH) CGO_LDFLAGS_ALLOW ?= (-Wl,-wrap,pthread_create)|(-Wl,-z,now) SPHINXENV=doc/.sphinx/venv/bin/activate SPHINXPIPPATH=doc/.sphinx/venv/bin/pip -GOMIN=1.22.4 +GOMIN=1.22.5 ifneq "$(wildcard vendor)" "" DQLITE_PATH=$(CURDIR)/vendor/dqlite @@ -247,8 +247,7 @@ ifeq ($(shell command -v flake8),) exit 1 endif flake8 test/deps/import-busybox - shellcheck --shell bash test/*.sh test/includes/*.sh test/suites/*.sh test/backends/*.sh test/lint/*.sh - shellcheck test/extras/*.sh + shellcheck test/*.sh test/includes/*.sh test/suites/*.sh test/backends/*.sh test/lint/*.sh test/extras/*.sh NOT_EXEC="$(shell find test/lint -type f -not -executable)"; \ if [ -n "$$NOT_EXEC" ]; then \ echo "lint scripts not executable: $$NOT_EXEC"; \ diff --git a/doc/.custom_wordlist.txt b/doc/.custom_wordlist.txt index 5989f235383c..3714e581f865 100644 --- a/doc/.custom_wordlist.txt +++ b/doc/.custom_wordlist.txt @@ -13,6 +13,7 @@ ASN AXFR backend backends +backticks balancers benchmarking BGP diff --git a/doc/.sphinx/.markdownlint/exceptions.txt b/doc/.sphinx/.markdownlint/exceptions.txt index 902dfbdf5738..dff8d656e4f0 100644 --- a/doc/.sphinx/.markdownlint/exceptions.txt +++ b/doc/.sphinx/.markdownlint/exceptions.txt @@ -9,3 +9,15 @@ .tmp/doc/howto/network_forwards.md:67: MD032 Lists should be surrounded by blank lines .tmp/doc/howto/network_forwards.md:72: MD032 Lists should be surrounded by blank lines .tmp/doc/doc-cheat-sheet-myst.md:171: MD034 Bare URL used +.tmp/doc/contributing.md:124: MD004 Unordered list style +.tmp/doc/contributing.md:129: MD004 Unordered list style +.tmp/doc/contributing.md:136: MD004 Unordered list style +.tmp/doc/contributing.md:124: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:125: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:126: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:129: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:131: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:133: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:136: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:138: MD005 Inconsistent indentation for list items at the same level +.tmp/doc/contributing.md:125: MD032 Lists should be surrounded by blank lines diff --git a/doc/authentication.md b/doc/authentication.md index e5f8e768a955..6e6045ce4f4a 100644 --- a/doc/authentication.md +++ b/doc/authentication.md @@ -114,26 +114,59 @@ Alternatively, the clients can provide the token directly when adding the remote In a {abbr}`PKI (Public key infrastructure)` setup, a system administrator manages a central PKI that issues client certificates for all the LXD clients and server certificates for all the LXD daemons. -In PKI mode, TLS authentication requires that client certificates are signed be the CA. +In PKI mode, TLS authentication requires that client certificates are signed be the {abbr}`CA (Certificate authority)`. This requirement does not apply to clients that authenticate via [OIDC](authentication-openid). -To enable PKI mode, complete the following steps: +The steps for enabling PKI mode differ slightly depending on whether you use an ACME provider in addition (see {ref}`authentication-server-certificate`). -1. Add the {abbr}`CA (Certificate authority)` certificate to all machines: +`````{tabs} +````{group-tab} Only PKI +If you use a PKI system, both the server certificates and the client certificates are issued by a secondary CA. + +1. Add the CA certificate to all machines: - Place the `client.ca` file in the clients' configuration directories (`~/.config/lxc` or `~/snap/lxd/common/config` for snap users). - Place the `server.ca` file in the server's configuration directory (`/var/lib/lxd` or `/var/snap/lxd/common/lxd` for snap users). + + ```{note} + In a cluster setup, the CA certificate must be named `cluster.ca`, and the same file must be added to all cluster members. + ``` + 1. Place the certificates issued by the CA in the clients' configuration directories, replacing the automatically generated `client.crt` and `client.key` files. 1. If you want clients to automatically trust the server, place the certificates issued by the CA in the server's configuration directory, replacing the automatically generated `server.crt` and `server.key` files. - When a client adds a PKI-enabled server as a remote, it checks the server certificate and prompts the user to trust the server certificate only if the certificate has not been signed by the CA. + ```{note} + In a cluster setup, the certificate files must be named `cluster.crt` and `cluster.key`. + They must be identical on all cluster members. + ``` + + When a client adds a PKI-enabled server or cluster as a remote, it checks the server certificate and prompts the user to trust the server certificate only if the certificate has not been signed by the CA. 1. Restart the LXD daemon. +```` +````{group-tab} PKI and ACME +If you use a PKI system alongside an ACME provider, the server certificates are issued by the ACME provider, and the client certificates are issued by a secondary CA. -Note that CA-signed client certificates are not automatically trusted. +1. Place the CA certificate for the server (`server.ca`) in the server's configuration directory (`/var/lib/lxd` or `/var/snap/lxd/common/lxd` for snap users), so that the server can authenticate the clients. + + ```{note} + In a cluster setup, the CA certificate must be named `cluster.ca`, and the same file must be added to all cluster members. + ``` + +1. Place the certificates issued by the CA in the clients' configuration directories, replacing the automatically generated `client.crt` and `client.key` files. +1. Restart the LXD daemon. + +```` +````` + +#### Trusting certificates + +CA-signed client certificates are not automatically trusted. You must still add them to the server in one of the ways described in {ref}`authentication-trusted-clients`. To automatically trust CA-signed client certificates, set the {config:option}`server-core:core.trust_ca_certificates` server configuration to true. When `core.trust_ca_certificates` is enabled, any new clients with a CA-signed certificate will have full access to LXD. +#### Revoking certificates + To revoke certificates via the PKI, place a certificate revocation list in the server's configuration directory as `ca.crl` and restart the LXD daemon. A client with a CA-signed certificate that has been revoked, and is present in `ca.crl`, will not be able to authenticate with LXD, nor add LXD as a remote via [mutual TLS](authentication-trusted-clients). @@ -172,14 +205,6 @@ This can be achieved by using a reverse proxy such as [HAProxy](http://www.hapro Here's a minimal HAProxy configuration that uses `lxd.example.net` as the domain. After the certificate has been issued, LXD will be reachable from `https://lxd.example.net/`. -```{note} -A [PKI system](authentication-pki) can be used alongside an ACME provider to verify client certificates. -In this case, a secondary CA issues certificates to clients. -A `server.ca` and `ca.crl` file should still be added to the server's configuration directory, but server certificates must not be issued by the secondary CA, as they are managed by the ACME provider. -Client certificates issued by the secondary CA should be added to the clients' configuration directories, but the `client.ca` file *must not* be added to the clients' configuration directories. -This is because the server certificates were issued by the ACME provider and not the secondary CA. -``` - ``` # Global configuration global diff --git a/doc/contributing.md b/doc/contributing.md index ae54f8c328a1..b3d88b3525ef 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -93,6 +93,52 @@ You can (and should!) run these tests locally as well with the following command - Check the Markdown formatting: `make doc-lint` - Check for inclusive language: `make doc-woke` +### Document instructions (how-to guides) + +LXD can be used with different clients, most importantly the command-line interface (CLI), the API, and the UI. +The documentation contains instructions for all of these. +Therefore, when adding or updating how-to guides, remember to update the documentation for all clients. + +Information that is different for each client should be put on tabs: + +````` +````{tabs} +```{group-tab} CLI +[...] +``` +```{group-tab} API +[...] +``` +```{group-tab} UI +[...] +``` +```` +````` + +```{tip} +You might need to increase the number of backticks (`) if there are code blocks or other directives in the tab content. +``` + +Adhere to the following guidelines when adding instructions: + +CLI instructions +: - Add a link to the command reference of the `lxc` command (syntax example: ``[`lxc init`](lxc_init.md)``). + - You don't need to document all available flags of a command, but you should mention any that are especially relevant. + - Examples are very helpful, so add a few if it makes sense. + +API instructions +: - If possible, use [`lxc query`](lxc_query.md) to demonstrate the API calls. + For more complicated calls, use curl or other widely available tools. + - In the request data, include all fields that are required for the request to succeed. + Keep it as simple as possible though - no need to include all available fields. + - Add a link to the API call reference (syntax example: ``[`POST /1.0/instances`](swagger:/instances/instances_post)``). + +UI instructions +: - You can include screenshots to illustrate the instructions, but use them sparingly. + Screenshots are difficult to maintain and keep up-to-date. + - When referring to labels in the UI, use the `{guilabel}` role. + For example: ``To create an instance, go to the {guilabel}`Instances` section and click {guilabel}`Create instance`.`` + ### Document configuration options The documentation of configuration options is extracted from comments in the Go code. diff --git a/doc/howto/import_machines_to_instances.md b/doc/howto/import_machines_to_instances.md index cbc83bfa52b3..39046e25bc95 100644 --- a/doc/howto/import_machines_to_instances.md +++ b/doc/howto/import_machines_to_instances.md @@ -69,11 +69,6 @@ Complete the following steps to migrate an existing machine to a LXD instance: The tool then asks you to provide the information required for the migration. - ```{tip} - As an alternative to running the tool interactively, you can provide the configuration as parameters to the command. - See `./bin.linux.lxd-migrate --help` for more information. - ``` - 1. Specify the LXD server URL, either as an IP address or as a DNS name. ```{note} @@ -157,8 +152,8 @@ Complete the following steps to migrate an existing machine to a LXD instance: Please pick one of the options above [default=1]: 4 Please provide the storage pool to use: default - Do you want to change the storage size? [default=no]: yes - Please specify the storage size: 20GiB + Do you want to change the storage volume size? [default=no]: yes + Please specify the storage volume size: 20GiB Instance to be created: Name: foo @@ -166,7 +161,7 @@ Complete the following steps to migrate an existing machine to a LXD instance: Type: container Source: / Storage pool: default - Storage pool size: 20GiB + Storage volume size: 20GiB Config: limits.cpu: "2" @@ -186,7 +181,7 @@ Complete the following steps to migrate an existing machine to a LXD instance: Type: container Source: / Storage pool: default - Storage pool size: 20GiB + Storage volume size: 20GiB Network name: lxdbr0 Config: limits.cpu: "2" @@ -264,8 +259,8 @@ Complete the following steps to migrate an existing machine to a LXD instance: Please pick one of the options above [default=1]: 4 Please provide the storage pool to use: default - Do you want to change the storage size? [default=no]: yes - Please specify the storage size: 20GiB + Do you want to change the storage volume size? [default=no]: yes + Please specify the storage volume size: 20GiB Instance to be created: Name: foo @@ -273,7 +268,7 @@ Complete the following steps to migrate an existing machine to a LXD instance: Type: virtual-machine Source: ./virtual-machine.img Storage pool: default - Storage pool size: 20GiB + Storage volume size: 20GiB Config: limits.cpu: "2" security.secureboot: "false" @@ -294,7 +289,7 @@ Complete the following steps to migrate an existing machine to a LXD instance: Type: virtual-machine Source: ./virtual-machine.img Storage pool: default - Storage pool size: 20GiB + Storage volume size: 20GiB Network name: lxdbr0 Config: limits.cpu: "2" diff --git a/doc/howto/index.md b/doc/howto/index.md index 2263fd6cbd06..ca3517582302 100644 --- a/doc/howto/index.md +++ b/doc/howto/index.md @@ -39,7 +39,7 @@ You'll also need to set up and configure other entities. ## Get ready for production Once you are ready for production, consider setting up a LXD cluster to support the required load. -You should also monitor you server or servers and configure them for the expected load. +You should also monitor your server or servers and configure them for the expected load. ```{toctree} :titlesonly: diff --git a/doc/metrics.md b/doc/metrics.md index 0462030d59cb..3f0dfb9a98cc 100644 --- a/doc/metrics.md +++ b/doc/metrics.md @@ -124,7 +124,7 @@ cp /var/snap/lxd/common/lxd/server.crt /var/snap/prometheus/common/tls/ # Create a symbolic link pointing to tls directory that you created # https://bugs.launchpad.net/prometheus-snap/+bug/2066910 -ln -s /var/snap/prometheus/common/tls/ /var/snap/prometheus/current/tls/ +ln -s /var/snap/prometheus/common/tls/ /var/snap/prometheus/current/tls ``` If you are not using the snap, you must also make sure that Prometheus can read these files (usually, Prometheus is run as user `prometheus`): diff --git a/doc/reference/storage_drivers.md b/doc/reference/storage_drivers.md index 07d8f0fa32af..195d60534d16 100644 --- a/doc/reference/storage_drivers.md +++ b/doc/reference/storage_drivers.md @@ -27,22 +27,23 @@ See the corresponding pages for driver-specific information and configuration op Where possible, LXD uses the advanced features of each storage system to optimize operations. -Feature | Directory | Btrfs | LVM | ZFS | Ceph RBD | CephFS | Ceph Object | Dell PowerFlex -:--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- -{ref}`storage-optimized-image-storage` | no | yes | yes | yes | yes | n/a | n/a | no -Optimized instance creation | no | yes | yes | yes | yes | n/a | n/a | no -Optimized snapshot creation | no | yes | yes | yes | yes | yes | n/a | yes -Optimized image transfer | no | yes | no | yes | yes | n/a | n/a | no -{ref}`storage-optimized-volume-transfer` | no | yes | no | yes | yes[^1] | n/a | n/a | no -{ref}`storage-optimized-volume-refresh` | no | yes | yes[^2] | yes | yes[^3] | n/a | n/a | no -Copy on write | no | yes | yes | yes | yes | yes | n/a | yes -Block based | no | no | yes | no | yes | no | n/a | yes -Instant cloning | no | yes | yes | yes | yes | yes | n/a | no -Storage driver usable inside a container | yes | yes | no | yes[^4] | no | n/a | n/a | no -Restore from older snapshots (not latest) | yes | yes | yes | no | yes | yes | n/a | yes -Storage quotas | yes[^5] | yes | yes | yes | yes | yes | yes | yes -Available on `lxd init` | yes | yes | yes | yes | yes | no | no | no -Object storage | yes | yes | yes | yes | no | no | yes | no +Feature | Directory | Btrfs | LVM | ZFS | Ceph RBD | CephFS | Ceph Object | Dell PowerFlex +:--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- +{ref}`storage-optimized-image-storage` | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ | ➖ | ❌ +Optimized instance creation | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ | ➖ | ❌ +Optimized snapshot creation | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ | ✅ +Optimized image transfer | ❌ | ✅ | ❌ | ✅ | ✅ | ➖ | ➖ | ❌ +Optimized backup (import/export) | ❌ | ✅ | ❌ | ✅ | ❌ | ➖ | ➖ | ❌ +{ref}`storage-optimized-volume-transfer` | ❌ | ✅ | ❌ | ✅ | ✅[^1] | ➖ | ➖ | ❌ +{ref}`storage-optimized-volume-refresh` | ❌ | ✅ | ✅[^2] | ✅ | ✅[^3] | ➖ | ➖ | ❌ +Copy on write | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ | ✅ +Block based | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ➖ | ✅ +Instant cloning | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ | ❌ +Storage driver usable inside a container | ✅ | ✅ | ❌ | ✅[^4] | ❌ | ➖ | ➖ | ❌ +Restore from older snapshots (not latest) | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ➖ | ✅ +Storage quotas | ✅[^5] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ +Available on `lxd init` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ +Object storage | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ [^1]: Volumes of type `block` will fall back to non-optimized transfer when migrating to an older LXD server that doesn't yet support the `RBD_AND_RSYNC` migration type. [^2]: Requires {config:option}`storage-lvm-pool-conf:lvm.use_thinpool` to be enabled. Only when refreshing local volumes. diff --git a/doc/requirements.md b/doc/requirements.md index fd46e0b1ccef..6a08460851d7 100644 --- a/doc/requirements.md +++ b/doc/requirements.md @@ -4,7 +4,7 @@ (requirements-go)= ## Go -LXD requires Go 1.22.4 or higher and is only tested with the Golang compiler. +LXD requires Go 1.22.5 or higher and is only tested with the Golang compiler. We recommend having at least 2GiB of RAM to allow the build to complete. diff --git a/go.mod b/go.mod index f474d5782d93..6d965231be35 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/canonical/lxd -go 1.22.4 +go 1.22.5 require ( github.com/Rican7/retry v0.3.1 @@ -34,16 +34,16 @@ require ( github.com/mdlayher/netx v0.0.0-20230430222610-7e21880baee8 github.com/mdlayher/vsock v1.2.1 github.com/miekg/dns v1.1.61 - github.com/minio/minio-go/v7 v7.0.73 + github.com/minio/minio-go/v7 v7.0.74 github.com/mitchellh/mapstructure v1.5.0 github.com/oklog/ulid/v2 v2.1.0 github.com/olekukonko/tablewriter v0.0.5 - github.com/openfga/api/proto v0.0.0-20240612172407-db6f98774490 + github.com/openfga/api/proto v0.0.0-20240722084519-a9261bb50796 github.com/openfga/language/pkg/go v0.2.0-beta.0 - github.com/openfga/openfga v1.5.5 + github.com/openfga/openfga v1.5.6 github.com/osrg/gobgp/v3 v3.28.0 github.com/pkg/sftp v1.13.6 - github.com/pkg/xattr v0.4.9 + github.com/pkg/xattr v0.4.10 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 @@ -51,10 +51,10 @@ require ( github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 github.com/vishvananda/netlink v1.2.1-beta.2 github.com/zitadel/oidc/v3 v3.26.0 - go.starlark.net v0.0.0-20240520160348-046347dcd104 + go.starlark.net v0.0.0-20240705175910-70002002b310 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.7.0 golang.org/x/sys v0.22.0 @@ -63,7 +63,7 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gopkg.in/yaml.v2 v2.4.0 - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 ) require ( @@ -75,10 +75,11 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7 // indirect + github.com/digitalocean/go-libvirt v0.0.0-20240709142323-d8406205c752 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eapache/channels v1.1.0 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ini/ini v1.67.0 // indirect @@ -114,13 +115,14 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/wrap v0.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.54.0 // indirect + github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.0 // indirect @@ -139,20 +141,19 @@ require ( github.com/zitadel/logging v0.6.0 // indirect github.com/zitadel/schema v1.3.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect - google.golang.org/grpc v1.64.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/grpc v1.65.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1d4b2c73c036..900f9427ef76 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/dell/goscaleio v1.15.0 h1:DzI1ZlQhdIR+V4AKGOMwz1Viu2bAtj3N6kTyixB0Qg8 github.com/dell/goscaleio v1.15.0/go.mod h1:h7SCmReARG/szFWBMQGETGkZObknhS45lQipQbtdmJ8= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7 h1:KMOLn19gbh7KbPEgu76ZIf/b2CnnYhC2GFLgLiN/YkA= -github.com/digitalocean/go-libvirt v0.0.0-20240610184155-f66fb3c0f6d7/go.mod h1:DMUPOdO9OHCbF88MGmFNUnCBH9GLjeHl2RaA49Vy3vo= +github.com/digitalocean/go-libvirt v0.0.0-20240709142323-d8406205c752 h1:NI7XEcHzWVvBfVjSVK6Qk4wmrUfoyQxCNpBjrHelZFk= +github.com/digitalocean/go-libvirt v0.0.0-20240709142323-d8406205c752/go.mod h1:/Ok8PA2qi/ve0Py38+oL+VxoYmlowigYRyLEODRYdgc= github.com/digitalocean/go-qemu v0.0.0-20230711162256-2e3d0186973e h1:x5PInTuXLddHWHlePCNAcM8QtUfOGx44f3UmYPMtDcI= github.com/digitalocean/go-qemu v0.0.0-20230711162256-2e3d0186973e/go.mod h1:K4+o74YGNjOb9N6yyG+LPj1NjHtk+Qz0IYQPvirbaLs= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= @@ -130,6 +130,8 @@ github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4 github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -183,8 +185,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -492,8 +494,8 @@ github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.73 h1:qr2vi96Qm7kZ4v7LLebjte+MQh621fFWnv93p12htEo= -github.com/minio/minio-go/v7 v7.0.73/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= +github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0= +github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -525,6 +527,8 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk= github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -537,12 +541,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/openfga/api/proto v0.0.0-20240612172407-db6f98774490 h1:Osd/j+jTWlZi/aXwKjzRPfsFKmdhB8nYMlidpKST8f8= -github.com/openfga/api/proto v0.0.0-20240612172407-db6f98774490/go.mod h1:gil5LBD8tSdFQbUkCQdnXsoeU9kDJdJgbGdHkgJfcd0= +github.com/openfga/api/proto v0.0.0-20240722084519-a9261bb50796 h1:4z3o/S0mxuoB3n1p4Xrg72cLY1V6Zj4z+I6sEFHcPpk= +github.com/openfga/api/proto v0.0.0-20240722084519-a9261bb50796/go.mod h1:gil5LBD8tSdFQbUkCQdnXsoeU9kDJdJgbGdHkgJfcd0= github.com/openfga/language/pkg/go v0.2.0-beta.0 h1:dTvgDkQImfNnH1iDvxnUIbz4INvKr4kS46dI12oAEzM= github.com/openfga/language/pkg/go v0.2.0-beta.0/go.mod h1:mCwEY2IQvyNgfEwbfH0C0ERUwtL8z6UjSAF8zgn5Xbg= -github.com/openfga/openfga v1.5.5 h1:KPVX176JuHOCX9iARSacbS06VqxL5+jZ3WvIGws/xQw= -github.com/openfga/openfga v1.5.5/go.mod h1:9R4YjXJZZsd7x+oV2qCVFZNbEOck8DCnXwkaJ3zowwY= +github.com/openfga/openfga v1.5.6 h1:V5VPXbDnThXHORJaP0Hv0kdw0gtS62eV4H0IQk0EqfE= +github.com/openfga/openfga v1.5.6/go.mod h1:Iv2BfL2b6ANYrqWIANSoEveZPh51LV2YnoexrUI8bvU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/osrg/gobgp/v3 v3.28.0 h1:Oy96v6TUiCxMq32b2cmfcREhPFwBoNK+JtBKwjhGQgw= github.com/osrg/gobgp/v3 v3.28.0/go.mod h1:ZGeSti9mURR/o5hf5R6T1FM5g1yiEBZbhP+TuqYJUpI= @@ -558,8 +562,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= -github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= -github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= +github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -573,8 +577,8 @@ github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJL github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -698,20 +702,20 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg= -go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= +go.starlark.net v0.0.0-20240705175910-70002002b310 h1:tEAOMoNmN2MqVNi0MMEWpTtPI4YNCXgxmAGtuv3mST0= +go.starlark.net v0.0.0-20240705175910-70002002b310/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -751,8 +755,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1065,10 +1069,10 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= -google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1089,8 +1093,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1142,8 +1146,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/lxd-migrate/main_migrate.go b/lxd-migrate/main_migrate.go index 21d212c2f02a..9e0d56bbb36a 100644 --- a/lxd-migrate/main_migrate.go +++ b/lxd-migrate/main_migrate.go @@ -71,7 +71,7 @@ func (c *cmdMigrateData) render() string { Mounts []string `yaml:"Mounts,omitempty"` Profiles []string `yaml:"Profiles,omitempty"` StoragePool string `yaml:"Storage pool,omitempty"` - StorageSize string `yaml:"Storage pool size,omitempty"` + StorageSize string `yaml:"Storage volume size,omitempty"` Network string `yaml:"Network name,omitempty"` Config map[string]string `yaml:"Config,omitempty"` }{ @@ -333,6 +333,17 @@ func (c *cmdMigrate) runInteractive(server lxd.InstanceServer) (cmdMigrateData, return errors.New("Path does not exist") } + if config.InstanceArgs.Type == api.InstanceTypeVM && config.InstanceArgs.Source.Type == "migration" { + isImageTypeRaw, err := isImageTypeRaw(config.SourcePath) + if err != nil { + return err + } + + if !isImageTypeRaw { + return fmt.Errorf(`Source disk format cannot be converted by server. Source disk should be in raw format`) + } + } + _, err := os.Stat(s) if err != nil { return err @@ -586,7 +597,7 @@ func (c *cmdMigrate) run(cmd *cobra.Command, args []string) error { return err } - err = transferRootfs(ctx, server, op, fullPath, c.flagRsyncArgs, config.InstanceArgs.Type) + err = transferRootDiskForMigration(ctx, op, fullPath, c.flagRsyncArgs, config.InstanceArgs.Type) if err != nil { return err } @@ -677,13 +688,13 @@ func (c *cmdMigrate) askStorage(server lxd.InstanceServer, config *cmdMigrateDat "path": "/", } - changeStorageSize, err := c.global.asker.AskBool("Do you want to change the storage size? [default=no]: ", "no") + changeStorageSize, err := c.global.asker.AskBool("Do you want to change the storage volume size? [default=no]: ", "no") if err != nil { return err } if changeStorageSize { - size, err := c.global.asker.AskString("Please specify the storage size: ", "", func(s string) error { + size, err := c.global.asker.AskString("Please specify the storage volume size: ", "", func(s string) error { _, err := units.ParseByteSizeString(s) return err }) diff --git a/lxd-migrate/transfer.go b/lxd-migrate/transfer.go index 4a0dbc245e05..ff05a7672fb3 100644 --- a/lxd-migrate/transfer.go +++ b/lxd-migrate/transfer.go @@ -131,6 +131,28 @@ func rsyncSendSetup(ctx context.Context, path string, rsyncArgs string, instance return cmd, conn, stderr, nil } +func sendBlockVol(ctx context.Context, conn io.WriteCloser, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + go func() { + <-ctx.Done() + _ = conn.Close() + _ = f.Close() + }() + + _, err = io.Copy(conn, f) + if err != nil { + return err + } + + return conn.Close() +} + func protoSendError(ws *websocket.Conn, err error) { migration.ProtoSendControl(ws, err) diff --git a/lxd-migrate/utils.go b/lxd-migrate/utils.go index 21be44156d75..6c74ed4913cb 100644 --- a/lxd-migrate/utils.go +++ b/lxd-migrate/utils.go @@ -6,9 +6,9 @@ import ( "crypto/x509" "encoding/pem" "fmt" - "io" "net/url" "os" + "os/exec" "path/filepath" "reflect" "strings" @@ -24,7 +24,7 @@ import ( "github.com/canonical/lxd/shared/ws" ) -func transferRootfs(ctx context.Context, dst lxd.InstanceServer, op lxd.Operation, rootfs string, rsyncArgs string, instanceType api.InstanceType) error { +func transferRootDiskForMigration(ctx context.Context, op lxd.Operation, rootfs string, rsyncArgs string, instanceType api.InstanceType) error { opAPI := op.Get() // Connect to the websockets @@ -101,27 +101,7 @@ func transferRootfs(ctx context.Context, dst lxd.InstanceServer, op lxd.Operatio // Send block volume if instanceType == api.InstanceTypeVM { - f, err := os.Open(filepath.Join(rootfs, "root.img")) - if err != nil { - return abort(err) - } - - defer func() { _ = f.Close() }() - - conn := ws.NewWrapper(wsFs) - - go func() { - <-ctx.Done() - _ = conn.Close() - _ = f.Close() - }() - - _, err = io.Copy(conn, f) - if err != nil { - return abort(fmt.Errorf("Failed sending block volume: %w", err)) - } - - err = conn.Close() + err := sendBlockVol(ctx, ws.NewWrapper(wsFs), filepath.Join(rootfs, "root.img")) if err != nil { return abort(err) } @@ -352,3 +332,16 @@ func parseURL(URL string) (string, error) { return u.String(), nil } + +// isImageTypeRaw checks whether the file on a given path represents a disk, +// partition, or image in raw format. +func isImageTypeRaw(path string) (bool, error) { + cmd := exec.Command("file", "--brief", path) + out, err := cmd.Output() + if err != nil { + return false, fmt.Errorf("Failed to extract image file type: %v", err) + } + + isRaw := strings.HasPrefix(string(out), "DOS/MBR boot sector") || strings.HasPrefix(string(out), "block special") + return isRaw, nil +} diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index a5791f173dca..e5b21ae54fe3 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "os" + "slices" "github.com/canonical/lxd/client" "github.com/canonical/lxd/lxd/auth" @@ -17,6 +18,7 @@ import ( "github.com/canonical/lxd/lxd/config" "github.com/canonical/lxd/lxd/db" instanceDrivers "github.com/canonical/lxd/lxd/instance/drivers" + "github.com/canonical/lxd/lxd/instance/instancetype" "github.com/canonical/lxd/lxd/lifecycle" "github.com/canonical/lxd/lxd/node" "github.com/canonical/lxd/lxd/request" @@ -329,7 +331,18 @@ func api10Get(d *Daemon, r *http.Request) response.Response { } drivers := instanceDrivers.DriverStatuses() - for _, driver := range drivers { + + // Sort drivers map keys in order to produce consistent results. + driverKeys := make([]instancetype.Type, 0, len(drivers)) + for k := range drivers { + driverKeys = append(driverKeys, k) + } + + slices.Sort(driverKeys) + + for _, key := range driverKeys { + driver := drivers[key] + // Only report the supported drivers. if !driver.Supported { continue diff --git a/lxd/api_project.go b/lxd/api_project.go index 5e75f1ca5d41..693ec8954844 100644 --- a/lxd/api_project.go +++ b/lxd/api_project.go @@ -599,14 +599,14 @@ func projectPatch(d *Daemon, r *http.Request) response.Response { req.Description = project.Description } - config, err := reqRaw.GetMap("config") - if err != nil { - req.Config = project.Config - } else { - for k, v := range project.Config { - _, ok := config[k] - if !ok { - config[k] = v + // Perform config patch + req.Config = util.CopyConfig(project.Config) + patches, err := reqRaw.GetMap("config") + if err == nil { + for k, v := range patches { + strVal, ok := v.(string) + if ok { + req.Config[k] = strVal } } } diff --git a/lxd/apparmor/apparmor.go b/lxd/apparmor/apparmor.go index fac585699e21..34be5481a76c 100644 --- a/lxd/apparmor/apparmor.go +++ b/lxd/apparmor/apparmor.go @@ -185,6 +185,22 @@ func parserSupports(sysOS *sys.OS, feature string) (bool, error) { return ver.Compare(minVer) >= 0, nil } + if feature == "mount_nosymfollow" || feature == "userns_rule" { + sysOS.AppArmorFeatures.Lock() + defer sysOS.AppArmorFeatures.Unlock() + supported, ok := sysOS.AppArmorFeatures.Map[feature] + if !ok { + supported, err = FeatureCheck(sysOS, feature) + if err != nil { + return false, nil + } + + sysOS.AppArmorFeatures.Map[feature] = supported + } + + return supported, nil + } + return false, nil } diff --git a/lxd/apparmor/archive.go b/lxd/apparmor/archive.go index e4a691722382..ee9789293dd0 100644 --- a/lxd/apparmor/archive.go +++ b/lxd/apparmor/archive.go @@ -122,7 +122,7 @@ func archiveProfile(outputPath string, allowedCommandPaths []string) (string, er } // Render the profile. - var sb *strings.Builder = &strings.Builder{} + sb := &strings.Builder{} err = archiveProfileTpl.Execute(sb, map[string]any{ "name": ArchiveProfileName(outputPath), // Use non-deferenced outputPath for name. "outputPath": outputPathFull, // Use deferenced path in AppArmor profile. diff --git a/lxd/apparmor/feature_check.go b/lxd/apparmor/feature_check.go new file mode 100644 index 000000000000..1d0f3a08f9b2 --- /dev/null +++ b/lxd/apparmor/feature_check.go @@ -0,0 +1,79 @@ +package apparmor + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/google/uuid" + + "github.com/canonical/lxd/lxd/sys" + "github.com/canonical/lxd/shared" +) + +var featureCheckProfileTpl = template.Must(template.New("featureCheckProfile").Parse(` +profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) { + +{{- if eq .feature "mount_nosymfollow" }} + mount options=(nosymfollow) /, +{{- end }} + +{{- if eq .feature "userns_rule" }} + userns, +{{- end }} + +} +`)) + +// FeatureCheck tries to generate feature check profile and process it with apparmor_parser. +func FeatureCheck(sysOS *sys.OS, feature string) (bool, error) { + randomUUID := uuid.New().String() + name := fmt.Sprintf("<%s-%s>", randomUUID, feature) + profileName := profileName("featurecheck", name) + profilePath := filepath.Join(aaPath, "profiles", profileName) + content, err := os.ReadFile(profilePath) + if err != nil && !os.IsNotExist(err) { + return false, err + } + + updated, err := featureCheckProfile(profileName, feature) + if err != nil { + return false, err + } + + if string(content) != string(updated) { + err = os.WriteFile(profilePath, []byte(updated), 0600) + if err != nil { + return false, err + } + } + + defer func() { + _ = deleteProfile(sysOS, profileName, profileName) + }() + + err = parseProfile(sysOS, profileName) + if err != nil { + return false, nil + } + + return true, nil +} + +// featureCheckProfile generates the AppArmor profile. +func featureCheckProfile(profileName string, feature string) (string, error) { + // Render the profile. + sb := &strings.Builder{} + err := featureCheckProfileTpl.Execute(sb, map[string]any{ + "name": profileName, + "snap": shared.InSnap(), + "feature": feature, + }) + if err != nil { + return "", err + } + + return sb.String(), nil +} diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go index fbef81c2b3aa..2595ecc428bc 100644 --- a/lxd/apparmor/instance.go +++ b/lxd/apparmor/instance.go @@ -154,18 +154,30 @@ func instanceProfile(sysOS *sys.OS, inst instance) (string, error) { } // Render the profile. - var sb *strings.Builder = &strings.Builder{} + sb := &strings.Builder{} if inst.Type() == instancetype.Container { + mountNosymfollowSupported, err := parserSupports(sysOS, "mount_nosymfollow") + if err != nil { + return "", err + } + + usernsRuleSupported, err := parserSupports(sysOS, "userns_rule") + if err != nil { + return "", err + } + err = lxcProfileTpl.Execute(sb, map[string]any{ - "feature_cgns": sysOS.CGInfo.Namespacing, - "feature_cgroup2": sysOS.CGInfo.Layout == cgroup.CgroupsUnified || sysOS.CGInfo.Layout == cgroup.CgroupsHybrid, - "feature_stacking": sysOS.AppArmorStacking && !sysOS.AppArmorStacked, - "feature_unix": unixSupported, - "name": InstanceProfileName(inst), - "namespace": InstanceNamespaceName(inst), - "nesting": shared.IsTrue(inst.ExpandedConfig()["security.nesting"]), - "raw": rawContent, - "unprivileged": shared.IsFalseOrEmpty(inst.ExpandedConfig()["security.privileged"]) || sysOS.RunningInUserNS, + "feature_cgns": sysOS.CGInfo.Namespacing, + "feature_cgroup2": sysOS.CGInfo.Layout == cgroup.CgroupsUnified || sysOS.CGInfo.Layout == cgroup.CgroupsHybrid, + "feature_stacking": sysOS.AppArmorStacking && !sysOS.AppArmorStacked, + "feature_unix": unixSupported, + "feature_mount_nosymfollow": mountNosymfollowSupported, + "feature_userns_rule": usernsRuleSupported, + "name": InstanceProfileName(inst), + "namespace": InstanceNamespaceName(inst), + "nesting": shared.IsTrue(inst.ExpandedConfig()["security.nesting"]), + "raw": rawContent, + "unprivileged": shared.IsFalseOrEmpty(inst.ExpandedConfig()["security.privileged"]) || sysOS.RunningInUserNS, }) if err != nil { return "", err diff --git a/lxd/apparmor/instance_lxc.go b/lxd/apparmor/instance_lxc.go index a41fd7f09358..e61f306bf575 100644 --- a/lxd/apparmor/instance_lxc.go +++ b/lxd/apparmor/instance_lxc.go @@ -255,6 +255,26 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) { mount options=(ro,remount,bind,nosuid,noexec,strictatime) /sy[^s]*{,/**}, mount options=(ro,remount,bind,nosuid,noexec,strictatime) /sys?*{,/**}, +{{- if .feature_mount_nosymfollow }} + # see https://github.com/canonical/lxd/pull/12698 + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /[^spd]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /d[^e]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /de[^v]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev/.[^l]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev/.l[^x]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev/.lx[^c]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev/.lxc?*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev/[^.]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /dev?*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /p[^r]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /pr[^o]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /pro[^c]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /proc?*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /s[^y]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /sy[^s]*{,/**}, + mount options=(ro,remount,bind,nosuid,noexec,nodev,nosymfollow) /sys?*{,/**}, +{{- end }} + # Allow bind-mounts of anything except /proc, /sys and /dev/.lxc mount options=(rw,bind) /[^spd]*{,/**}, mount options=(rw,bind) /d[^e]*{,/**}, @@ -446,6 +466,11 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) { ### Configuration: nesting pivot_root, + # Allow user namespaces to be created +{{- if .feature_userns_rule }} + userns, +{{- end }} + # Allow sending signals and tracing children namespaces ptrace, signal, diff --git a/lxd/apparmor/rsync.go b/lxd/apparmor/rsync.go index a12a153a950f..346551dfee31 100644 --- a/lxd/apparmor/rsync.go +++ b/lxd/apparmor/rsync.go @@ -175,7 +175,7 @@ func rsyncProfile(sysOS *sys.OS, name string, sourcePath string, dstPath string) execPath = fullPath } - var sb *strings.Builder = &strings.Builder{} + sb := &strings.Builder{} err = rsyncProfileTpl.Execute(sb, map[string]any{ "name": name, "execPath": execPath, diff --git a/lxd/daemon.go b/lxd/daemon.go index 5d62a22d786b..4ecdac10bf4b 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -388,7 +388,7 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (trusted b } // Devlxd unix socket credentials on main API. - if r.RemoteAddr == "@devlxd" { + if r.RemoteAddr == devlxdRemoteAddress { return false, "", "", nil, fmt.Errorf("Main API query can't come from /dev/lxd socket") } diff --git a/lxd/db/openfga/openfga.go b/lxd/db/openfga/openfga.go index eeada05d3b01..5227a8cca03b 100644 --- a/lxd/db/openfga/openfga.go +++ b/lxd/db/openfga/openfga.go @@ -305,12 +305,18 @@ WHERE auth_groups_permissions.entitlement = ? AND auth_groups_permissions.entity // Observations: // // - This method appears to be called in four scenarios: +// // 1. Listing objects related to the server object via `server`. +// // 2. Listing objects related to project objects via `project`. +// // 3. Listing objects that a group is related to via an entitlement. +// // 4. Listing objects that an identity is related to via an entitlement. // -// - The UserFilter field of storage.ReadStartingWithUserFilter always has length 1. +// - The UserFilter field of storage.ReadStartingWithUserFilter usually has length 1. Sometimes there is another user +// when a type-bound public access is used (e.g. `identity:*`). We return early if this is the case, as we don't need +// to call the database. // // Implementation: // - For the first two cases we can perform a simple lookup for entities of the requested type (with project name if project relation). @@ -356,16 +362,30 @@ func (o *openfgaStore) ReadStartingWithUser(ctx context.Context, store string, f return nil, api.StatusErrorf(http.StatusBadRequest, "ReadStartingWithUser: Must provide relation") } - // Expect that there will be exactly one user filter. - if len(filter.UserFilter) != 1 { - return nil, fmt.Errorf("ReadStartingWithUser: Unexpected user filter list length") - } - - // Validate the entity type. entityType := entity.Type(filter.ObjectType) err := entityType.Validate() if err != nil { - return nil, fmt.Errorf("ReadStartingWithUser: Invalid entity type %q: %w", entityType, err) + return nil, fmt.Errorf("ReadUsersetTuples: Invalid object filter %q: %w", entityType, err) + } + + // Check for type-bound public access exception. + if entityType == entity.TypeServer && filter.Relation == string(auth.EntitlementCanView) { + return storage.NewStaticTupleIterator([]*openfgav1.Tuple{ + // Only returning one tuple here for the identity. When adding service accounts, we'll need + // to add another tuple to account for them. + { + Key: &openfgav1.TupleKey{ + Object: fmt.Sprintf("%s:%s", entity.TypeServer, entity.ServerURL().String()), + Relation: string(auth.EntitlementCanView), + User: fmt.Sprintf("%s:*", entity.TypeIdentity), + }, + }, + }), nil + } + + // Expect that there will be exactly one user filter when not dealing with a type-bound public access. + if len(filter.UserFilter) != 1 { + return nil, fmt.Errorf("ReadStartingWithUser: Unexpected user filter list length") } // Expect that the user filter object has an entity type and a URL. diff --git a/lxd/device/device_utils_disk.go b/lxd/device/device_utils_disk.go index 2b70ad05b995..c9bdd53712d7 100644 --- a/lxd/device/device_utils_disk.go +++ b/lxd/device/device_utils_disk.go @@ -21,6 +21,7 @@ import ( "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/osarch" "github.com/canonical/lxd/shared/revert" + "github.com/canonical/lxd/shared/version" ) // RBDFormatPrefix is the prefix used in disk paths to identify RBD. @@ -424,7 +425,7 @@ func DiskVMVirtfsProxyStop(pidPath string) error { // Returns UnsupportedError error if the host system or instance does not support virtiosfd, returns normal error // type if process cannot be started for other reasons. // Returns revert function and listener file handle on success. -func DiskVMVirtiofsdStart(execPath string, inst instance.Instance, socketPath string, pidPath string, logPath string, sharePath string, idmaps []idmap.IdmapEntry) (func(), net.Listener, error) { +func DiskVMVirtiofsdStart(kernelVersion version.DottedVersion, inst instance.Instance, socketPath string, pidPath string, logPath string, sharePath string, idmaps []idmap.IdmapEntry) (func(), net.Listener, error) { revert := revert.New() defer revert.Fail() @@ -502,7 +503,18 @@ func DiskVMVirtiofsdStart(execPath string, inst instance.Instance, socketPath st defer func() { _ = unixFile.Close() }() // Start the virtiofsd process in non-daemon mode. - args := []string{"--fd=3", "-o", fmt.Sprintf("source=%s", sharePath)} + args := []string{ + "--fd=3", + "-o", fmt.Sprintf("source=%s", sharePath), + } + + // Virtiofsd defaults to namespace sandbox mode which requires pidfd_open support. + // This was added in Linux 5.3, so if running an earlier kernel fallback to chroot sandbox mode. + minVer, _ := version.NewDottedVersion("5.3.0") + if kernelVersion.Compare(minVer) < 0 { + args = append(args, "--sandbox=chroot") + } + proc, err := subprocess.NewProcess(cmd, args, logPath, logPath) if err != nil { return nil, nil, err diff --git a/lxd/device/disk.go b/lxd/device/disk.go index 255d6ca0e156..da61a882acf4 100644 --- a/lxd/device/disk.go +++ b/lxd/device/disk.go @@ -851,15 +851,15 @@ func (d *disk) startContainer() (*deviceConfig.RunConfig, error) { // vmVirtfsProxyHelperPaths returns the path for PID file to use with virtfs-proxy-helper process. func (d *disk) vmVirtfsProxyHelperPaths() string { - pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", d.name)) + pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", filesystem.PathNameEncode(d.name))) return pidPath } // vmVirtiofsdPaths returns the path for the socket and PID file to use with virtiofsd process. func (d *disk) vmVirtiofsdPaths() (sockPath string, pidPath string) { - sockPath = filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("virtio-fs.%s.sock", d.name)) - pidPath = filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("virtio-fs.%s.pid", d.name)) + sockPath = filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("virtio-fs.%s.sock", filesystem.PathNameEncode(d.name))) + pidPath = filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("virtio-fs.%s.pid", filesystem.PathNameEncode(d.name))) return sockPath, pidPath } @@ -1115,10 +1115,10 @@ func (d *disk) startVM() (*deviceConfig.RunConfig, error) { // virtfs-proxy-helper 9p share. The 9p share will only be used as a fallback. err = func() error { sockPath, pidPath := d.vmVirtiofsdPaths() - logPath := filepath.Join(d.inst.LogPath(), fmt.Sprintf("disk.%s.log", d.name)) + logPath := filepath.Join(d.inst.LogPath(), fmt.Sprintf("disk.%s.log", filesystem.PathNameEncode(d.name))) _ = os.Remove(logPath) // Remove old log if needed. - revertFunc, unixListener, err := DiskVMVirtiofsdStart(d.state.OS.ExecPath, d.inst, sockPath, pidPath, logPath, mount.DevPath, rawIDMaps) + revertFunc, unixListener, err := DiskVMVirtiofsdStart(d.state.OS.KernelVersion, d.inst, sockPath, pidPath, logPath, mount.DevPath, rawIDMaps) if err != nil { var errUnsupported UnsupportedError if errors.As(err, &errUnsupported) { diff --git a/lxd/device/tpm.go b/lxd/device/tpm.go index 477a520cd753..b2e59f5fd1e9 100644 --- a/lxd/device/tpm.go +++ b/lxd/device/tpm.go @@ -13,6 +13,7 @@ import ( deviceConfig "github.com/canonical/lxd/lxd/device/config" "github.com/canonical/lxd/lxd/instance" "github.com/canonical/lxd/lxd/instance/instancetype" + "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/lxd/subprocess" "github.com/canonical/lxd/lxd/util" "github.com/canonical/lxd/shared" @@ -97,7 +98,7 @@ func (d *tpm) Start() (*deviceConfig.RunConfig, error) { return nil, fmt.Errorf("Failed to validate environment: %w", err) } - tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", d.name)) + tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", filesystem.PathNameEncode(d.name))) if !shared.PathExists(tpmDevPath) { err := os.Mkdir(tpmDevPath, 0700) @@ -114,8 +115,9 @@ func (d *tpm) Start() (*deviceConfig.RunConfig, error) { } func (d *tpm) startContainer() (*deviceConfig.RunConfig, error) { - tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", d.name)) - logFileName := fmt.Sprintf("tpm.%s.log", d.name) + escapedDeviceName := filesystem.PathNameEncode(d.name) + tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", escapedDeviceName)) + logFileName := fmt.Sprintf("tpm.%s.log", escapedDeviceName) logPath := filepath.Join(d.inst.LogPath(), logFileName) proc, err := subprocess.NewProcess("swtpm", []string{"chardev", "--tpm2", "--tpmstate", fmt.Sprintf("dir=%s", tpmDevPath), "--vtpm-proxy"}, logPath, "") @@ -134,7 +136,7 @@ func (d *tpm) startContainer() (*deviceConfig.RunConfig, error) { // Stop the TPM emulator if anything goes wrong. revert.Add(func() { _ = proc.Stop() }) - pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", d.name)) + pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", escapedDeviceName)) err = proc.Save(pidPath) if err != nil { @@ -212,8 +214,9 @@ func (d *tpm) startVM() (*deviceConfig.RunConfig, error) { revert := revert.New() defer revert.Fail() - tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", d.name)) - socketPath := filepath.Join(tpmDevPath, fmt.Sprintf("swtpm-%s.sock", d.name)) + escapedDeviceName := filesystem.PathNameEncode(d.name) + tpmDevPath := filepath.Join(d.inst.Path(), fmt.Sprintf("tpm.%s", escapedDeviceName)) + socketPath := filepath.Join(tpmDevPath, fmt.Sprintf("swtpm-%s.sock", escapedDeviceName)) runConf := deviceConfig.RunConfig{ TPMDevice: []deviceConfig.RunConfigItem{ {Key: "devName", Value: d.name}, @@ -280,7 +283,7 @@ func (d *tpm) startVM() (*deviceConfig.RunConfig, error) { revert.Add(func() { _ = proc.Stop() }) - pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", d.name)) + pidPath := filepath.Join(d.inst.DevicesPath(), fmt.Sprintf("%s.pid", escapedDeviceName)) err = proc.Save(pidPath) if err != nil { diff --git a/lxd/devices.go b/lxd/devices.go index 65a7340f7b6f..d6e50b3c473d 100644 --- a/lxd/devices.go +++ b/lxd/devices.go @@ -1,37 +1,5 @@ package main -/* -#ifndef _GNU_SOURCE -#define _GNU_SOURCE 1 -#endif -#include -#include - -#include "include/memory_utils.h" - -#ifndef HIDIOCGRAWINFO -#define HIDIOCGRAWINFO _IOR('H', 0x03, struct hidraw_devinfo) -struct hidraw_devinfo { - __u32 bustype; - __s16 vendor; - __s16 product; -}; -#endif - -static int get_hidraw_devinfo(int fd, struct hidraw_devinfo *info) -{ - int ret; - - ret = ioctl(fd, HIDIOCGRAWINFO, info); - if (ret) - return -1; - - return 0; -} - -*/ -import "C" - import ( "fmt" "os" @@ -40,14 +8,15 @@ import ( "sort" "strconv" "strings" + "unsafe" "golang.org/x/sys/unix" "github.com/canonical/lxd/lxd/cgroup" "github.com/canonical/lxd/lxd/device" - _ "github.com/canonical/lxd/lxd/include" // Used by cgo "github.com/canonical/lxd/lxd/instance" "github.com/canonical/lxd/lxd/instance/instancetype" + "github.com/canonical/lxd/lxd/linux" "github.com/canonical/lxd/lxd/resources" "github.com/canonical/lxd/lxd/state" "github.com/canonical/lxd/shared" @@ -710,10 +679,16 @@ func devicesRegister(instances []instance.Instance) { } func getHidrawDevInfo(fd int) (vendor string, product string, err error) { - info := C.struct_hidraw_devinfo{} - ret, err := C.get_hidraw_devinfo(C.int(fd), &info) - if ret != 0 { - return "", "", err + type hidInfo struct { + busType uint32 + vendor int16 + product int16 + } + + var info hidInfo + _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), linux.IoctlHIDIOCGrawInfo, uintptr(unsafe.Pointer(&info))) + if errno != 0 { + return "", "", fmt.Errorf("Failed setting received UUID: %w", unix.Errno(errno)) } return fmt.Sprintf("%04x", info.vendor), fmt.Sprintf("%04x", info.product), nil diff --git a/lxd/devlxd.go b/lxd/devlxd.go index 82902ea70997..5940d729cbb6 100644 --- a/lxd/devlxd.go +++ b/lxd/devlxd.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "net" "net/http" @@ -30,8 +31,12 @@ import ( "github.com/canonical/lxd/shared/ws" ) +const devlxdRemoteAddress = "@devlxd" + type hoistFunc func(f func(*Daemon, instance.Instance, http.ResponseWriter, *http.Request) response.Response, d *Daemon) func(http.ResponseWriter, *http.Request) +type devlxdHandlerFunc func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response + // DevLxdServer creates an http.Server capable of handling requests against the // /dev/lxd Unix socket endpoint created inside containers. func devLxdServer(d *Daemon) *http.Server { @@ -51,10 +56,15 @@ type devLxdHandler struct { * server side right now either, I went the simple route to avoid * needless noise. */ - f func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response + handlerFunc devlxdHandlerFunc +} + +var devlxdConfigGet = devLxdHandler{ + path: "/1.0/config", + handlerFunc: devlxdConfigGetHandler, } -var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdConfigGetHandler(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(c.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), c.Type() == instancetype.VM) } @@ -67,9 +77,14 @@ var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, c instance.In } return response.DevLxdResponse(http.StatusOK, filtered, "json", c.Type() == instancetype.VM) -}} +} + +var devlxdConfigKeyGet = devLxdHandler{ + path: "/1.0/config/{key}", + handlerFunc: devlxdConfigKeyGetHandler, +} -var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdConfigKeyGetHandler(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(c.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), c.Type() == instancetype.VM) } @@ -89,9 +104,14 @@ var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, c in } return response.DevLxdResponse(http.StatusOK, value, "raw", c.Type() == instancetype.VM) -}} +} + +var devlxdImageExport = devLxdHandler{ + path: "/1.0/images/{fingerprint}/export", + handlerFunc: devlxdImageExportHandler, +} -var devlxdImageExport = devLxdHandler{"/1.0/images/{fingerprint}/export", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdImageExportHandler(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(c.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), c.Type() == instancetype.VM) } @@ -101,7 +121,7 @@ var devlxdImageExport = devLxdHandler{"/1.0/images/{fingerprint}/export", func(d } // Use by security checks to distinguish devlxd vs lxd APIs - r.RemoteAddr = "@devlxd" + r.RemoteAddr = devlxdRemoteAddress resp := imageExport(d, r) @@ -111,9 +131,14 @@ var devlxdImageExport = devLxdHandler{"/1.0/images/{fingerprint}/export", func(d } return response.DevLxdResponse(http.StatusOK, "", "raw", c.Type() == instancetype.VM) -}} +} + +var devlxdMetadataGet = devLxdHandler{ + path: "/1.0/meta-data", + handlerFunc: devlxdMetadataGetHandler, +} -var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, inst instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdMetadataGetHandler(d *Daemon, inst instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(inst.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), inst.Type() == instancetype.VM) } @@ -121,9 +146,14 @@ var devlxdMetadataGet = devLxdHandler{"/1.0/meta-data", func(d *Daemon, inst ins value := inst.ExpandedConfig()["user.meta-data"] return response.DevLxdResponse(http.StatusOK, fmt.Sprintf("#cloud-config\ninstance-id: %s\nlocal-hostname: %s\n%s", inst.CloudInitID(), inst.Name(), value), "raw", inst.Type() == instancetype.VM) -}} +} + +var devlxdEventsGet = devLxdHandler{ + path: "/1.0/events", + handlerFunc: devlxdEventsGetHandler, +} -var devlxdEventsGet = devLxdHandler{"/1.0/events", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdEventsGetHandler(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(c.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), c.Type() == instancetype.VM) } @@ -178,9 +208,14 @@ var devlxdEventsGet = devLxdHandler{"/1.0/events", func(d *Daemon, c instance.In listener.Wait(r.Context()) return resp -}} +} + +var devlxdAPIHandler = devLxdHandler{ + path: "/1.0", + handlerFunc: devlxdAPIHandlerFunc, +} -var devlxdAPIHandler = devLxdHandler{"/1.0", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdAPIHandlerFunc(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { s := d.State() if r.Method == "GET" { @@ -236,10 +271,14 @@ var devlxdAPIHandler = devLxdHandler{"/1.0", func(d *Daemon, c instance.Instance } return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusMethodNotAllowed, fmt.Sprintf("method %q not allowed", r.Method)), c.Type() == instancetype.VM) +} -}} +var devlxdDevicesGet = devLxdHandler{ + path: "/1.0/devices", + handlerFunc: devlxdDevicesGetHandler, +} -var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { +func devlxdDevicesGetHandler(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { if shared.IsFalse(c.ExpandedConfig()["security.devlxd"]) { return response.DevLxdErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), c.Type() == instancetype.VM) } @@ -256,12 +295,15 @@ var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, c instance. } return response.DevLxdResponse(http.StatusOK, c.ExpandedDevices(), "json", c.Type() == instancetype.VM) -}} +} var handlers = []devLxdHandler{ - {"/", func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { - return response.DevLxdResponse(http.StatusOK, []string{"/1.0"}, "json", c.Type() == instancetype.VM) - }}, + { + path: "/", + handlerFunc: func(d *Daemon, c instance.Instance, w http.ResponseWriter, r *http.Request) response.Response { + return response.DevLxdResponse(http.StatusOK, []string{"/1.0"}, "json", c.Type() == instancetype.VM) + }, + }, devlxdAPIHandler, devlxdConfigGet, devlxdConfigKeyGet, @@ -276,7 +318,7 @@ func hoistReq(f func(*Daemon, instance.Instance, http.ResponseWriter, *http.Requ conn := ucred.GetConnFromContext(r.Context()) cred, ok := pidMapper.m[conn.(*net.UnixConn)] if !ok { - http.Error(w, pidNotInContainerErr.Error(), http.StatusInternalServerError) + http.Error(w, errPIDNotInContainer.Error(), http.StatusInternalServerError) return } @@ -312,7 +354,7 @@ func devLxdAPI(d *Daemon, f hoistFunc) http.Handler { m.UseEncodedPath() // Allow encoded values in path segments. for _, handler := range handlers { - m.HandleFunc(handler.path, f(handler.f, d)) + m.HandleFunc(handler.path, f(handler.handlerFunc, d)) } return m @@ -345,18 +387,27 @@ func devLxdAPI(d *Daemon, f hoistFunc) http.Handler { */ var pidMapper = ConnPidMapper{m: map[*net.UnixConn]*unix.Ucred{}} +// ConnPidMapper is threadsafe cache of unix connections to process IDs. We use this in hoistReq to determine +// the instance that the connection has been made from. type ConnPidMapper struct { m map[*net.UnixConn]*unix.Ucred mLock sync.Mutex } +// ConnStateHandler is used in the `ConnState` field of the devlxd http.Server so that we can cache the process ID of the +// caller when a new connection is made and delete it when the connection is closed. func (m *ConnPidMapper) ConnStateHandler(conn net.Conn, state http.ConnState) { - unixConn := conn.(*net.UnixConn) + unixConn, _ := conn.(*net.UnixConn) + if unixConn == nil { + logger.Error("Invalid type for devlxd connection", logger.Ctx{"conn_type": fmt.Sprintf("%T", conn)}) + return + } + switch state { case http.StateNew: cred, err := ucred.GetCred(unixConn) if err != nil { - logger.Debugf("Error getting ucred for conn %s", err) + logger.Debug("Error getting ucred for devlxd connection", logger.Ctx{"err": err}) } else { m.mLock.Lock() m.m[unixConn] = cred @@ -384,11 +435,11 @@ func (m *ConnPidMapper) ConnStateHandler(conn net.Conn, state http.ConnState) { delete(m.m, unixConn) m.mLock.Unlock() default: - logger.Debugf("Unknown state for connection %s", state) + logger.Debug("Unknown state for devlxd connection", logger.Ctx{"state": state.String()}) } } -var pidNotInContainerErr = fmt.Errorf("pid not in container?") +var errPIDNotInContainer = errors.New("Process ID not found in container") func findContainerForPid(pid int32, s *state.State) (instance.Container, error) { /* @@ -437,7 +488,9 @@ func findContainerForPid(pid int32, s *state.State) (instance.Container, error) return nil, fmt.Errorf("Instance is not container type") } - return inst.(instance.Container), nil + // Explicitly ignore type assertion check. We've just checked that it's a container. + c, _ := inst.(instance.Container) + return c, nil } status, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) @@ -490,9 +543,11 @@ func findContainerForPid(pid int32, s *state.State) (instance.Container, error) } if origPidNs == pidNs { - return inst.(instance.Container), nil + // Explicitly ignore type assertion check. The instance must be a container if we've found it via the process ID. + c, _ := inst.(instance.Container) + return c, nil } } - return nil, pidNotInContainerErr + return nil, errPIDNotInContainer } diff --git a/lxd/devlxd_test.go b/lxd/devlxd_test.go index 1ad667c4ac8a..6c21bae7c03f 100644 --- a/lxd/devlxd_test.go +++ b/lxd/devlxd_test.go @@ -169,7 +169,7 @@ func TestHttpRequest(t *testing.T) { t.Fatal(err) } - if !strings.Contains(string(resp), pidNotInContainerErr.Error()) { + if !strings.Contains(string(resp), errPIDNotInContainer.Error()) { t.Fatal("resp error not expected: ", string(resp)) } } diff --git a/lxd/firewall/drivers/drivers_nftables.go b/lxd/firewall/drivers/drivers_nftables.go index df1e14ec32ff..86d4ff5afccf 100644 --- a/lxd/firewall/drivers/drivers_nftables.go +++ b/lxd/firewall/drivers/drivers_nftables.go @@ -265,23 +265,15 @@ func (d Nftables) networkSetupOutboundNAT(networkName string, SNATV4 *SNATOpts, } // networkSetupICMPDHCPDNSAccess sets up basic nftables overrides for ICMP, DHCP and DNS. -func (d Nftables) networkSetupICMPDHCPDNSAccess(networkName string, ipVersions []uint) error { - ipFamilies := []string{} - for _, ipVersion := range ipVersions { - switch ipVersion { - case 4: - ipFamilies = append(ipFamilies, "ip") - case 6: - ipFamilies = append(ipFamilies, "ip6") - } - } - +// This should be called with at least one of (ip4Address, ip6Address) != nil. +func (d Nftables) networkSetupICMPDHCPDNSAccess(networkName string, ip4Address net.IP, ip6Address net.IP) error { tplFields := map[string]any{ "namespace": nftablesNamespace, "chainSeparator": nftablesChainSeparator, "networkName": networkName, + "ip4Address": ip4Address.String(), + "ip6Address": ip6Address.String(), "family": "inet", - "ipFamilies": ipFamilies, } err := d.applyNftConfig(nftablesNetICMPDHCPDNS, tplFields) @@ -315,7 +307,7 @@ func (d Nftables) networkSetupACLChainAndJumpRules(networkName string) error { } // NetworkSetup configure network firewall. -func (d Nftables) NetworkSetup(networkName string, opts Opts) error { +func (d Nftables) NetworkSetup(networkName string, ip4Address net.IP, ip6Address net.IP, opts Opts) error { // Do this first before adding other network rules, so jump to ACL rules come first. if opts.ACL { err := d.networkSetupACLChainAndJumpRules(networkName) @@ -331,21 +323,20 @@ func (d Nftables) NetworkSetup(networkName string, opts Opts) error { } } - dhcpDNSAccess := []uint{} var ip4ForwardingAllow, ip6ForwardingAllow *bool if opts.FeaturesV4 != nil || opts.FeaturesV6 != nil { if opts.FeaturesV4 != nil { - if opts.FeaturesV4.ICMPDHCPDNSAccess { - dhcpDNSAccess = append(dhcpDNSAccess, 4) + if !opts.FeaturesV4.ICMPDHCPDNSAccess { + ip4Address = nil } ip4ForwardingAllow = &opts.FeaturesV4.ForwardingAllow } if opts.FeaturesV6 != nil { - if opts.FeaturesV6.ICMPDHCPDNSAccess { - dhcpDNSAccess = append(dhcpDNSAccess, 6) + if !opts.FeaturesV6.ICMPDHCPDNSAccess { + ip6Address = nil } ip6ForwardingAllow = &opts.FeaturesV6.ForwardingAllow @@ -356,7 +347,7 @@ func (d Nftables) NetworkSetup(networkName string, opts Opts) error { return err } - err = d.networkSetupICMPDHCPDNSAccess(networkName, dhcpDNSAccess) + err = d.networkSetupICMPDHCPDNSAccess(networkName, ip4Address, ip6Address) if err != nil { return err } diff --git a/lxd/firewall/drivers/drivers_nftables_templates.go b/lxd/firewall/drivers/drivers_nftables_templates.go index b08577070e5e..be76d4366ff1 100644 --- a/lxd/firewall/drivers/drivers_nftables_templates.go +++ b/lxd/firewall/drivers/drivers_nftables_templates.go @@ -46,16 +46,24 @@ chain in{{.chainSeparator}}{{.networkName}} { iifname "{{.networkName}}" tcp dport 53 accept iifname "{{.networkName}}" udp dport 53 accept + iifname "lo" tcp dport 53 accept + iifname "lo" udp dport 53 accept + + {{if ne .ip4Address "" -}} + ip daddr == "{{.ip4Address}}" tcp dport 53 drop + ip daddr == "{{.ip4Address}}" udp dport 53 drop - {{- range .ipFamilies}} - {{if eq . "ip" -}} iifname "{{$.networkName}}" icmp type {3, 11, 12} accept iifname "{{$.networkName}}" udp dport 67 accept - {{else -}} + {{- end}} + + {{if ne .ip6Address "" -}} + ip6 daddr == "{{.ip6Address}}" tcp dport 53 drop + ip6 daddr == "{{.ip6Address}}" udp dport 53 drop + iifname "{{$.networkName}}" icmpv6 type {1, 2, 3, 4, 133, 135, 136, 143} accept iifname "{{$.networkName}}" udp dport 547 accept {{- end}} - {{- end}} } chain out{{.chainSeparator}}{{.networkName}} { diff --git a/lxd/firewall/drivers/drivers_xtables.go b/lxd/firewall/drivers/drivers_xtables.go index 2c063b2808e7..e83552dfb2c6 100644 --- a/lxd/firewall/drivers/drivers_xtables.go +++ b/lxd/firewall/drivers/drivers_xtables.go @@ -379,11 +379,17 @@ func (d Xtables) networkSetupOutboundNAT(networkName string, subnet *net.IPNet, } // networkSetupICMPDHCPDNSAccess sets up basic iptables overrides for ICMP, DHCP and DNS. -func (d Xtables) networkSetupICMPDHCPDNSAccess(networkName string, ipVersion uint) error { +func (d Xtables) networkSetupICMPDHCPDNSAccess(networkName string, networkAddress net.IP, ipVersion uint) error { var rules [][]string if ipVersion == 4 { rules = [][]string{ {"4", networkName, "filter", "INPUT", "-i", networkName, "-p", "udp", "--dport", "67", "-j", "ACCEPT"}, + // Prevent DNS requests to the bridge's dnsmasq except from lo and the bridge + // `rules` is reversed when applied (iptablesPrepend(...)), so the drop rules come first + {"4", networkName, "filter", "INPUT", "-d", networkAddress.String(), "-p", "udp", "--dport", "53", "-j", "DROP"}, + {"4", networkName, "filter", "INPUT", "-d", networkAddress.String(), "-p", "tcp", "--dport", "53", "-j", "DROP"}, + {"4", networkName, "filter", "INPUT", "-i", "lo", "-p", "udp", "--dport", "53", "-j", "ACCEPT"}, + {"4", networkName, "filter", "INPUT", "-i", "lo", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"}, {"4", networkName, "filter", "INPUT", "-i", networkName, "-p", "udp", "--dport", "53", "-j", "ACCEPT"}, {"4", networkName, "filter", "INPUT", "-i", networkName, "-p", "tcp", "--dport", "53", "-j", "ACCEPT"}, {"4", networkName, "filter", "OUTPUT", "-o", networkName, "-p", "udp", "--sport", "67", "-j", "ACCEPT"}, @@ -398,6 +404,10 @@ func (d Xtables) networkSetupICMPDHCPDNSAccess(networkName string, ipVersion uin } else if ipVersion == 6 { rules = [][]string{ {"6", networkName, "filter", "INPUT", "-i", networkName, "-p", "udp", "--dport", "547", "-j", "ACCEPT"}, + {"6", networkName, "filter", "INPUT", "-d", networkAddress.String(), "-p", "udp", "--dport", "53", "-j", "DROP"}, + {"6", networkName, "filter", "INPUT", "-d", networkAddress.String(), "-p", "tcp", "--dport", "53", "-j", "DROP"}, + {"6", networkName, "filter", "INPUT", "-i", "lo", "-p", "udp", "--dport", "53", "-j", "ACCEPT"}, + {"6", networkName, "filter", "INPUT", "-i", "lo", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"}, {"6", networkName, "filter", "INPUT", "-i", networkName, "-p", "udp", "--dport", "53", "-j", "ACCEPT"}, {"6", networkName, "filter", "INPUT", "-i", networkName, "-p", "tcp", "--dport", "53", "-j", "ACCEPT"}, {"6", networkName, "filter", "OUTPUT", "-o", networkName, "-p", "udp", "--sport", "547", "-j", "ACCEPT"}, @@ -441,7 +451,7 @@ func (d Xtables) networkSetupDHCPv4Checksum(networkName string) error { } // NetworkSetup configure network firewall. -func (d Xtables) NetworkSetup(networkName string, opts Opts) error { +func (d Xtables) NetworkSetup(networkName string, ipv4Address net.IP, ipv6Address net.IP, opts Opts) error { if opts.SNATV4 != nil { err := d.networkSetupOutboundNAT(networkName, opts.SNATV4.Subnet, opts.SNATV4.SNATAddress, opts.SNATV4.Append) if err != nil { @@ -458,7 +468,7 @@ func (d Xtables) NetworkSetup(networkName string, opts Opts) error { if opts.FeaturesV4 != nil { if opts.FeaturesV4.ICMPDHCPDNSAccess { - err := d.networkSetupICMPDHCPDNSAccess(networkName, 4) + err := d.networkSetupICMPDHCPDNSAccess(networkName, ipv4Address, 4) if err != nil { return err } @@ -477,7 +487,7 @@ func (d Xtables) NetworkSetup(networkName string, opts Opts) error { if opts.FeaturesV6 != nil { if opts.FeaturesV6.ICMPDHCPDNSAccess { - err := d.networkSetupICMPDHCPDNSAccess(networkName, 6) + err := d.networkSetupICMPDHCPDNSAccess(networkName, ipv6Address, 6) if err != nil { return err } diff --git a/lxd/firewall/firewall_interface.go b/lxd/firewall/firewall_interface.go index 0510ccfb9cbf..f850ab429a54 100644 --- a/lxd/firewall/firewall_interface.go +++ b/lxd/firewall/firewall_interface.go @@ -11,7 +11,7 @@ type Firewall interface { String() string Compat() (bool, error) - NetworkSetup(networkName string, opts drivers.Opts) error + NetworkSetup(networkName string, ip4Address net.IP, ip6Address net.IP, opts drivers.Opts) error NetworkClear(networkName string, delete bool, ipVersions []uint) error NetworkApplyACLRules(networkName string, rules []drivers.ACLRule) error NetworkApplyForwards(networkName string, rules []drivers.AddressForward) error diff --git a/lxd/images.go b/lxd/images.go index ff35a2696095..341ae033a6d6 100644 --- a/lxd/images.go +++ b/lxd/images.go @@ -973,6 +973,7 @@ func imagesPost(d *Daemon, r *http.Request) response.Response { projectName := request.ProjectParam(r) + // If the client is not authenticated, CheckPermission will return a http.StatusForbidden api.StatusError. var userCanCreateImages bool err := s.Authorizer.CheckPermission(r.Context(), entity.ProjectURL(projectName), auth.EntitlementCanCreateImages) if err != nil && !auth.IsDeniedError(err) { @@ -1631,14 +1632,12 @@ func imagesGet(d *Daemon, r *http.Request) response.Response { request.SetCtxValue(r, request.CtxEffectiveProjectName, effectiveProjectName) - // Check if the caller is authenticated via the request context. - trusted, err := request.GetCtxValue[bool](r.Context(), request.CtxTrusted) - if err != nil { - return response.InternalError(fmt.Errorf("Failed getting authentication status: %w", err)) - } + // If the caller is not trusted, we only want to list public images. + publicOnly := !auth.IsTrusted(r.Context()) // Get a permission checker. If the caller is not authenticated, the permission checker will deny all. - // However, the permission checker will not be called for public images. + // However, the permission checker is only called when an image is private. Both trusted and untrusted clients will + // still see public images. canViewImage, err := s.Authorizer.GetPermissionChecker(r.Context(), auth.EntitlementCanView, entity.TypeImage) if err != nil { return response.SmartError(err) @@ -1651,7 +1650,7 @@ func imagesGet(d *Daemon, r *http.Request) response.Response { var result any err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, !trusted, clauses, canViewImage) + result, err = doImagesGet(ctx, tx, util.IsRecursionRequest(r), projectName, publicOnly, clauses, canViewImage) if err != nil { return err } @@ -2993,37 +2992,65 @@ func imageGet(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Get the image (expand partial fingerprints). + trusted := auth.IsTrusted(r.Context()) + secret := r.FormValue("secret") + + // Unauthenticated clients that do not provide a secret may only view public images. + publicOnly := !trusted && secret == "" + + // Get the image. We need to do this before the permission check because the URL in the permission check will not + // work with partial fingerprints. var info *api.Image err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { - info, err = doImageGet(ctx, tx, projectName, fingerprint, false) + info, err = doImageGet(ctx, tx, projectName, fingerprint, publicOnly) if err != nil { return err } return nil }) - if err != nil { + if err != nil && api.StatusErrorCheck(err, http.StatusNotFound) { + // Return a generic not found. This is so that the caller cannot determine the existence of an image by the + // contents of the error message. + return response.NotFound(nil) + } else if err != nil { return response.SmartError(err) } + // Access check. var userCanViewImage bool - err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, info.Fingerprint), auth.EntitlementCanView) - if err != nil && !auth.IsDeniedError(err) { - return response.SmartError(err) - } else if err == nil { - userCanViewImage = true - } + if secret != "" { + // If a secret was provided, validate it regardless of whether the image is public or the caller has sufficient + // privilege. This is to ensure the image token operation is cancelled. + op, err := imageValidSecret(s, r, projectName, info.Fingerprint, secret) + if err != nil { + return response.SmartError(err) + } - secret := r.FormValue("secret") + // If an operation was found the caller has access, otherwise continue to other access checks. + if op != nil { + userCanViewImage = true + } + } - op, err := imageValidSecret(s, r, projectName, info.Fingerprint, secret) - if err != nil { - return response.SmartError(err) + // No operation found for the secret. Perform other access checks. + if !userCanViewImage { + if info.Public { + // If the image is public any client can view it. + userCanViewImage = true + } else { + // Otherwise perform an access check with the full image fingerprint. + err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, info.Fingerprint), auth.EntitlementCanView) + if err != nil && !auth.IsDeniedError(err) { + return response.SmartError(err) + } else if err == nil { + userCanViewImage = true + } + } } - // If the caller does not have permission to view the image and the secret was invalid, return generic not found. - if !info.Public && !userCanViewImage && op == nil { + // If the client still cannot view the image, return a generic not found error. + if !userCanViewImage { return response.NotFound(nil) } @@ -3597,6 +3624,8 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response { s := d.State() + // Set `userCanViewImageAlias` to true only when the caller is authenticated and can view the alias. + // We don't abort the request if this is false because the image alias may be for a public image. var userCanViewImageAlias bool err = s.Authorizer.CheckPermission(r.Context(), entity.ImageAliasURL(projectName, name), auth.EntitlementCanView) if err != nil && !auth.IsDeniedError(err) { @@ -3607,12 +3636,16 @@ func imageAliasGet(d *Daemon, r *http.Request) response.Response { var alias api.ImageAliasesEntry err = d.State().DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + // If `userCanViewImageAlias` is false, the query will be restricted to public images only. _, alias, err = tx.GetImageAlias(ctx, projectName, name, userCanViewImageAlias) return err }) - if err != nil { + if err != nil && !api.StatusErrorCheck(err, http.StatusNotFound) { return response.SmartError(err) + } else if err != nil { + // Return a generic not found error. + return response.NotFound(nil) } return response.SyncResponseETag(true, alias, alias) @@ -4010,43 +4043,82 @@ func imageExport(d *Daemon, r *http.Request) response.Response { return response.SmartError(err) } - // Get the image (expand the fingerprint). + isDevLXDQuery := r.RemoteAddr == devlxdRemoteAddress + secret := r.FormValue("secret") + trusted := auth.IsTrusted(r.Context()) + + // Unauthenticated remote clients that do not provide a secret may only view public images. + // For devlxd, we allow querying for private images. We'll subsequently perform additional access checks. + publicOnly := !trusted && secret == "" && !isDevLXDQuery + + // Get the image. We need to do this before the permission check because the URL in the permission check will not + // work with partial fingerprints. var imgInfo *api.Image - err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { - // Get the image (expand the fingerprint). - _, imgInfo, err = tx.GetImage(ctx, fingerprint, dbCluster.ImageFilter{Project: &projectName}) + err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { + filter := dbCluster.ImageFilter{Project: &projectName} + if publicOnly { + filter.Public = &publicOnly + } + _, imgInfo, err = tx.GetImage(ctx, fingerprint, filter) return err }) - if err != nil { + if err != nil && api.StatusErrorCheck(err, http.StatusNotFound) { + // Return a generic not found. This is so that the caller cannot determine the existence of an image by the + // contents of the error message. + return response.NotFound(nil) + } else if err != nil { return response.SmartError(err) } // Access control. var userCanViewImage bool - err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, imgInfo.Fingerprint), auth.EntitlementCanView) - if err != nil && !auth.IsDeniedError(err) { - return response.SmartError(err) - } else if err == nil { - userCanViewImage = true - } - - secret := r.FormValue("secret") - - if r.RemoteAddr == "@devlxd" { - if !imgInfo.Public && !imgInfo.Cached { - return response.NotFound(fmt.Errorf("Image %q not found", fingerprint)) - } - } else { + if secret != "" { + // If a secret was provided, validate it regardless of whether the image is public or the caller has sufficient + // privilege. This is to ensure the image token operation is cancelled. op, err := imageValidSecret(s, r, projectName, imgInfo.Fingerprint, secret) if err != nil { return response.SmartError(err) } - // If the image is not public and the caller cannot view it, return a generic not found error. - if !imgInfo.Public && !userCanViewImage && op == nil { + // If an operation was found the caller has access, otherwise continue to other access checks. + if op != nil { + userCanViewImage = true + } + } + + if isDevLXDQuery { + // A devlxd query must contain the full fingerprint of the image (no partials). + if fingerprint != imgInfo.Fingerprint { return response.NotFound(nil) } + + // A devlxd query must be for a public or cached image. + if !(imgInfo.Public || imgInfo.Cached) { + return response.NotFound(nil) + } + + userCanViewImage = true + } + + if !userCanViewImage { + if imgInfo.Public { + // If the image is public any client can view it. + userCanViewImage = true + } else { + // Otherwise perform an access check with the full image fingerprint. + err = s.Authorizer.CheckPermission(r.Context(), entity.ImageURL(projectName, imgInfo.Fingerprint), auth.EntitlementCanView) + if err != nil && !auth.IsDeniedError(err) { + return response.SmartError(err) + } else if err == nil { + userCanViewImage = true + } + } + } + + // If the client still cannot view the image, return a generic not found error. + if !userCanViewImage { + return response.NotFound(nil) } var address string @@ -4106,6 +4178,9 @@ func imageExport(d *Daemon, r *http.Request) response.Response { files[1].Path = rootfsPath files[1].Filename = filename + requestor := request.CreateRequestor(r) + s.Events.SendLifecycle(projectName, lifecycle.ImageRetrieved.Event(imgInfo.Fingerprint, projectName, requestor, nil)) + return response.FileResponse(r, files, nil) } diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index e6e2a10c0e13..ca56d180fc96 100644 --- a/lxd/instance/drivers/driver_qemu.go +++ b/lxd/instance/drivers/driver_qemu.go @@ -1,27 +1,13 @@ package drivers -/* - -#include -#include -#include - -#define VHOST_VIRTIO 0xAF -#define VHOST_VSOCK_SET_GUEST_CID _IOW(VHOST_VIRTIO, 0x60, __u64) - -*/ -import "C" - import ( "bufio" "bytes" "compress/gzip" "context" - "crypto/sha256" "crypto/tls" "crypto/x509" "database/sql" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -67,6 +53,7 @@ import ( "github.com/canonical/lxd/lxd/instance/operationlock" "github.com/canonical/lxd/lxd/instancewriter" "github.com/canonical/lxd/lxd/lifecycle" + "github.com/canonical/lxd/lxd/linux" "github.com/canonical/lxd/lxd/metrics" "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/network" @@ -109,7 +96,7 @@ const qemuPCIDeviceIDStart = 4 const qemuDeviceIDPrefix = "dev-lxd_" // qemuDeviceNameMaxLength used to indicate the maximum length of a qemu device ID. -const qemuDeviceIDMaxLength = 64 +const qemuDeviceIDMaxLength = 63 // qemuDeviceNamePrefix used as part of the name given QEMU blockdevs, netdevs and device tags generated from user added devices. const qemuDeviceNamePrefix = "lxd_" @@ -1401,7 +1388,7 @@ func (d *qemu) start(stateful bool, op *operationlock.InstanceOperation) error { // This is used by the lxd-agent in preference to 9p (due to its improved performance) and in scenarios // where 9p isn't available in the VM guest OS. configSockPath, configPIDPath := d.configVirtiofsdPaths() - revertFunc, unixListener, err := device.DiskVMVirtiofsdStart(d.state.OS.ExecPath, d, configSockPath, configPIDPath, "", configMntPath, nil) + revertFunc, unixListener, err := device.DiskVMVirtiofsdStart(d.state.OS.KernelVersion, d, configSockPath, configPIDPath, "", configMntPath, nil) if err != nil { var errUnsupported device.UnsupportedError if !errors.As(err, &errUnsupported) { @@ -2183,12 +2170,11 @@ func (d *qemu) deviceStart(dev device.Device, instanceRunning bool) (*deviceConf } func (d *qemu) deviceAttachPath(deviceName string) (mountTag string, err error) { - escapedDeviceName := filesystem.PathNameEncode(deviceName) - deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName) - mountTag = d.generateQemuDeviceName(deviceName) + deviceID := qemuDeviceNameOrID(qemuDeviceIDPrefix, deviceName, "-virtio-fs", qemuDeviceIDMaxLength) + mountTag = qemuDeviceNameOrID(qemuDeviceNamePrefix, deviceName, "", qemuDeviceNameMaxLength) // Detect virtiofsd path. - virtiofsdSockPath := filepath.Join(d.DevicesPath(), fmt.Sprintf("virtio-fs.%s.sock", deviceName)) + virtiofsdSockPath := filepath.Join(d.DevicesPath(), fmt.Sprintf("virtio-fs.%s.sock", filesystem.PathNameEncode(deviceName))) if !shared.PathExists(virtiofsdSockPath) { return "", fmt.Errorf("Virtiofsd isn't running") } @@ -2235,7 +2221,7 @@ func (d *qemu) deviceAttachPath(deviceName string) (mountTag string, err error) reverter.Add(func() { _ = monitor.CloseFile(virtiofsdSockPath) }) err = monitor.AddCharDevice(map[string]any{ - "id": mountTag, + "id": deviceID, "backend": map[string]any{ "type": "socket", "data": map[string]any{ @@ -2253,7 +2239,7 @@ func (d *qemu) deviceAttachPath(deviceName string) (mountTag string, err error) return "", fmt.Errorf("Failed to add the character device: %w", err) } - reverter.Add(func() { _ = monitor.RemoveCharDevice(mountTag) }) + reverter.Add(func() { _ = monitor.RemoveCharDevice(deviceID) }) // Figure out a hotplug slot. pciDevID := qemuPCIDeviceIDStart @@ -2277,7 +2263,7 @@ func (d *qemu) deviceAttachPath(deviceName string) (mountTag string, err error) "bus": pciDeviceName, "addr": "00.0", "tag": mountTag, - "chardev": mountTag, + "chardev": deviceID, "id": deviceID, } @@ -2311,9 +2297,7 @@ func (d *qemu) deviceAttachBlockDevice(mount deviceConfig.MountEntryItem) error } func (d *qemu) deviceDetachPath(deviceName string) error { - escapedDeviceName := filesystem.PathNameEncode(deviceName) - deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName) - mountTag := d.generateQemuDeviceName(deviceName) + deviceID := qemuDeviceNameOrID(qemuDeviceIDPrefix, deviceName, "-virtio-fs", qemuDeviceIDMaxLength) // Check if the agent is running. monitor, err := qmp.Connect(d.monitorPath(), qemuSerialChardevName, d.getMonitorEventHandler()) @@ -2329,7 +2313,7 @@ func (d *qemu) deviceDetachPath(deviceName string) error { waitDuration := time.Duration(time.Second * time.Duration(10)) waitUntil := time.Now().Add(waitDuration) for { - err = monitor.RemoveCharDevice(mountTag) + err = monitor.RemoveCharDevice(deviceID) if err == nil { break } @@ -2354,9 +2338,8 @@ func (d *qemu) deviceDetachBlockDevice(deviceName string) error { return err } - escapedDeviceName := filesystem.PathNameEncode(deviceName) - deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName) - blockDevName := d.generateQemuDeviceName(escapedDeviceName) + deviceID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, filesystem.PathNameEncode(deviceName)) + blockDevName := qemuDeviceNameOrID(qemuDeviceNamePrefix, deviceName, "", qemuDeviceNameMaxLength) err = monitor.RemoveFDFromFDSet(blockDevName) if err != nil { @@ -3734,7 +3717,7 @@ func (d *qemu) addRootDriveConfig(qemuDev map[string]string, mountInfo *storageP // addDriveDirConfig adds the qemu config required for adding a supplementary drive directory share. func (d *qemu) addDriveDirConfig(cfg *[]cfgSection, bus *qemuBus, fdFiles *[]*os.File, agentMounts *[]instancetype.VMAgentMount, driveConf deviceConfig.MountEntryItem) error { - mountTag := d.generateQemuDeviceName(driveConf.DevName) + mountTag := qemuDeviceNameOrID(qemuDeviceNamePrefix, driveConf.DevName, "", qemuDeviceNameMaxLength) agentMount := instancetype.VMAgentMount{ Source: mountTag, @@ -3957,8 +3940,6 @@ func (d *qemu) addDriveConfig(qemuDev map[string]string, bootIndexes map[string] directCache = false } - escapedDeviceName := filesystem.PathNameEncode(driveConf.DevName) - blockDev := map[string]any{ "aio": aioMode, "cache": map[string]any{ @@ -3967,7 +3948,7 @@ func (d *qemu) addDriveConfig(qemuDev map[string]string, bootIndexes map[string] }, "discard": "unmap", // Forward as an unmap request. This is the same as `discard=on` in the qemu config file. "driver": "file", - "node-name": d.generateQemuDeviceName(escapedDeviceName), + "node-name": qemuDeviceNameOrID(qemuDeviceNamePrefix, driveConf.DevName, "", qemuDeviceNameMaxLength), "read-only": false, } @@ -4075,6 +4056,8 @@ func (d *qemu) addDriveConfig(qemuDev map[string]string, bootIndexes map[string] qemuDev = map[string]string{} } + escapedDeviceName := filesystem.PathNameEncode(driveConf.DevName) + qemuDev["id"] = fmt.Sprintf("%s%s", qemuDeviceIDPrefix, escapedDeviceName) qemuDevDrive, ok := blockDev["node-name"].(string) if !ok { @@ -8093,7 +8076,8 @@ func (d *qemu) acquireVsockID(vsockID uint32) (*os.File, error) { // The vsock Context ID cannot be supplied as type uint32. vsockIDInt := uint64(vsockID) - _, _, errno := unix.Syscall(unix.SYS_IOCTL, vsockF.Fd(), C.VHOST_VSOCK_SET_GUEST_CID, uintptr(unsafe.Pointer(&vsockIDInt))) + // Call the ioctl to set the context ID. + _, _, errno := unix.Syscall(unix.SYS_IOCTL, vsockF.Fd(), linux.IoctlVhostVsockSetGuestCid, uintptr(unsafe.Pointer(&vsockIDInt))) if errno != 0 { if !errors.Is(errno, unix.EADDRINUSE) { return nil, fmt.Errorf("Failed ioctl syscall to vhost socket: %q", errno.Error()) @@ -8674,7 +8658,7 @@ func (d *qemu) checkFeatures(hostArch int, qemuPath string) (map[string]any, err // Check io_uring feature. blockDev := map[string]any{ - "node-name": d.generateQemuDeviceName("feature-check"), + "node-name": fmt.Sprintf("%s%s", qemuDeviceNamePrefix, "feature-check"), "driver": "file", "filename": blockDevPath.Name(), "aio": "io_uring", @@ -8895,36 +8879,6 @@ func (d *qemu) deviceDetachUSB(usbDev deviceConfig.USBDeviceItem) error { return nil } -// hashIfLonger returns a full or partial hash of a name as to fit it within a size limit. -func hashIfLonger(name string, maxLength int) string { - if len(name) <= maxLength { - return name - } - - // If the name is too long, hash it as SHA-256 (32 bytes). - // Then encode the SHA-256 binary hash as Base64 Raw URL format and trim down if needed. - hash := sha256.New() - hash.Write([]byte(name)) - binaryHash := hash.Sum(nil) - - // Raw URL avoids the use of "+" character and the padding "=" character which QEMU doesn't allow. - hashedName := base64.RawURLEncoding.EncodeToString(binaryHash) - if len(hashedName) > maxLength { - hashedName = hashedName[0:maxLength] - } - - return hashedName -} - -// Block node names and device tags may only be up to 31 characters long, so use a hash if longer. -func (d *qemu) generateQemuDeviceName(name string) string { - maxNameLength := qemuDeviceNameMaxLength - len(qemuDeviceNamePrefix) - name = hashIfLonger(name, maxNameLength) - - // Apply the lxd_ prefix. - return fmt.Sprintf("%s%s", qemuDeviceNamePrefix, name) -} - func (d *qemu) setCPUs(count int) error { if count == 0 { return nil diff --git a/lxd/instance/drivers/driver_qemu_config_test.go b/lxd/instance/drivers/driver_qemu_config_test.go index 309909bf5b7c..2811ae893c50 100644 --- a/lxd/instance/drivers/driver_qemu_config_test.go +++ b/lxd/instance/drivers/driver_qemu_config_test.go @@ -754,7 +754,7 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "9p", }, `# Config drive (9p) - [fsdev "qemu_config"] + [fsdev "dev-qemu_config-drive-9p"] fsdriver = "local" security_model = "none" readonly = "on" @@ -765,7 +765,7 @@ func TestQemuConfigTemplates(t *testing.T) { bus = "qemu_pcie0" addr = "00.5" mount_tag = "config" - fsdev = "qemu_config"`, + fsdev = "dev-qemu_config-drive-9p"`, }, { qemuDriveConfigOpts{ dev: qemuDevOpts{"pcie", "qemu_pcie1", "10.2", true}, @@ -773,7 +773,7 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "virtio-fs", }, `# Config drive (virtio-fs) - [chardev "qemu_config"] + [chardev "dev-qemu_config-drive-virtio-fs"] backend = "socket" path = "/dev/virtio-fs" @@ -783,7 +783,7 @@ func TestQemuConfigTemplates(t *testing.T) { addr = "10.2" multifunction = "on" tag = "config" - chardev = "qemu_config"`, + chardev = "dev-qemu_config-drive-virtio-fs"`, }, { qemuDriveConfigOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", false}, @@ -791,14 +791,14 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "virtio-fs", }, `# Config drive (virtio-fs) - [chardev "qemu_config"] + [chardev "dev-qemu_config-drive-virtio-fs"] backend = "socket" path = "/var/virtio-fs" [device "dev-qemu_config-drive-virtio-fs"] driver = "vhost-user-fs-ccw" tag = "config" - chardev = "qemu_config"`, + chardev = "dev-qemu_config-drive-virtio-fs"`, }, { qemuDriveConfigOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", true}, @@ -806,7 +806,7 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "9p", }, `# Config drive (9p) - [fsdev "qemu_config"] + [fsdev "dev-qemu_config-drive-9p"] fsdriver = "local" security_model = "none" readonly = "on" @@ -816,7 +816,7 @@ func TestQemuConfigTemplates(t *testing.T) { driver = "virtio-9p-ccw" multifunction = "on" mount_tag = "config" - fsdev = "qemu_config"`, + fsdev = "dev-qemu_config-drive-9p"`, }, { qemuDriveConfigOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", true}, @@ -844,7 +844,7 @@ func TestQemuConfigTemplates(t *testing.T) { proxyFD: 5, }, `# stub drive (9p) - [fsdev "lxd_stub"] + [fsdev "dev-lxd_stub-9p"] fsdriver = "proxy" sock_fd = "5" readonly = "off" @@ -855,7 +855,7 @@ func TestQemuConfigTemplates(t *testing.T) { addr = "00.5" multifunction = "on" mount_tag = "mtag" - fsdev = "lxd_stub"`, + fsdev = "dev-lxd_stub-9p"`, }, { qemuDriveDirOpts{ dev: qemuDevOpts{"pcie", "qemu_pcie1", "10.2", false}, @@ -865,7 +865,7 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "virtio-fs", }, `# vfs drive (virtio-fs) - [chardev "lxd_vfs"] + [chardev "dev-lxd_vfs-virtio-fs"] backend = "socket" path = "/dev/virtio" @@ -874,7 +874,7 @@ func TestQemuConfigTemplates(t *testing.T) { bus = "qemu_pcie1" addr = "10.2" tag = "vtag" - chardev = "lxd_vfs"`, + chardev = "dev-lxd_vfs-virtio-fs"`, }, { qemuDriveDirOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", true}, @@ -884,7 +884,7 @@ func TestQemuConfigTemplates(t *testing.T) { protocol: "virtio-fs", }, `# vfs drive (virtio-fs) - [chardev "lxd_vfs"] + [chardev "dev-lxd_vfs-virtio-fs"] backend = "socket" path = "/dev/vio" @@ -892,7 +892,7 @@ func TestQemuConfigTemplates(t *testing.T) { driver = "vhost-user-fs-ccw" multifunction = "on" tag = "vtag" - chardev = "lxd_vfs"`, + chardev = "dev-lxd_vfs-virtio-fs"`, }, { qemuDriveDirOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", false}, @@ -903,7 +903,7 @@ func TestQemuConfigTemplates(t *testing.T) { proxyFD: 3, }, `# stub2 drive (9p) - [fsdev "lxd_stub2"] + [fsdev "dev-lxd_stub2-9p"] fsdriver = "proxy" sock_fd = "3" readonly = "on" @@ -911,7 +911,7 @@ func TestQemuConfigTemplates(t *testing.T) { [device "dev-lxd_stub2-9p"] driver = "virtio-9p-ccw" mount_tag = "mtag2" - fsdev = "lxd_stub2"`, + fsdev = "dev-lxd_stub2-9p"`, }, { qemuDriveDirOpts{ dev: qemuDevOpts{"ccw", "devBus", "busAddr", true}, diff --git a/lxd/instance/drivers/driver_qemu_templates.go b/lxd/instance/drivers/driver_qemu_templates.go index c6b789266352..dd491bcad188 100644 --- a/lxd/instance/drivers/driver_qemu_templates.go +++ b/lxd/instance/drivers/driver_qemu_templates.go @@ -1,13 +1,39 @@ package drivers import ( + "crypto/sha256" + "encoding/base64" "fmt" "strings" "github.com/canonical/lxd/lxd/resources" + "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/shared/osarch" ) +// qemuDeviceNameOrID generates a QEMU device name or ID. +// Respects the property length limit by hashing the device name when necessary. Also escapes / to -, and - to --. +func qemuDeviceNameOrID(prefix string, deviceName string, suffix string, maxLength int) string { + baseName := filesystem.PathNameEncode(deviceName) + maxNameLength := maxLength - (len(prefix) + len(suffix)) + + if len(baseName) > maxNameLength { + // If the name is too long, hash it as SHA-256 (32 bytes). + // Then encode the SHA-256 binary hash as Base64 Raw URL format and trim down if needed. + hash := sha256.New() + hash.Write([]byte(baseName)) + binaryHash := hash.Sum(nil) + + // Raw URL avoids the use of "+" character and the padding "=" character which QEMU doesn't allow. + baseName = base64.RawURLEncoding.EncodeToString(binaryHash) + if len(baseName) > maxNameLength { + baseName = baseName[0:maxNameLength] + } + } + + return fmt.Sprintf("%s%s%s", prefix, baseName, suffix) +} + type cfgEntry struct { key string value string @@ -630,8 +656,8 @@ func qemuDriveFirmware(opts *qemuDriveFirmwareOpts) []cfgSection { type qemuHostDriveOpts struct { dev qemuDevOpts + id string name string - nameSuffix string comment string fsdriver string mountTag string @@ -656,7 +682,7 @@ func qemuHostDrive(opts *qemuHostDriveOpts) []cfgSection { } driveSection = cfgSection{ - name: fmt.Sprintf(`fsdev "%s"`, opts.name), + name: fmt.Sprintf(`fsdev "%s"`, opts.id), comment: opts.comment, entries: []cfgEntry{ {key: "fsdriver", value: opts.fsdriver}, @@ -672,11 +698,11 @@ func qemuHostDrive(opts *qemuHostDriveOpts) []cfgSection { extraDeviceEntries = []cfgEntry{ {key: "mount_tag", value: opts.mountTag}, - {key: "fsdev", value: opts.name}, + {key: "fsdev", value: opts.id}, } } else if opts.protocol == "virtio-fs" { driveSection = cfgSection{ - name: fmt.Sprintf(`chardev "%s"`, opts.name), + name: fmt.Sprintf(`chardev "%s"`, opts.id), comment: opts.comment, entries: []cfgEntry{ {key: "backend", value: "socket"}, @@ -689,7 +715,7 @@ func qemuHostDrive(opts *qemuHostDriveOpts) []cfgSection { extraDeviceEntries = []cfgEntry{ {key: "tag", value: opts.mountTag}, - {key: "chardev", value: opts.name}, + {key: "chardev", value: opts.id}, } } else { return []cfgSection{} @@ -698,7 +724,7 @@ func qemuHostDrive(opts *qemuHostDriveOpts) []cfgSection { return []cfgSection{ driveSection, { - name: fmt.Sprintf(`device "dev-%s%s-%s"`, opts.name, opts.nameSuffix, opts.protocol), + name: fmt.Sprintf(`device "%s"`, opts.id), entries: append(qemuDeviceEntries(&deviceOpts), extraDeviceEntries...), }, } @@ -713,9 +739,9 @@ type qemuDriveConfigOpts struct { func qemuDriveConfig(opts *qemuDriveConfigOpts) []cfgSection { return qemuHostDrive(&qemuHostDriveOpts{ dev: opts.dev, + id: fmt.Sprintf("dev-qemu_config-drive-%s", opts.protocol), // Devices use "qemu_" prefix indicating that this is a internally named device. name: "qemu_config", - nameSuffix: "-drive", comment: fmt.Sprintf("Config drive (%s)", opts.protocol), mountTag: "config", protocol: opts.protocol, @@ -739,6 +765,7 @@ type qemuDriveDirOpts struct { func qemuDriveDir(opts *qemuDriveDirOpts) []cfgSection { return qemuHostDrive(&qemuHostDriveOpts{ dev: opts.dev, + id: qemuDeviceNameOrID(qemuDeviceIDPrefix, opts.devName, "-"+opts.protocol, qemuDeviceIDMaxLength), // Devices use "lxd_" prefix indicating that this is a user named device. name: fmt.Sprintf("lxd_%s", opts.devName), comment: fmt.Sprintf("%s drive (%s)", opts.devName, opts.protocol), @@ -865,8 +892,9 @@ type qemuTPMOpts struct { } func qemuTPM(opts *qemuTPMOpts) []cfgSection { - chardev := fmt.Sprintf("qemu_tpm-chardev_%s", opts.devName) - tpmdev := fmt.Sprintf("qemu_tpm-tpmdev_%s", opts.devName) + chardev := qemuDeviceNameOrID("qemu_tpm-chardev_", opts.devName, "", qemuDeviceIDMaxLength) + tpmdev := qemuDeviceNameOrID("qemu_tpm-tpmdev_", opts.devName, "", qemuDeviceIDMaxLength) + device := qemuDeviceNameOrID(qemuDeviceIDPrefix, opts.devName, "", qemuDeviceIDMaxLength) return []cfgSection{{ name: fmt.Sprintf(`chardev "%s"`, chardev), @@ -881,7 +909,7 @@ func qemuTPM(opts *qemuTPMOpts) []cfgSection { {key: "chardev", value: chardev}, }, }, { - name: fmt.Sprintf(`device "dev-lxd_%s"`, opts.devName), + name: fmt.Sprintf(`device "%s"`, device), entries: []cfgEntry{ {key: "driver", value: "tpm-crb"}, {key: "tpmdev", value: tpmdev}, diff --git a/lxd/instances_post.go b/lxd/instances_post.go index ec1c97c723c0..67b0dad1b371 100644 --- a/lxd/instances_post.go +++ b/lxd/instances_post.go @@ -354,15 +354,15 @@ func createFromMigration(s *state.State, r *http.Request, projectName string, pr } migrationArgs := migrationSinkArgs{ - URL: req.Source.Operation, - Dialer: dialer, - Instance: inst, - Secrets: req.Source.Websockets, - Push: push, - Live: req.Source.Live, - InstanceOnly: instanceOnly, - ClusterMoveSourceName: clusterMoveSourceName, - Refresh: req.Source.Refresh, + url: req.Source.Operation, + dialer: dialer, + instance: inst, + secrets: req.Source.Websockets, + push: push, + live: req.Source.Live, + instanceOnly: instanceOnly, + clusterMoveSourceName: clusterMoveSourceName, + refresh: req.Source.Refresh, } sink, err := newMigrationSink(&migrationArgs) diff --git a/lxd/linux/ioctls.go b/lxd/linux/ioctls.go new file mode 100644 index 000000000000..4e10a5945584 --- /dev/null +++ b/lxd/linux/ioctls.go @@ -0,0 +1,17 @@ +package linux + +/* +#include +#include +#include +*/ +import "C" + +// IoctlBtrfsSetReceivedSubvol is used to set information about a received subvolume. +const IoctlBtrfsSetReceivedSubvol = C.BTRFS_IOC_SET_RECEIVED_SUBVOL + +// IoctlHIDIOCGrawInfo contains the bus type, the vendor ID (VID), and product ID (PID) of the device. +const IoctlHIDIOCGrawInfo = C.HIDIOCGRAWINFO + +// IoctlVhostVsockSetGuestCid is used to set the vsock guest context ID. +const IoctlVhostVsockSetGuestCid = C.VHOST_VSOCK_SET_GUEST_CID diff --git a/lxd/migrate.go b/lxd/migrate.go index cd76b9ee3fee..e85936e86b9b 100644 --- a/lxd/migrate.go +++ b/lxd/migrate.go @@ -15,7 +15,6 @@ import ( "github.com/gorilla/websocket" "google.golang.org/protobuf/proto" - "github.com/canonical/lxd/lxd/idmap" "github.com/canonical/lxd/lxd/instance" "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/operations" @@ -144,6 +143,8 @@ type migrationSourceWs struct { pushSecrets map[string]string } +// Metadata returns a map where each key is a connection name and each value is +// the secret of the corresponding websocket connection. func (s *migrationSourceWs) Metadata() any { secrets := make(shared.Jmap, len(s.conns)) for connName, conn := range s.conns { @@ -153,6 +154,9 @@ func (s *migrationSourceWs) Metadata() any { return secrets } +// Connect handles an incoming HTTP request to establish a websocket connection for migration. +// It verifies the provided secret and matches it to the appropriate connection. If the secret +// is valid, it accepts the incoming connection. Otherwise, it returns an error. func (s *migrationSourceWs) Connect(op *operations.Operation, r *http.Request, w http.ResponseWriter) error { incomingSecret := r.FormValue("secret") if incomingSecret == "" { @@ -186,29 +190,27 @@ type migrationSink struct { refresh bool } -// MigrationSinkArgs arguments to configure migration sink. +// migrationSinkArgs arguments to configure migration sink. type migrationSinkArgs struct { // General migration fields - Dialer *websocket.Dialer - Push bool - Secrets map[string]string - URL string - - // Instance specific fields - Instance instance.Instance - InstanceOnly bool - Idmap *idmap.IdmapSet - Live bool - Refresh bool - ClusterMoveSourceName string - Snapshots []*migration.Snapshot + dialer *websocket.Dialer + push bool + secrets map[string]string + url string + + // instance specific fields + instance instance.Instance + instanceOnly bool + live bool + refresh bool + clusterMoveSourceName string + snapshots []*migration.Snapshot // Storage specific fields - VolumeOnly bool - VolumeSize int64 + volumeOnly bool // Transport specific fields - RsyncFeatures []string + rsyncFeatures []string } // Metadata returns metadata for the migration sink. diff --git a/lxd/migrate_instance.go b/lxd/migrate_instance.go index 48b8fcc0a5c3..6a197ffd079f 100644 --- a/lxd/migrate_instance.go +++ b/lxd/migrate_instance.go @@ -81,6 +81,9 @@ func newMigrationSource(inst instance.Instance, stateful bool, instanceOnly bool return &ret, nil } +// Do performs the migration operation on the source side for the given state and +// operation. It sets up the necessary websocket connections for control, state, +// and filesystem, and then initiates the migration process. func (s *migrationSourceWs) Do(state *state.State, migrateOp *operations.Operation) error { l := logger.AddContext(logger.Ctx{"project": s.instance.Project().Name, "instance": s.instance.Name(), "live": s.live, "clusterMoveSourceName": s.clusterMoveSourceName, "push": s.pushOperationURL != ""}) @@ -158,14 +161,14 @@ func (s *migrationSourceWs) Do(state *state.State, migrateOp *operations.Operati func newMigrationSink(args *migrationSinkArgs) (*migrationSink, error) { sink := migrationSink{ migrationFields: migrationFields{ - instance: args.Instance, - instanceOnly: args.InstanceOnly, - live: args.Live, + instance: args.instance, + instanceOnly: args.instanceOnly, + live: args.live, }, - url: args.URL, - clusterMoveSourceName: args.ClusterMoveSourceName, - push: args.Push, - refresh: args.Refresh, + url: args.url, + clusterMoveSourceName: args.clusterMoveSourceName, + push: args.push, + refresh: args.refresh, } secretNames := []string{api.SecretNameControl, api.SecretNameFilesystem} @@ -183,16 +186,16 @@ func newMigrationSink(args *migrationSinkArgs) (*migrationSink, error) { sink.conns = make(map[string]*migrationConn, len(secretNames)) for _, connName := range secretNames { if !sink.push { - if args.Secrets[connName] == "" { + if args.secrets[connName] == "" { return nil, fmt.Errorf("Expected %q connection secret missing from migration sink target request", connName) } - u, err := url.Parse(fmt.Sprintf("wss://%s/websocket", strings.TrimPrefix(args.URL, "https://"))) + u, err := url.Parse(fmt.Sprintf("wss://%s/websocket", strings.TrimPrefix(args.url, "https://"))) if err != nil { return nil, fmt.Errorf("Failed parsing websocket URL for migration sink %q connection: %w", connName, err) } - sink.conns[connName] = newMigrationConn(args.Secrets[connName], args.Dialer, u) + sink.conns[connName] = newMigrationConn(args.secrets[connName], args.dialer, u) } else { secret, err := shared.RandomCryptoString() if err != nil { @@ -206,6 +209,9 @@ func newMigrationSink(args *migrationSinkArgs) (*migrationSink, error) { return &sink, nil } +// Do performs the migration operation on the target side (sink) for the given +// state and instance operation. It sets up the necessary websocket connections +// for control, state, and filesystem, and then receives the migration data. func (c *migrationSink) Do(state *state.State, instOp *operationlock.InstanceOperation) error { l := logger.AddContext(logger.Ctx{"project": c.instance.Project().Name, "instance": c.instance.Name(), "live": c.live, "clusterMoveSourceName": c.clusterMoveSourceName, "push": c.push}) diff --git a/lxd/migrate_storage_volumes.go b/lxd/migrate_storage_volumes.go index 918e83b88aeb..7f1f8ed36483 100644 --- a/lxd/migrate_storage_volumes.go +++ b/lxd/migrate_storage_volumes.go @@ -65,6 +65,9 @@ func newStorageMigrationSource(volumeOnly bool, pushTarget *api.StorageVolumePos return &ret, nil } +// DoStorage handles the migration of a storage volume from the source to the target. +// It waits for migration connections, negotiates migration types, and initiates +// the volume transfer. func (s *migrationSourceWs) DoStorage(state *state.State, projectName string, poolName string, volName string, migrateOp *operations.Operation) error { l := logger.AddContext(logger.Ctx{"project": projectName, "pool": poolName, "volume": volName, "push": s.pushOperationURL != ""}) @@ -204,27 +207,27 @@ func (s *migrationSourceWs) DoStorage(state *state.State, projectName string, po func newStorageMigrationSink(args *migrationSinkArgs) (*migrationSink, error) { sink := migrationSink{ migrationFields: migrationFields{ - volumeOnly: args.VolumeOnly, + volumeOnly: args.volumeOnly, }, - url: args.URL, - push: args.Push, - refresh: args.Refresh, + url: args.url, + push: args.push, + refresh: args.refresh, } secretNames := []string{api.SecretNameControl, api.SecretNameFilesystem} sink.conns = make(map[string]*migrationConn, len(secretNames)) for _, connName := range secretNames { if !sink.push { - if args.Secrets[connName] == "" { + if args.secrets[connName] == "" { return nil, fmt.Errorf("Expected %q connection secret missing from migration sink target request", connName) } - u, err := url.Parse(fmt.Sprintf("wss://%s/websocket", strings.TrimPrefix(args.URL, "https://"))) + u, err := url.Parse(fmt.Sprintf("wss://%s/websocket", strings.TrimPrefix(args.url, "https://"))) if err != nil { return nil, fmt.Errorf("Failed parsing websocket URL for migration sink %q connection: %w", connName, err) } - sink.conns[connName] = newMigrationConn(args.Secrets[connName], args.Dialer, u) + sink.conns[connName] = newMigrationConn(args.secrets[connName], args.dialer, u) } else { secret, err := shared.RandomCryptoString() if err != nil { @@ -238,6 +241,8 @@ func newStorageMigrationSink(args *migrationSinkArgs) (*migrationSink, error) { return &sink, nil } +// DoStorage handles the storage volume migration on the target side. It waits for +// migration connections, negotiates migration types, and initiates the volume reception. func (c *migrationSink) DoStorage(state *state.State, projectName string, poolName string, req *api.StorageVolumesPost, op *operations.Operation) error { l := logger.AddContext(logger.Ctx{"project": projectName, "pool": poolName, "volume": req.Name, "push": c.push}) @@ -326,15 +331,15 @@ func (c *migrationSink) DoStorage(state *state.State, projectName string, poolNa MigrationType: respTypes[0], TrackProgress: true, ContentType: req.ContentType, - Refresh: args.Refresh, - VolumeOnly: args.VolumeOnly, + Refresh: args.refresh, + VolumeOnly: args.volumeOnly, } // A zero length Snapshots slice indicates volume only migration in // VolumeTargetArgs. So if VoluneOnly was requested, do not populate them. - if !args.VolumeOnly { - volTargetArgs.Snapshots = make([]string, 0, len(args.Snapshots)) - for _, snap := range args.Snapshots { + if !args.volumeOnly { + volTargetArgs.Snapshots = make([]string, 0, len(args.snapshots)) + for _, snap := range args.snapshots { volTargetArgs.Snapshots = append(volTargetArgs.Snapshots, *snap.Name) } } @@ -423,10 +428,10 @@ func (c *migrationSink) DoStorage(state *state.State, projectName string, poolNa // as part of MigrationSinkArgs below. rsyncFeatures := respHeader.GetRsyncFeaturesSlice() args := migrationSinkArgs{ - RsyncFeatures: rsyncFeatures, - Snapshots: respHeader.Snapshots, - VolumeOnly: c.volumeOnly, - Refresh: c.refresh, + rsyncFeatures: rsyncFeatures, + snapshots: respHeader.Snapshots, + volumeOnly: c.volumeOnly, + refresh: c.refresh, } fsConn, err := c.conns[api.SecretNameFilesystem].WebsocketIO(context.TODO()) diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go index c8e46f5508a4..9716ab3e17d1 100644 --- a/lxd/network/driver_bridge.go +++ b/lxd/network/driver_bridge.go @@ -1357,16 +1357,20 @@ func (n *bridge) setup(oldConfig map[string]string) error { } } + var ipv4Address net.IP + // Configure IPv4. if !shared.ValueInSlice(n.config["ipv4.address"], []string{"", "none"}) { + var subnet *net.IPNet + // Parse the subnet. - ipAddress, subnet, err := net.ParseCIDR(n.config["ipv4.address"]) + ipv4Address, subnet, err = net.ParseCIDR(n.config["ipv4.address"]) if err != nil { return fmt.Errorf("Failed parsing ipv4.address: %w", err) } // Update the dnsmasq config. - dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--listen-address=%s", ipAddress.String())) + dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--listen-address=%s", ipv4Address.String())) if n.DHCPv4Subnet() != nil { if !shared.ValueInSlice("--dhcp-no-override", dnsmasqCmd) { dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-no-override", "--dhcp-authoritative", fmt.Sprintf("--dhcp-leasefile=%s", shared.VarPath("networks", n.name, "dnsmasq.leases")), fmt.Sprintf("--dhcp-hostsfile=%s", shared.VarPath("networks", n.name, "dnsmasq.hosts"))}...) @@ -1482,6 +1486,8 @@ func (n *bridge) setup(oldConfig map[string]string) error { return err } + var ipv6Address net.IP + // Configure IPv6. if !shared.ValueInSlice(n.config["ipv6.address"], []string{"", "none"}) { // Enable IPv6 for the subnet. @@ -1490,8 +1496,10 @@ func (n *bridge) setup(oldConfig map[string]string) error { return err } + var subnet *net.IPNet + // Parse the subnet. - ipAddress, subnet, err := net.ParseCIDR(n.config["ipv6.address"]) + ipv6Address, subnet, err = net.ParseCIDR(n.config["ipv6.address"]) if err != nil { return fmt.Errorf("Failed parsing ipv6.address: %w", err) } @@ -1514,7 +1522,7 @@ func (n *bridge) setup(oldConfig map[string]string) error { } // Update the dnsmasq config. - dnsmasqCmd = append(dnsmasqCmd, []string{fmt.Sprintf("--listen-address=%s", ipAddress.String()), "--enable-ra"}...) + dnsmasqCmd = append(dnsmasqCmd, []string{fmt.Sprintf("--listen-address=%s", ipv6Address.String()), "--enable-ra"}...) if n.DHCPv6Subnet() != nil { if n.hasIPv6Firewall() { fwOpts.FeaturesV6.ICMPDHCPDNSAccess = true @@ -1736,6 +1744,9 @@ func (n *bridge) setup(oldConfig map[string]string) error { fmt.Sprintf("--dhcp-hostsfile=%s", shared.VarPath("networks", n.name, "dnsmasq.hosts")), "--dhcp-range", fmt.Sprintf("%s,%s,%s", dhcpalloc.GetIP(hostSubnet, 2).String(), dhcpalloc.GetIP(hostSubnet, -2).String(), expiry)}...) + // Save the dnsmasq listen address so that firewall rules can be added later + ipv4Address = net.ParseIP(addr[0]) + // Setup the tunnel. if n.config["fan.type"] == "ipip" { r := &ip.Route{ @@ -2118,7 +2129,7 @@ func (n *bridge) setup(oldConfig map[string]string) error { // Setup firewall. n.logger.Debug("Setting up firewall") - err = n.state.Firewall.NetworkSetup(n.name, fwOpts) + err = n.state.Firewall.NetworkSetup(n.name, ipv4Address, ipv6Address, fwOpts) if err != nil { return fmt.Errorf("Failed to setup firewall: %w", err) } diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go index 9e60069ba930..ebafe4447918 100644 --- a/lxd/storage/backend_lxd.go +++ b/lxd/storage/backend_lxd.go @@ -37,6 +37,7 @@ import ( "github.com/canonical/lxd/lxd/project" "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/state" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/drivers" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/lxd/storage/memorypipe" @@ -1453,7 +1454,7 @@ func (b *lxdBackend) RefreshCustomVolume(projectName string, srcProjectName stri return err } - volSize, err = drivers.BlockDiskSizeBytes(volDiskPath) + volSize, err = block.DiskSizeBytes(volDiskPath) if err != nil { return err } @@ -2125,10 +2126,10 @@ func (b *lxdBackend) CreateInstanceFromMigration(inst instance.Instance, conn io // This way if the volume being received is larger than the pool default size, the block volume created // will still be able to accommodate it. if args.VolumeSize > 0 && contentType == drivers.ContentTypeBlock { - b.logger.Debug("Setting volume size from offer header", logger.Ctx{"size": args.VolumeSize}) + l.Debug("Setting volume size from offer header", logger.Ctx{"size": args.VolumeSize}) args.Config["size"] = fmt.Sprintf("%d", args.VolumeSize) } else if args.Config["size"] != "" { - b.logger.Debug("Using volume size from root disk config", logger.Ctx{"size": args.Config["size"]}) + l.Debug("Using volume size from root disk config", logger.Ctx{"size": args.Config["size"]}) } var preFiller drivers.VolumeFiller @@ -4981,7 +4982,7 @@ func (b *lxdBackend) CreateCustomVolumeFromCopy(projectName string, srcProjectNa return err } - volSize, err = drivers.BlockDiskSizeBytes(volDiskPath) + volSize, err = block.DiskSizeBytes(volDiskPath) if err != nil { return err } diff --git a/lxd/storage/backend_mock.go b/lxd/storage/backend_mock.go index 1424892de51e..860556bca112 100644 --- a/lxd/storage/backend_mock.go +++ b/lxd/storage/backend_mock.go @@ -27,42 +27,52 @@ type mockBackend struct { driver drivers.Driver } +// ID ... func (b *mockBackend) ID() int64 { return 1 // The tests expect the storage pool ID to be 1. } +// Name ... func (b *mockBackend) Name() string { return b.name } +// Description ... func (b *mockBackend) Description() string { return "" } +// ValidateName ... func (b *mockBackend) ValidateName(value string) error { return nil } +// Validate ... func (b *mockBackend) Validate(config map[string]string) error { return nil } +// Status ... func (b *mockBackend) Status() string { return api.NetworkStatusUnknown } +// LocalStatus ... func (b *mockBackend) LocalStatus() string { return api.NetworkStatusUnknown } +// ToAPI ... func (b *mockBackend) ToAPI() api.StoragePool { return api.StoragePool{} } +// Driver ... func (b *mockBackend) Driver() drivers.Driver { return b.driver } +// MigrationTypes ... func (b *mockBackend) MigrationTypes(contentType drivers.ContentType, refresh bool, copySnapshots bool) []migration.Type { return []migration.Type{ { @@ -72,82 +82,102 @@ func (b *mockBackend) MigrationTypes(contentType drivers.ContentType, refresh bo } } +// GetResources ... func (b *mockBackend) GetResources() (*api.ResourcesStoragePool, error) { return nil, nil } +// IsUsed ... func (b *mockBackend) IsUsed() (bool, error) { return false, nil } +// Delete ... func (b *mockBackend) Delete(clientType request.ClientType, op *operations.Operation) error { return nil } +// Update ... func (b *mockBackend) Update(clientType request.ClientType, newDescription string, newConfig map[string]string, op *operations.Operation) error { return nil } +// Create ... func (b *mockBackend) Create(clientType request.ClientType, op *operations.Operation) error { return nil } +// Mount ... func (b *mockBackend) Mount() (bool, error) { return true, nil } +// Unmount ... func (b *mockBackend) Unmount() (bool, error) { return true, nil } +// ApplyPatch ... func (b *mockBackend) ApplyPatch(name string) error { return nil } +// GetVolume ... func (b *mockBackend) GetVolume(volType drivers.VolumeType, contentType drivers.ContentType, volName string, volConfig map[string]string) drivers.Volume { return drivers.Volume{} } +// CreateInstance ... func (b *mockBackend) CreateInstance(inst instance.Instance, op *operations.Operation) error { return nil } +// CreateInstanceFromBackup ... func (b *mockBackend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op *operations.Operation) (func(instance.Instance) error, revert.Hook, error) { return nil, nil, nil } +// CreateInstanceFromCopy ... func (b *mockBackend) CreateInstanceFromCopy(inst instance.Instance, src instance.Instance, snapshots bool, allowInconsistent bool, op *operations.Operation) error { return nil } +// CreateInstanceFromImage ... func (b *mockBackend) CreateInstanceFromImage(inst instance.Instance, fingerprint string, op *operations.Operation) error { return nil } +// CreateInstanceFromMigration ... func (b *mockBackend) CreateInstanceFromMigration(inst instance.Instance, conn io.ReadWriteCloser, args migration.VolumeTargetArgs, op *operations.Operation) error { return nil } +// RenameInstance ... func (b *mockBackend) RenameInstance(inst instance.Instance, newName string, op *operations.Operation) error { return nil } +// DeleteInstance ... func (b *mockBackend) DeleteInstance(inst instance.Instance, op *operations.Operation) error { return nil } +// UpdateInstance ... func (b *mockBackend) UpdateInstance(inst instance.Instance, newDesc string, newConfig map[string]string, op *operations.Operation) error { return nil } +// GenerateCustomVolumeBackupConfig ... func (b *mockBackend) GenerateCustomVolumeBackupConfig(projectName string, volName string, snapshots bool, op *operations.Operation) (*backupConfig.Config, error) { return nil, nil } +// GenerateInstanceBackupConfig ... func (b *mockBackend) GenerateInstanceBackupConfig(inst instance.Instance, snapshots bool, op *operations.Operation) (*backupConfig.Config, error) { return nil, nil } +// UpdateInstanceBackupFile ... func (b *mockBackend) UpdateInstanceBackupFile(inst instance.Instance, snapshot bool, op *operations.Operation) error { return nil } @@ -157,202 +187,252 @@ func (b *mockBackend) CheckInstanceBackupFileSnapshots(backupConf *backupConfig. return nil, nil } +// ListUnknownVolumes ... func (b *mockBackend) ListUnknownVolumes(op *operations.Operation) (map[string][]*backupConfig.Config, error) { return nil, nil } +// ImportInstance ... func (b *mockBackend) ImportInstance(inst instance.Instance, poolVol *backupConfig.Config, op *operations.Operation) (revert.Hook, error) { return nil, nil } +// MigrateInstance ... func (b *mockBackend) MigrateInstance(inst instance.Instance, conn io.ReadWriteCloser, args *migration.VolumeSourceArgs, op *operations.Operation) error { return nil } +// CleanupInstancePaths ... func (b *mockBackend) CleanupInstancePaths(inst instance.Instance, op *operations.Operation) error { return nil } +// RefreshCustomVolume ... func (b *mockBackend) RefreshCustomVolume(projectName string, srcProjectName string, volName string, desc string, config map[string]string, srcPoolName, srcVolName string, srcVolOnly bool, op *operations.Operation) error { return nil } +// RefreshInstance ... func (b *mockBackend) RefreshInstance(inst instance.Instance, src instance.Instance, srcSnapshots []instance.Instance, allowInconsistent bool, op *operations.Operation) error { return nil } +// BackupInstance ... func (b *mockBackend) BackupInstance(inst instance.Instance, tarWriter *instancewriter.InstanceTarWriter, optimized bool, snapshots bool, op *operations.Operation) error { return nil } +// GetInstanceUsage ... func (b *mockBackend) GetInstanceUsage(inst instance.Instance) (*VolumeUsage, error) { return nil, nil } +// SetInstanceQuota ... func (b *mockBackend) SetInstanceQuota(inst instance.Instance, size string, vmStateSize string, op *operations.Operation) error { return nil } +// MountInstance ... func (b *mockBackend) MountInstance(inst instance.Instance, op *operations.Operation) (*MountInfo, error) { return &MountInfo{}, nil } +// UnmountInstance ... func (b *mockBackend) UnmountInstance(inst instance.Instance, op *operations.Operation) error { return nil } +// CreateInstanceSnapshot ... func (b *mockBackend) CreateInstanceSnapshot(i instance.Instance, src instance.Instance, op *operations.Operation) error { return nil } +// RenameInstanceSnapshot ... func (b *mockBackend) RenameInstanceSnapshot(inst instance.Instance, newName string, op *operations.Operation) error { return nil } +// DeleteInstanceSnapshot ... func (b *mockBackend) DeleteInstanceSnapshot(inst instance.Instance, op *operations.Operation) error { return nil } +// RestoreInstanceSnapshot ... func (b *mockBackend) RestoreInstanceSnapshot(inst instance.Instance, src instance.Instance, op *operations.Operation) error { return nil } +// MountInstanceSnapshot ... func (b *mockBackend) MountInstanceSnapshot(inst instance.Instance, op *operations.Operation) (*MountInfo, error) { return &MountInfo{}, nil } +// UnmountInstanceSnapshot ... func (b *mockBackend) UnmountInstanceSnapshot(inst instance.Instance, op *operations.Operation) error { return nil } +// UpdateInstanceSnapshot ... func (b *mockBackend) UpdateInstanceSnapshot(inst instance.Instance, newDesc string, newConfig map[string]string, op *operations.Operation) error { return nil } +// EnsureImage ... func (b *mockBackend) EnsureImage(fingerprint string, op *operations.Operation) error { return nil } +// DeleteImage ... func (b *mockBackend) DeleteImage(fingerprint string, op *operations.Operation) error { return nil } +// UpdateImage ... func (b *mockBackend) UpdateImage(fingerprint, newDesc string, newConfig map[string]string, op *operations.Operation) error { return nil } +// CreateBucket ... func (b *mockBackend) CreateBucket(projectName string, bucket api.StorageBucketsPost, op *operations.Operation) error { return nil } +// UpdateBucket ... func (b *mockBackend) UpdateBucket(projectName string, bucketName string, bucket api.StorageBucketPut, op *operations.Operation) error { return nil } +// DeleteBucket ... func (b *mockBackend) DeleteBucket(projectName string, bucketName string, op *operations.Operation) error { return nil } +// ImportBucket ... func (b *mockBackend) ImportBucket(projectName string, poolVol *backupConfig.Config, op *operations.Operation) (revert.Hook, error) { return nil, nil } +// CreateBucketKey ... func (b *mockBackend) CreateBucketKey(projectName string, bucketName string, key api.StorageBucketKeysPost, op *operations.Operation) (*api.StorageBucketKey, error) { return nil, nil } +// UpdateBucketKey ... func (b *mockBackend) UpdateBucketKey(projectName string, bucketName string, keyName string, key api.StorageBucketKeyPut, op *operations.Operation) error { return nil } +// DeleteBucketKey ... func (b *mockBackend) DeleteBucketKey(projectName string, bucketName string, keyName string, op *operations.Operation) error { return nil } +// ActivateBucket ... func (b *mockBackend) ActivateBucket(projectName string, bucketName string, op *operations.Operation) (*miniod.Process, error) { return nil, nil } +// GetBucketURL ... func (b *mockBackend) GetBucketURL(bucketName string) *url.URL { return nil } +// CreateCustomVolume ... func (b *mockBackend) CreateCustomVolume(projectName string, volName string, desc string, config map[string]string, contentType drivers.ContentType, op *operations.Operation) error { return nil } +// CreateCustomVolumeFromCopy ... func (b *mockBackend) CreateCustomVolumeFromCopy(projectName string, srcProjectName string, volName string, desc string, config map[string]string, srcPoolName string, srcVolName string, srcVolOnly bool, op *operations.Operation) error { return nil } +// RenameCustomVolume ... func (b *mockBackend) RenameCustomVolume(projectName string, volName string, newName string, op *operations.Operation) error { return nil } +// UpdateCustomVolume ... func (b *mockBackend) UpdateCustomVolume(projectName string, volName string, newDesc string, newConfig map[string]string, op *operations.Operation) error { return nil } +// DeleteCustomVolume ... func (b *mockBackend) DeleteCustomVolume(projectName string, volName string, op *operations.Operation) error { return nil } +// MigrateCustomVolume ... func (b *mockBackend) MigrateCustomVolume(projectName string, conn io.ReadWriteCloser, args *migration.VolumeSourceArgs, op *operations.Operation) error { return nil } +// CreateCustomVolumeFromMigration ... func (b *mockBackend) CreateCustomVolumeFromMigration(projectName string, conn io.ReadWriteCloser, args migration.VolumeTargetArgs, op *operations.Operation) error { return nil } +// GetCustomVolumeDisk ... func (b *mockBackend) GetCustomVolumeDisk(projectName string, volName string) (string, error) { return "", nil } +// GetCustomVolumeUsage ... func (b *mockBackend) GetCustomVolumeUsage(projectName string, volName string) (*VolumeUsage, error) { return nil, nil } +// MountCustomVolume ... func (b *mockBackend) MountCustomVolume(projectName string, volName string, op *operations.Operation) (*MountInfo, error) { return nil, nil } +// UnmountCustomVolume ... func (b *mockBackend) UnmountCustomVolume(projectName string, volName string, op *operations.Operation) (bool, error) { return true, nil } +// ImportCustomVolume ... func (b *mockBackend) ImportCustomVolume(projectName string, poolVol *backupConfig.Config, op *operations.Operation) (revert.Hook, error) { return nil, nil } +// CreateCustomVolumeSnapshot ... func (b *mockBackend) CreateCustomVolumeSnapshot(projectName string, volName string, newSnapshotName string, expiryDate time.Time, op *operations.Operation) error { return nil } +// RenameCustomVolumeSnapshot ... func (b *mockBackend) RenameCustomVolumeSnapshot(projectName string, volName string, newName string, op *operations.Operation) error { return nil } +// DeleteCustomVolumeSnapshot ... func (b *mockBackend) DeleteCustomVolumeSnapshot(projectName string, volName string, op *operations.Operation) error { return nil } +// UpdateCustomVolumeSnapshot ... func (b *mockBackend) UpdateCustomVolumeSnapshot(projectName string, volName string, newDesc string, newConfig map[string]string, expiryDate time.Time, op *operations.Operation) error { return nil } +// RestoreCustomVolume ... func (b *mockBackend) RestoreCustomVolume(projectName string, volName string, snapshotName string, op *operations.Operation) error { return nil } +// BackupCustomVolume ... func (b *mockBackend) BackupCustomVolume(projectName string, volName string, tarWriter *instancewriter.InstanceTarWriter, optimized bool, snapshots bool, op *operations.Operation) error { return nil } +// CreateCustomVolumeFromBackup ... func (b *mockBackend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op *operations.Operation) error { return nil } +// CreateCustomVolumeFromISO ... func (b *mockBackend) CreateCustomVolumeFromISO(projectName string, volName string, srcData io.ReadSeeker, size int64, op *operations.Operation) error { return nil } diff --git a/lxd/storage/block/utils.go b/lxd/storage/block/utils.go new file mode 100644 index 000000000000..ba5e113ee2ec --- /dev/null +++ b/lxd/storage/block/utils.go @@ -0,0 +1,39 @@ +package block + +import ( + "os" + + "golang.org/x/sys/unix" + + "github.com/canonical/lxd/shared" +) + +// DiskSizeBytes returns the size of a block disk (path can be either block device or raw file). +func DiskSizeBytes(blockDiskPath string) (int64, error) { + if shared.IsBlockdevPath(blockDiskPath) { + // Attempt to open the device path. + f, err := os.Open(blockDiskPath) + if err != nil { + return -1, err + } + + defer func() { _ = f.Close() }() + fd := int(f.Fd()) + + // Retrieve the block device size. + res, err := unix.IoctlGetInt(fd, unix.BLKGETSIZE64) + if err != nil { + return -1, err + } + + return int64(res), nil + } + + // Block device is assumed to be a raw file. + fi, err := os.Lstat(blockDiskPath) + if err != nil { + return -1, err + } + + return fi.Size(), nil +} diff --git a/lxd/storage/drivers/driver_btrfs_utils.go b/lxd/storage/drivers/driver_btrfs_utils.go index 018e812d52fc..2b91af112b8d 100644 --- a/lxd/storage/drivers/driver_btrfs_utils.go +++ b/lxd/storage/drivers/driver_btrfs_utils.go @@ -1,37 +1,5 @@ package drivers -/* - -#include -#include -#include - -// definitions are borrowed from include/uapi/linux/btrfs.h - -#define BTRFS_IOCTL_MAGIC 0x94 -#define BTRFS_UUID_SIZE 16 - -struct btrfs_ioctl_timespec { - __u64 sec; - __u32 nsec; -}; - -struct btrfs_ioctl_received_subvol_args { - char uuid[BTRFS_UUID_SIZE]; - __u64 stransid; - __u64 rtransid; - struct btrfs_ioctl_timespec stime; - struct btrfs_ioctl_timespec rtime; - __u64 flags; - __u64 reserved[16]; -}; - -#define BTRFS_IOC_SET_RECEIVED_SUBVOL _IOWR(BTRFS_IOCTL_MAGIC, 37, \ - struct btrfs_ioctl_received_subvol_args) - -*/ -import "C" - import ( "bufio" "bytes" @@ -51,6 +19,7 @@ import ( "gopkg.in/yaml.v2" "github.com/canonical/lxd/lxd/backup" + "github.com/canonical/lxd/lxd/linux" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/ioprogress" @@ -93,7 +62,7 @@ func setReceivedUUID(path string, UUID string) error { copy(args.uuid[:], binUUID) - _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), C.BTRFS_IOC_SET_RECEIVED_SUBVOL, uintptr(unsafe.Pointer(&args))) + _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), linux.IoctlBtrfsSetReceivedSubvol, uintptr(unsafe.Pointer(&args))) if errno != 0 { return fmt.Errorf("Failed setting received UUID: %w", unix.Errno(errno)) } diff --git a/lxd/storage/drivers/driver_btrfs_volumes.go b/lxd/storage/drivers/driver_btrfs_volumes.go index 45eb02394dd2..0aef7f4b1209 100644 --- a/lxd/storage/drivers/driver_btrfs_volumes.go +++ b/lxd/storage/drivers/driver_btrfs_volumes.go @@ -20,6 +20,7 @@ import ( "github.com/canonical/lxd/lxd/instancewriter" "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/operations" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" @@ -1010,7 +1011,7 @@ func (d *btrfs) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, // Custom handling for filesystem volume associated with a VM. if vol.volType == VolumeTypeVM && shared.PathExists(filepath.Join(volPath, genericVolumeDiskFile)) { // Get the size of the VM image. - blockSize, err := BlockDiskSizeBytes(filepath.Join(volPath, genericVolumeDiskFile)) + blockSize, err := block.DiskSizeBytes(filepath.Join(volPath, genericVolumeDiskFile)) if err != nil { return err } @@ -1272,6 +1273,7 @@ func (d *btrfs) migrateVolumeOptimized(vol Volume, conn io.ReadWriteCloser, volS sentVols := 0 // Send volume (and any subvolumes if supported) to target. + //revive:disable:defer Allow defer inside a loop. for _, subVolume := range subvolumes { if subVolume.Snapshot != snapName { continue // Only sending subvolumes related to snapshot name (empty for main vol). diff --git a/lxd/storage/drivers/driver_ceph_volumes.go b/lxd/storage/drivers/driver_ceph_volumes.go index db146c015d29..d94b2b33773b 100644 --- a/lxd/storage/drivers/driver_ceph_volumes.go +++ b/lxd/storage/drivers/driver_ceph_volumes.go @@ -19,6 +19,7 @@ import ( "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/operations" "github.com/canonical/lxd/lxd/response" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" @@ -1318,7 +1319,7 @@ func (d *ceph) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, o defer func() { _ = d.rbdUnmapVolume(vol, true) }() } - oldSizeBytes, err := BlockDiskSizeBytes(devPath) + oldSizeBytes, err := block.DiskSizeBytes(devPath) if err != nil { return fmt.Errorf("Error getting current size: %w", err) } diff --git a/lxd/storage/drivers/driver_dir_volumes.go b/lxd/storage/drivers/driver_dir_volumes.go index a9e09dfff98b..a824286e36e1 100644 --- a/lxd/storage/drivers/driver_dir_volumes.go +++ b/lxd/storage/drivers/driver_dir_volumes.go @@ -12,6 +12,7 @@ import ( "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/operations" "github.com/canonical/lxd/lxd/rsync" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/lxd/storage/quota" "github.com/canonical/lxd/shared" @@ -347,7 +348,7 @@ func (d *dir) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, op volPath := vol.MountPath() if sizeBytes > 0 && vol.volType == VolumeTypeVM && shared.PathExists(filepath.Join(volPath, genericVolumeDiskFile)) { // Get the size of the VM image. - blockSize, err := BlockDiskSizeBytes(filepath.Join(volPath, genericVolumeDiskFile)) + blockSize, err := block.DiskSizeBytes(filepath.Join(volPath, genericVolumeDiskFile)) if err != nil { return err } diff --git a/lxd/storage/drivers/driver_powerflex_volumes.go b/lxd/storage/drivers/driver_powerflex_volumes.go index d3a4a5466572..571b1b890d1f 100644 --- a/lxd/storage/drivers/driver_powerflex_volumes.go +++ b/lxd/storage/drivers/driver_powerflex_volumes.go @@ -15,6 +15,7 @@ import ( "github.com/canonical/lxd/lxd/instancewriter" "github.com/canonical/lxd/lxd/migration" "github.com/canonical/lxd/lxd/operations" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" @@ -552,7 +553,7 @@ func (d *powerflex) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bo defer cleanup() - oldSizeBytes, err := BlockDiskSizeBytes(devPath) + oldSizeBytes, err := block.DiskSizeBytes(devPath) if err != nil { return fmt.Errorf("Error getting current size: %w", err) } diff --git a/lxd/storage/drivers/generic_vfs.go b/lxd/storage/drivers/generic_vfs.go index a4ea67d91f28..e86c9946fbcc 100644 --- a/lxd/storage/drivers/generic_vfs.go +++ b/lxd/storage/drivers/generic_vfs.go @@ -15,6 +15,7 @@ import ( "github.com/canonical/lxd/lxd/operations" "github.com/canonical/lxd/lxd/rsync" "github.com/canonical/lxd/lxd/state" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/filesystem" "github.com/canonical/lxd/lxd/sys" "github.com/canonical/lxd/shared" @@ -569,7 +570,7 @@ func genericVFSBackupVolume(d Driver, vol VolumeCopy, tarWriter *instancewriter. } // Get size of disk block device for tarball header. - blockDiskSize, err := BlockDiskSizeBytes(blockPath) + blockDiskSize, err := block.DiskSizeBytes(blockPath) if err != nil { return fmt.Errorf("Error getting block device size %q: %w", blockPath, err) } diff --git a/lxd/storage/drivers/utils.go b/lxd/storage/drivers/utils.go index 6460a9547dc6..5ca347b6c425 100644 --- a/lxd/storage/drivers/utils.go +++ b/lxd/storage/drivers/utils.go @@ -787,36 +787,6 @@ func ShiftZFSSkipper(dir string, absPath string, fi os.FileInfo) bool { return false } -// BlockDiskSizeBytes returns the size of a block disk (path can be either block device or raw file). -func BlockDiskSizeBytes(blockDiskPath string) (int64, error) { - if shared.IsBlockdevPath(blockDiskPath) { - // Attempt to open the device path. - f, err := os.Open(blockDiskPath) - if err != nil { - return -1, err - } - - defer func() { _ = f.Close() }() - fd := int(f.Fd()) - - // Retrieve the block device size. - res, err := unix.IoctlGetInt(fd, unix.BLKGETSIZE64) - if err != nil { - return -1, err - } - - return int64(res), nil - } - - // Block device is assumed to be a raw file. - fi, err := os.Lstat(blockDiskPath) - if err != nil { - return -1, err - } - - return fi.Size(), nil -} - // OperationLockName returns the storage specific lock name to use with locking package. func OperationLockName(operationName string, poolName string, volType VolumeType, contentType ContentType, volName string) string { return fmt.Sprintf("%s/%s/%s/%s/%s", operationName, poolName, volType, contentType, volName) diff --git a/lxd/storage/utils.go b/lxd/storage/utils.go index 668ad8a1c92c..5b264e178a9c 100644 --- a/lxd/storage/utils.go +++ b/lxd/storage/utils.go @@ -24,6 +24,7 @@ import ( "github.com/canonical/lxd/lxd/response" "github.com/canonical/lxd/lxd/rsync" "github.com/canonical/lxd/lxd/state" + "github.com/canonical/lxd/lxd/storage/block" "github.com/canonical/lxd/lxd/storage/drivers" "github.com/canonical/lxd/lxd/sys" "github.com/canonical/lxd/shared" @@ -762,7 +763,7 @@ func ImageUnpack(imageFile string, vol drivers.Volume, destBlockFile string, sys } if shared.PathExists(dstPath) { - volSizeBytes, err := drivers.BlockDiskSizeBytes(dstPath) + volSizeBytes, err := block.DiskSizeBytes(dstPath) if err != nil { return -1, fmt.Errorf("Error getting current size of %q: %w", dstPath, err) } @@ -1188,7 +1189,7 @@ func InstanceDiskBlockSize(pool Pool, inst instance.Instance, op *operations.Ope return -1, fmt.Errorf("No disk path available from mount") } - blockDiskSize, err := drivers.BlockDiskSizeBytes(mountInfo.DiskPath) + blockDiskSize, err := block.DiskSizeBytes(mountInfo.DiskPath) if err != nil { return -1, fmt.Errorf("Error getting block disk size %q: %w", mountInfo.DiskPath, err) } diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go index 37dcf965c771..57cfd91705a2 100644 --- a/lxd/storage_volumes.go +++ b/lxd/storage_volumes.go @@ -1221,16 +1221,16 @@ func doVolumeMigration(s *state.State, r *http.Request, requestProjectName strin // Initialise migrationArgs, don't set the Storage property yet, this is done in DoStorage, // to avoid this function relying on the legacy storage layer. migrationArgs := migrationSinkArgs{ - URL: req.Source.Operation, - Dialer: &websocket.Dialer{ + url: req.Source.Operation, + dialer: &websocket.Dialer{ TLSClientConfig: config, NetDialContext: shared.RFC3493Dialer, HandshakeTimeout: time.Second * 5, }, - Secrets: req.Source.Websockets, - Push: push, - VolumeOnly: req.Source.VolumeOnly, - Refresh: req.Source.Refresh, + secrets: req.Source.Websockets, + push: push, + volumeOnly: req.Source.VolumeOnly, + refresh: req.Source.Refresh, } sink, err := newStorageMigrationSink(&migrationArgs) diff --git a/lxd/sys/apparmor.go b/lxd/sys/apparmor.go index 0c82fa7f5a14..e54e6a44adba 100644 --- a/lxd/sys/apparmor.go +++ b/lxd/sys/apparmor.go @@ -86,6 +86,8 @@ func (s *OS) initAppArmor() []cluster.Warning { s.AppArmorConfined = true } + s.AppArmorFeatures.Map = map[string]bool{} + return dbWarnings } diff --git a/lxd/sys/os.go b/lxd/sys/os.go index 12588a97a5d2..3c18e5bbf6f0 100644 --- a/lxd/sys/os.go +++ b/lxd/sys/os.go @@ -38,6 +38,12 @@ type InotifyInfo struct { Targets map[string]*InotifyTargetInfo } +// AppArmorFeaturesInfo records the AppArmor features availability. +type AppArmorFeaturesInfo struct { + sync.Mutex + Map map[string]bool +} + // OS is a high-level facade for accessing all operating-system // level functionality that LXD uses. type OS struct { @@ -69,6 +75,7 @@ type OS struct { AppArmorConfined bool AppArmorStacked bool AppArmorStacking bool + AppArmorFeatures AppArmorFeaturesInfo // Cgroup features CGInfo cgroup.Info diff --git a/lxd/util/http.go b/lxd/util/http.go index b5ded988a21f..5b33f04907cf 100644 --- a/lxd/util/http.go +++ b/lxd/util/http.go @@ -200,7 +200,7 @@ func CheckCASignature(cert x509.Certificate, networkCert *shared.CertInfo) (trus err = crl.CheckSignatureFrom(ca) if err != nil { - logger.Error("Certificate revokation list has not been signed by server CA", logger.Ctx{"error": err}) + logger.Error("Certificate revokation list has not been signed by server CA", logger.Ctx{"err": err}) return false, false, "" } diff --git a/shared/api/instance.go b/shared/api/instance.go index e7855198055b..efb92359d942 100644 --- a/shared/api/instance.go +++ b/shared/api/instance.go @@ -6,7 +6,7 @@ import ( ) // GetParentAndSnapshotName returns the parent name, snapshot name, and whether it actually was a snapshot name. -func GetParentAndSnapshotName(name string) (string, string, bool) { +func GetParentAndSnapshotName(name string) (parentName string, snapshotName string, isSnapshot bool) { fields := strings.SplitN(name, "/", 2) if len(fields) == 1 { return name, "", false diff --git a/shared/simplestreams/products.go b/shared/simplestreams/products.go index 4013f59b9bb7..7939bef787f9 100644 --- a/shared/simplestreams/products.go +++ b/shared/simplestreams/products.go @@ -70,7 +70,8 @@ func (s *Products) ToLXD() ([]api.Image, map[string][][]string) { downloads := map[string][][]string{} images := []api.Image{} - nameLayout := "20060102" + nameLayoutLong := "20060102_0304" // Date and time. + nameLayoutShort := "20060102" // Date only. eolLayout := "2006-01-02" for _, product := range s.Products { @@ -91,9 +92,15 @@ func (s *Products) ToLXD() ([]api.Image, map[string][][]string) { continue } - creationDate, err := time.Parse(nameLayout, name[0:8]) + // Parse the creation date from the version name. First, check if the version contains + // both date and upload time. If it doesn't, try parsing just the date part. If the date + // cannot be fetched, skip that version instead of erroring out. + creationDate, err := time.Parse(nameLayoutLong, name) if err != nil { - continue + creationDate, err = time.Parse(nameLayoutShort, name[0:8]) + if err != nil { + continue + } } // Image processing function diff --git a/shared/util_linux.go b/shared/util_linux.go index f1b8ac40d433..17288ac5e353 100644 --- a/shared/util_linux.go +++ b/shared/util_linux.go @@ -25,11 +25,13 @@ import ( // --- pure Go functions --- +// GetFileStat retrieves the UID, GID, major and minor device numbers, inode, and number of hard links for +// the given file path. func GetFileStat(p string) (uid int, gid int, major uint32, minor uint32, inode uint64, nlink int, err error) { var stat unix.Stat_t err = unix.Lstat(p, &stat) if err != nil { - return + return 0, 0, 0, 0, 0, 0, err } uid = int(stat.Uid) @@ -41,7 +43,7 @@ func GetFileStat(p string) (uid int, gid int, major uint32, minor uint32, inode minor = unix.Minor(uint64(stat.Rdev)) } - return + return uid, gid, major, minor, inode, nlink, nil } // GetPathMode returns a os.FileMode for the provided path. @@ -55,6 +57,7 @@ func GetPathMode(path string) (os.FileMode, error) { return mode, nil } +// SetSize sets the terminal size to the specified width and height for the given file descriptor. func SetSize(fd int, width int, height int) (err error) { var dimensions [4]uint16 dimensions[0] = uint16(height) @@ -94,8 +97,10 @@ func GetAllXattr(path string) (map[string]string, error) { return xattrs, nil } -var ObjectFound = fmt.Errorf("Found requested object") +// ErrObjectFound indicates that the requested object was found. +var ErrObjectFound = fmt.Errorf("Found requested object") +// LookupUUIDByBlockDevPath finds and returns the UUID of a block device by its path. func LookupUUIDByBlockDevPath(diskDevice string) (string, error) { uuid := "" readUUID := func(path string, info os.FileInfo, err error) error { @@ -117,14 +122,14 @@ func LookupUUIDByBlockDevPath(diskDevice string) (string, error) { uuid = path // Will allows us to avoid needlessly travers // the whole directory. - return ObjectFound + return ErrObjectFound } } return nil } err := filepath.Walk("/dev/disk/by-uuid", readUUID) - if err != nil && err != ObjectFound { + if err != nil && err != ErrObjectFound { return "", fmt.Errorf("Failed to detect UUID: %s", err) } @@ -136,7 +141,9 @@ func LookupUUIDByBlockDevPath(diskDevice string) (string, error) { return uuid[lastSlash+1:], nil } -// Detect whether err is an errno. +// GetErrno detects whether the error is an errno. +// +//revive:disable:error-return Error is returned first because this is similar to assertion. func GetErrno(err error) (errno error, iserrno bool) { sysErr, ok := err.(*os.SyscallError) if ok { @@ -220,10 +227,12 @@ func intArrayToString(arr any) string { return s } +// DeviceTotalMemory returns the total memory of the device by reading /proc/meminfo. func DeviceTotalMemory() (int64, error) { return GetMeminfo("MemTotal") } +// GetMeminfo retrieves the memory information for the specified field from /proc/meminfo. func GetMeminfo(field string) (int64, error) { // Open /proc/meminfo f, err := os.Open("/proc/meminfo") @@ -260,7 +269,7 @@ func GetMeminfo(field string) (int64, error) { } // OpenPtyInDevpts creates a new PTS pair, configures them and returns them. -func OpenPtyInDevpts(devpts_fd int, uid, gid int64) (*os.File, *os.File, error) { +func OpenPtyInDevpts(devptsFD int, uid, gid int64) (*os.File, *os.File, error) { revert := revert.New() defer revert.Fail() var fd int @@ -268,8 +277,8 @@ func OpenPtyInDevpts(devpts_fd int, uid, gid int64) (*os.File, *os.File, error) var err error // Create a PTS pair. - if devpts_fd >= 0 { - fd, err = unix.Openat(devpts_fd, "ptmx", unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0) + if devptsFD >= 0 { + fd, err = unix.Openat(devptsFD, "ptmx", unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0) } else { fd, err = unix.Openat(-1, "/dev/ptmx", unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0) } @@ -301,7 +310,7 @@ func OpenPtyInDevpts(devpts_fd int, uid, gid int64) (*os.File, *os.File, error) pty = os.NewFile(ptyFd, fmt.Sprintf("/dev/pts/%d", id)) } else { - if devpts_fd >= 0 { + if devptsFD >= 0 { return nil, nil, fmt.Errorf("TIOCGPTPEER required but not available") } @@ -401,7 +410,7 @@ func ExitStatus(err error) (int, error) { } // GetPollRevents poll for events on provided fd. -func GetPollRevents(fd int, timeout int, flags int) (int, int, error) { +func GetPollRevents(fd int, timeout int, flags int) (n int, revents int, err error) { pollFd := unix.PollFd{ Fd: int32(fd), Events: int16(flags), @@ -411,7 +420,7 @@ func GetPollRevents(fd int, timeout int, flags int) (int, int, error) { pollFds := []unix.PollFd{pollFd} again: - n, err := unix.Poll(pollFds, timeout) + n, err = unix.Poll(pollFds, timeout) if err != nil { if err == unix.EAGAIN || err == unix.EINTR { goto again @@ -498,10 +507,12 @@ func (w *execWrapper) Read(p []byte) (int, error) { return n, opErr } +// Write writes data to the underlying os.File. func (w *execWrapper) Write(p []byte) (int, error) { return w.f.Write(p) } +// Close closes the underlying os.File. func (w *execWrapper) Close() error { return w.f.Close() } diff --git a/test/backends/btrfs.sh b/test/backends/btrfs.sh index bbc40e61a135..829728094034 100644 --- a/test/backends/btrfs.sh +++ b/test/backends/btrfs.sh @@ -1,5 +1,4 @@ btrfs_setup() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -8,7 +7,6 @@ btrfs_setup() { } btrfs_configure() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -20,7 +18,6 @@ btrfs_configure() { } btrfs_teardown() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 diff --git a/test/backends/ceph.sh b/test/backends/ceph.sh index 4d5511f33e93..2874693a180c 100644 --- a/test/backends/ceph.sh +++ b/test/backends/ceph.sh @@ -1,5 +1,4 @@ ceph_setup() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -8,7 +7,6 @@ ceph_setup() { } ceph_configure() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -20,7 +18,6 @@ ceph_configure() { } ceph_teardown() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 diff --git a/test/backends/dir.sh b/test/backends/dir.sh index 4a8dfffe2568..c0d4a4d21c02 100644 --- a/test/backends/dir.sh +++ b/test/backends/dir.sh @@ -4,7 +4,6 @@ # Any necessary backend-specific setup dir_setup() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -14,7 +13,6 @@ dir_setup() { # Do the API voodoo necessary to configure LXD to use this backend dir_configure() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -26,7 +24,6 @@ dir_configure() { } dir_teardown() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 diff --git a/test/backends/lvm.sh b/test/backends/lvm.sh index 9ff4231855c6..3001547ff194 100644 --- a/test/backends/lvm.sh +++ b/test/backends/lvm.sh @@ -1,5 +1,4 @@ lvm_setup() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -8,7 +7,6 @@ lvm_setup() { } lvm_configure() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -20,7 +18,6 @@ lvm_configure() { } lvm_teardown() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 diff --git a/test/backends/zfs.sh b/test/backends/zfs.sh index 5e55f0160c13..a9d9c25730df 100644 --- a/test/backends/zfs.sh +++ b/test/backends/zfs.sh @@ -1,5 +1,4 @@ zfs_setup() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -8,7 +7,6 @@ zfs_setup() { } zfs_configure() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 @@ -20,7 +18,6 @@ zfs_configure() { } zfs_teardown() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_DIR=$1 diff --git a/test/godeps/lxd-agent.list b/test/godeps/lxd-agent.list index 795beb686a30..137b7543a2e3 100644 --- a/test/godeps/lxd-agent.list +++ b/test/godeps/lxd-agent.list @@ -241,6 +241,7 @@ google.golang.org/grpc/backoff google.golang.org/grpc/balancer google.golang.org/grpc/balancer/base google.golang.org/grpc/balancer/grpclb/state +google.golang.org/grpc/balancer/pickfirst google.golang.org/grpc/balancer/roundrobin google.golang.org/grpc/binarylog/grpc_binarylog_v1 google.golang.org/grpc/channelz @@ -261,7 +262,6 @@ google.golang.org/grpc/internal/channelz google.golang.org/grpc/internal/credentials google.golang.org/grpc/internal/envconfig google.golang.org/grpc/internal/grpclog -google.golang.org/grpc/internal/grpcrand google.golang.org/grpc/internal/grpcsync google.golang.org/grpc/internal/grpcutil google.golang.org/grpc/internal/idle diff --git a/test/includes/check.sh b/test/includes/check.sh index ac3d0b33a7b5..3e2f4d1ce6a0 100644 --- a/test/includes/check.sh +++ b/test/includes/check.sh @@ -1,7 +1,6 @@ # Miscellaneous test checks. check_dependencies() { - # shellcheck disable=SC2039,3043 local dep missing missing="" diff --git a/test/includes/clustering.sh b/test/includes/clustering.sh index cd2fda907d06..15f8a8fad4d8 100644 --- a/test/includes/clustering.sh +++ b/test/includes/clustering.sh @@ -109,7 +109,6 @@ teardown_clustering_netns() { } spawn_lxd_and_bootstrap_cluster() { - # shellcheck disable=SC2039,SC3043 local LXD_NETNS set -e @@ -199,7 +198,6 @@ EOF } spawn_lxd_and_join_cluster() { - # shellcheck disable=SC2039,SC3043 local LXD_NETNS set -e @@ -386,7 +384,7 @@ EOF } respawn_lxd_cluster_member() { - # shellcheck disable=SC2039,SC2034,SC3043 + # shellcheck disable=SC2034 local LXD_NETNS set -e diff --git a/test/includes/lxc.sh b/test/includes/lxc.sh index 3fa0f8c34adb..901b82e68184 100644 --- a/test/includes/lxc.sh +++ b/test/includes/lxc.sh @@ -7,7 +7,6 @@ lxc() { lxc_remote() { set +x - # shellcheck disable=SC2039,3043 local injected cmd arg injected=0 diff --git a/test/includes/lxd.sh b/test/includes/lxd.sh index 752e22830854..91f5c7ca459f 100644 --- a/test/includes/lxd.sh +++ b/test/includes/lxd.sh @@ -5,7 +5,6 @@ spawn_lxd() { # LXD_DIR is local here because since $(lxc) is actually a function, it # overwrites the environment and we would lose LXD_DIR's value otherwise. - # shellcheck disable=2039,3043 local LXD_DIR lxddir lxd_backend lxddir=${1} @@ -37,7 +36,7 @@ spawn_lxd() { LXD_DIR="${lxddir}" lxd --logfile "${lxddir}/lxd.log" "${DEBUG-}" "$@" 2>&1 & else # shellcheck disable=SC2153 - pid="$(cat "${TEST_DIR}/ns/${LXD_NETNS}/PID")" + read -r pid < "${TEST_DIR}/ns/${LXD_NETNS}/PID" LXD_DIR="${lxddir}" nsenter -n -m -t "${pid}" lxd --logfile "${lxddir}/lxd.log" "${DEBUG-}" "$@" 2>&1 & fi LXD_PID=$! @@ -82,7 +81,6 @@ respawn_lxd() { # LXD_DIR is local here because since $(lxc) is actually a function, it # overwrites the environment and we would lose LXD_DIR's value otherwise. - # shellcheck disable=2039,3043 local LXD_DIR lxddir=${1} @@ -96,7 +94,7 @@ respawn_lxd() { if [ "${LXD_NETNS}" = "" ]; then LXD_DIR="${lxddir}" lxd --logfile "${lxddir}/lxd.log" "${DEBUG-}" "$@" 2>&1 & else - pid="$(cat "${TEST_DIR}/ns/${LXD_NETNS}/PID")" + read -r pid < "${TEST_DIR}/ns/${LXD_NETNS}/PID" LXD_DIR="${lxddir}" nsenter -n -m -t "${pid}" lxd --logfile "${lxddir}/lxd.log" "${DEBUG-}" "$@" 2>&1 & fi LXD_PID=$! @@ -117,7 +115,6 @@ kill_lxd() { # LXD_DIR is local here because since $(lxc) is actually a function, it # overwrites the environment and we would lose LXD_DIR's value otherwise. - # shellcheck disable=2039,3043 local LXD_DIR daemon_dir daemon_pid check_leftovers lxd_backend daemon_dir=${1} @@ -210,11 +207,7 @@ kill_lxd() { rm -f "${daemon_dir}/containers/lxc-monitord.log" # Support AppArmor policy cache directory - if apparmor_parser --help | grep -q -- '--print-cache.dir'; then - apparmor_cache_dir="$(apparmor_parser -L "${daemon_dir}"/security/apparmor/cache --print-cache-dir)" - else - apparmor_cache_dir="${daemon_dir}/security/apparmor/cache" - fi + apparmor_cache_dir="$(apparmor_parser -L "${daemon_dir}"/security/apparmor/cache --print-cache-dir)" rm -f "${apparmor_cache_dir}/.features" check_empty "${daemon_dir}/containers/" check_empty "${daemon_dir}/devices/" @@ -265,7 +258,6 @@ shutdown_lxd() { # LXD_DIR is local here because since $(lxc) is actually a function, it # overwrites the environment and we would lose LXD_DIR's value otherwise. - # shellcheck disable=2039,3043 local LXD_DIR daemon_dir=${1} @@ -283,7 +275,6 @@ shutdown_lxd() { } wait_for() { - # shellcheck disable=SC2039,3043 local addr op addr=${1} @@ -300,7 +291,6 @@ wipe() { fi fi - # shellcheck disable=SC2039,3043 local pid # shellcheck disable=SC2009 ps aux | grep lxc-monitord | grep "${1}" | awk '{print $2}' | while read -r pid; do @@ -316,7 +306,6 @@ wipe() { # Kill and cleanup LXD instances and related resources cleanup_lxds() { - # shellcheck disable=SC2039,3043 local test_dir daemon_dir test_dir="$1" diff --git a/test/includes/net.sh b/test/includes/net.sh index 448655c419fd..4f9ad546297d 100644 --- a/test/includes/net.sh +++ b/test/includes/net.sh @@ -13,7 +13,6 @@ EOF return fi - # shellcheck disable=SC2039,3043 local port pid while true; do diff --git a/test/includes/setup.sh b/test/includes/setup.sh index e3430e5b58c9..83c90d902bb4 100644 --- a/test/includes/setup.sh +++ b/test/includes/setup.sh @@ -1,7 +1,6 @@ # Test setup helper functions. ensure_has_localhost_remote() { - # shellcheck disable=SC2039,3043 local addr="${1}" if ! lxc remote list | grep -q "localhost"; then lxc remote add localhost "https://${addr}" --accept-certificate --password foo diff --git a/test/includes/storage.sh b/test/includes/storage.sh index 5189223c1d56..9cb4fa98e5ab 100644 --- a/test/includes/storage.sh +++ b/test/includes/storage.sh @@ -2,7 +2,6 @@ # Whether a storage backend is available storage_backend_available() { - # shellcheck disable=2039,3043 local backends backends="$(available_storage_backends)" if [ "${backends#*"$1"}" != "$backends" ]; then @@ -16,6 +15,13 @@ storage_backend_available() { false } +# Returns 0 if --optimized-storage works for backups (export/import) +storage_backend_optimized_backup() { + [ "${1}" = "btrfs" ] && return 0 + [ "${1}" = "zfs" ] && return 0 + return 1 +} + # Choose a random available backend, excluding LXD_BACKEND random_storage_backend() { # shellcheck disable=2046 @@ -24,12 +30,11 @@ random_storage_backend() { # Return the storage backend being used by a LXD instance storage_backend() { - cat "$1/lxd.backend" + read -r backend < "$1/lxd.backend" && echo "${backend}" } # Return a list of available storage backends available_storage_backends() { - # shellcheck disable=2039,3043 local backend backends storage_backends backends="dir" # always available @@ -49,7 +54,6 @@ available_storage_backends() { } import_storage_backends() { - # shellcheck disable=SC2039,3043 local backend for backend in $(available_storage_backends); do # shellcheck disable=SC1090 @@ -58,7 +62,6 @@ import_storage_backends() { } configure_loop_device() { - # shellcheck disable=SC2039,3043 local lv_loop_file pvloopdev # shellcheck disable=SC2153 @@ -75,17 +78,13 @@ configure_loop_device() { # The following code enables to return a value from a shell function by # calling the function as: fun VAR1 - # shellcheck disable=2039,3043 local __tmp1="${1}" - # shellcheck disable=2039,3043 local res1="${lv_loop_file}" if [ "${__tmp1}" ]; then eval "${__tmp1}='${res1}'" fi - # shellcheck disable=2039,3043 local __tmp2="${2}" - # shellcheck disable=2039,3043 local res2="${pvloopdev}" if [ "${__tmp2}" ]; then eval "${__tmp2}='${res2}'" @@ -93,7 +92,6 @@ configure_loop_device() { } deconfigure_loop_device() { - # shellcheck disable=SC2039,3043 local lv_loop_file loopdev success lv_loop_file="${1}" loopdev="${2}" @@ -122,7 +120,6 @@ deconfigure_loop_device() { } umount_loops() { - # shellcheck disable=SC2039,3043 local line test_dir test_dir="$1" diff --git a/test/lint/auth-up-to-date.sh b/test/lint/auth-up-to-date.sh index 972b23650e82..be13938222f0 100755 --- a/test/lint/auth-up-to-date.sh +++ b/test/lint/auth-up-to-date.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu f="lxd/auth/entitlements_generated.go" diff --git a/test/lint/golangci.sh b/test/lint/golangci.sh index 06df8ff78c28..1b183d69aa55 100755 --- a/test/lint/golangci.sh +++ b/test/lint/golangci.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu # Default target branch. target_branch="main" diff --git a/test/lint/i18n-up-to-date.sh b/test/lint/i18n-up-to-date.sh index 4af412c953d3..d45c8d7cc4da 100755 --- a/test/lint/i18n-up-to-date.sh +++ b/test/lint/i18n-up-to-date.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu safe_pot_hash() { sed -e "/Project-Id-Version/,/Content-Transfer-Encoding/d" -e "/^#/d" "po/lxd.pot" | md5sum | cut -f1 -d" " diff --git a/test/lint/licenses.sh b/test/lint/licenses.sh index e79aed1a31e9..fe06a022d925 100755 --- a/test/lint/licenses.sh +++ b/test/lint/licenses.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu # Check LXD doesn't include non-permissive licenses (except for itself). mv COPYING COPYING.tmp diff --git a/test/lint/metadata-up-to-date.sh b/test/lint/metadata-up-to-date.sh index 700a0552c7da..6744d0d571d9 100755 --- a/test/lint/metadata-up-to-date.sh +++ b/test/lint/metadata-up-to-date.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu hash_before="lxd/metadata-before.txt" hash_after="lxd/metadata-after.txt" diff --git a/test/lint/mixed-whitespace.sh b/test/lint/mixed-whitespace.sh index 8d329561d949..1f950bf32d29 100755 --- a/test/lint/mixed-whitespace.sh +++ b/test/lint/mixed-whitespace.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking for mixed tabs and spaces in shell scripts..." diff --git a/test/lint/negated-is-bool.sh b/test/lint/negated-is-bool.sh index f70d27e1063b..8aeec354966b 100755 --- a/test/lint/negated-is-bool.sh +++ b/test/lint/negated-is-bool.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking usage of negated shared.Is(True|False)*() functions..." diff --git a/test/lint/newline-after-block.sh b/test/lint/newline-after-block.sh index 21972bc50d6c..f37020a79a73 100755 --- a/test/lint/newline-after-block.sh +++ b/test/lint/newline-after-block.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking that functional blocks are followed by newlines..." diff --git a/test/lint/no-oneline-assign-and-test.sh b/test/lint/no-oneline-assign-and-test.sh index c30937d09aec..d180a955a4d2 100755 --- a/test/lint/no-oneline-assign-and-test.sh +++ b/test/lint/no-oneline-assign-and-test.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking for oneline assign & test..." diff --git a/test/lint/no-short-form-imports.sh b/test/lint/no-short-form-imports.sh index 5bf4688d8d8c..9cfbca56f375 100755 --- a/test/lint/no-short-form-imports.sh +++ b/test/lint/no-short-form-imports.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking for short form imports..." diff --git a/test/lint/rest-api-up-to-date.sh b/test/lint/rest-api-up-to-date.sh index f52fd54a5b10..bf3c5874b0e2 100755 --- a/test/lint/rest-api-up-to-date.sh +++ b/test/lint/rest-api-up-to-date.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu f="doc/rest-api.yaml" diff --git a/test/lint/trailing-space.sh b/test/lint/trailing-space.sh index fa44e1c06955..b902fbb645c7 100755 --- a/test/lint/trailing-space.sh +++ b/test/lint/trailing-space.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu echo "Checking that there are no trailing spaces in shell scripts..." diff --git a/test/main.sh b/test/main.sh index 97b8525b4ed6..52209d29c54e 100755 --- a/test/main.sh +++ b/test/main.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu [ -n "${GOPATH:-}" ] && export "PATH=${GOPATH}/bin:${PATH}" # Don't translate lxc output for parsing in it in tests. @@ -29,7 +30,6 @@ LXD_NETNS="" import_subdir_files() { test "$1" - # shellcheck disable=SC2039,3043 local file for file in "$1"/*.sh; do # shellcheck disable=SC1090 @@ -158,7 +158,6 @@ run_test() { echo "==> TEST BEGIN: ${TEST_CURRENT_DESCRIPTION}" START_TIME=$(date +%s) - # shellcheck disable=SC2039,3043 local skip=false # Skip test if requested. @@ -288,6 +287,7 @@ if [ "${1:-"all"}" != "cluster" ]; then run_test test_container_devices_nic_ipvlan "container devices - nic - ipvlan" run_test test_container_devices_nic_sriov "container devices - nic - sriov" run_test test_container_devices_nic_routed "container devices - nic - routed" + run_test test_container_devices_none "container devices - none" run_test test_container_devices_infiniband_physical "container devices - infiniband - physical" run_test test_container_devices_infiniband_sriov "container devices - infiniband - sriov" run_test test_container_devices_proxy "container devices - proxy" diff --git a/test/perf.sh b/test/perf.sh index 2aaa4764a924..ee7944874c9e 100755 --- a/test/perf.sh +++ b/test/perf.sh @@ -1,4 +1,5 @@ -#!/bin/sh -eu +#!/bin/bash +set -eu # # Performance tests runner # @@ -12,7 +13,6 @@ LXD_NETNS="" import_subdir_files() { test "$1" - # shellcheck disable=SC2039,3043 local file for file in "$1"/*.sh; do # shellcheck disable=SC1090 @@ -27,7 +27,6 @@ log_message() { } run_benchmark() { - # shellcheck disable=SC2039,3043 local label description label="$1" description="$2" diff --git a/test/suites/backup.sh b/test/suites/backup.sh index cef45ef4f4e4..fb23360d8dd5 100644 --- a/test/suites/backup.sh +++ b/test/suites/backup.sh @@ -371,7 +371,7 @@ _backup_import_with_project() { # container only # create backup - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc export c1 "${LXD_DIR}/c1-optimized.tar.gz" --optimized-storage --instance-only fi @@ -384,7 +384,7 @@ _backup_import_with_project() { lxc start c1 lxc delete --force c1 - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc import "${LXD_DIR}/c1-optimized.tar.gz" lxc info c1 lxc start c1 @@ -393,7 +393,7 @@ _backup_import_with_project() { # with snapshots - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc export c2 "${LXD_DIR}/c2-optimized.tar.gz" --optimized-storage fi @@ -442,7 +442,7 @@ _backup_import_with_project() { lxc delete --force c3 - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc import "${LXD_DIR}/c2-optimized.tar.gz" lxc import "${LXD_DIR}/c2-optimized.tar.gz" c3 lxc info c2 | grep snap0 @@ -489,13 +489,13 @@ _backup_import_with_project() { lxc export c3 "${LXD_DIR}/c3.tar.gz" # Remove container and storage pool - lxc rm -f c3 + lxc delete -f c3 lxc storage delete pool_1 # This should succeed as it will fall back on the default pool lxc import "${LXD_DIR}/c3.tar.gz" - lxc rm -f c3 + lxc delete -f c3 # Remove root device lxc profile device remove default root @@ -509,13 +509,16 @@ _backup_import_with_project() { # Specify pool explicitly lxc import "${LXD_DIR}/c3.tar.gz" -s pool_2 - lxc rm -f c3 + lxc delete -f c3 # Reset default storage pool lxc profile device add default root disk path=/ pool="${default_pool}" lxc storage delete pool_2 + # Cleanup exported tarballs + rm -f "${LXD_DIR}"/c*.tar.gz + if [ "$#" -ne 0 ]; then lxc image rm testimage lxc image rm testimage --project "$project-b" @@ -557,10 +560,11 @@ _backup_export_with_project() { # container only - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc export c1 "${LXD_DIR}/c1-optimized.tar.gz" --optimized-storage --instance-only tar -xzf "${LXD_DIR}/c1-optimized.tar.gz" -C "${LXD_DIR}/optimized" + ls -l "${LXD_DIR}/optimized/backup/" [ -f "${LXD_DIR}/optimized/backup/index.yaml" ] [ -f "${LXD_DIR}/optimized/backup/container.bin" ] [ ! -d "${LXD_DIR}/optimized/backup/snapshots" ] @@ -570,6 +574,7 @@ _backup_export_with_project() { tar -xzf "${LXD_DIR}/c1.tar.gz" -C "${LXD_DIR}/non-optimized" # check tarball content + ls -l "${LXD_DIR}/non-optimized/backup/" [ -f "${LXD_DIR}/non-optimized/backup/index.yaml" ] [ -d "${LXD_DIR}/non-optimized/backup/container" ] [ ! -d "${LXD_DIR}/non-optimized/backup/snapshots" ] @@ -577,11 +582,11 @@ _backup_export_with_project() { rm -rf "${LXD_DIR}/non-optimized/"* "${LXD_DIR}/optimized/"* # with snapshots - - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc export c1 "${LXD_DIR}/c1-optimized.tar.gz" --optimized-storage tar -xzf "${LXD_DIR}/c1-optimized.tar.gz" -C "${LXD_DIR}/optimized" + ls -l "${LXD_DIR}/optimized/backup/" [ -f "${LXD_DIR}/optimized/backup/index.yaml" ] [ -f "${LXD_DIR}/optimized/backup/container.bin" ] [ -f "${LXD_DIR}/optimized/backup/snapshots/snap0.bin" ] @@ -591,6 +596,7 @@ _backup_export_with_project() { tar -xzf "${LXD_DIR}/c1.tar.gz" -C "${LXD_DIR}/non-optimized" # check tarball content + ls -l "${LXD_DIR}/non-optimized/backup/" [ -f "${LXD_DIR}/non-optimized/backup/index.yaml" ] [ -d "${LXD_DIR}/non-optimized/backup/container" ] [ -d "${LXD_DIR}/non-optimized/backup/snapshots/snap0" ] @@ -606,6 +612,9 @@ _backup_export_with_project() { lxc delete --force c1-foo + # Cleanup exported tarballs + rm -f "${LXD_DIR}"/c*.tar.gz + if [ "$#" -ne 0 ]; then lxc image rm testimage lxc project switch default @@ -716,7 +725,7 @@ _backup_volume_export_with_project() { lxc storage volume snapshot "${custom_vol_pool}" testvol test-snap1 lxc storage volume set "${custom_vol_pool}" testvol user.foo=post-test-snap1 - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then # Create optimized backup without snapshots. lxc storage volume export "${custom_vol_pool}" testvol "${LXD_DIR}/testvol-optimized.tar.gz" --volume-only --optimized-storage @@ -725,6 +734,7 @@ _backup_volume_export_with_project() { # Extract backup tarball. tar -xzf "${LXD_DIR}/testvol-optimized.tar.gz" -C "${LXD_DIR}/optimized" + ls -l "${LXD_DIR}/optimized/backup/" [ -f "${LXD_DIR}/optimized/backup/index.yaml" ] [ -f "${LXD_DIR}/optimized/backup/volume.bin" ] [ ! -d "${LXD_DIR}/optimized/backup/volume-snapshots" ] @@ -739,6 +749,7 @@ _backup_volume_export_with_project() { tar -xzf "${LXD_DIR}/testvol.tar.gz" -C "${LXD_DIR}/non-optimized" # Check tarball content. + ls -l "${LXD_DIR}/non-optimized/backup/" [ -f "${LXD_DIR}/non-optimized/backup/index.yaml" ] [ -d "${LXD_DIR}/non-optimized/backup/volume" ] [ "$(cat "${LXD_DIR}/non-optimized/backup/volume/test")" = "bar" ] @@ -749,7 +760,7 @@ _backup_volume_export_with_project() { rm -rf "${LXD_DIR}/non-optimized/"* rm "${LXD_DIR}/testvol.tar.gz" - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then # Create optimized backup with snapshots. lxc storage volume export "${custom_vol_pool}" testvol "${LXD_DIR}/testvol-optimized.tar.gz" --optimized-storage @@ -758,6 +769,7 @@ _backup_volume_export_with_project() { # Extract backup tarball. tar -xzf "${LXD_DIR}/testvol-optimized.tar.gz" -C "${LXD_DIR}/optimized" + ls -l "${LXD_DIR}/optimized/backup/" [ -f "${LXD_DIR}/optimized/backup/index.yaml" ] [ -f "${LXD_DIR}/optimized/backup/volume.bin" ] [ -f "${LXD_DIR}/optimized/backup/volume-snapshots/test-snap0.bin" ] @@ -772,6 +784,7 @@ _backup_volume_export_with_project() { tar -xzf "${LXD_DIR}/testvol.tar.gz" -C "${LXD_DIR}/non-optimized" # Check tarball content. + ls -l "${LXD_DIR}/non-optimized/backup/" [ -f "${LXD_DIR}/non-optimized/backup/index.yaml" ] [ -d "${LXD_DIR}/non-optimized/backup/volume" ] [ "$(cat "${LXD_DIR}/non-optimized/backup/volume/test")" = "bar" ] @@ -822,7 +835,7 @@ _backup_volume_export_with_project() { fi # Test optimized import. - if [ "$lxd_backend" = "btrfs" ] || [ "$lxd_backend" = "zfs" ]; then + if storage_backend_optimized_backup "$lxd_backend"; then lxc storage volume detach "${custom_vol_pool}" testvol c1 lxc storage volume detach "${custom_vol_pool}" testvol2 c1 lxc storage volume delete "${custom_vol_pool}" testvol @@ -856,7 +869,7 @@ _backup_volume_export_with_project() { lxc storage volume detach "${custom_vol_pool}" testvol2 c1 lxc storage volume rm "${custom_vol_pool}" testvol lxc storage volume rm "${custom_vol_pool}" testvol2 - lxc rm -f c1 + lxc delete -f c1 rmdir "${LXD_DIR}/optimized" rmdir "${LXD_DIR}/non-optimized" @@ -951,6 +964,9 @@ test_backup_different_instance_uuid() { fi lxc delete -f c1 + + # Cleanup exported tarballs + rm -f "${LXD_DIR}"/c*.tar.gz } test_backup_volume_expiry() { @@ -1008,7 +1024,7 @@ yes EOF # Remove recovered instance. - lxc rm -f c2 + lxc delete -f c2 ) } diff --git a/test/suites/basic.sh b/test/suites/basic.sh index 5aa9a5c5f4ab..a7c3b22e4f06 100644 --- a/test/suites/basic.sh +++ b/test/suites/basic.sh @@ -1,5 +1,4 @@ test_basic_usage() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/clustering.sh b/test/suites/clustering.sh index d892ab4b89b2..b780f6d46f7e 100644 --- a/test/suites/clustering.sh +++ b/test/suites/clustering.sh @@ -1,5 +1,4 @@ test_clustering_enable() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) @@ -179,7 +178,6 @@ test_clustering_enable() { } test_clustering_membership() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -384,7 +382,6 @@ test_clustering_membership() { } test_clustering_containers() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -570,7 +567,6 @@ test_clustering_containers() { } test_clustering_storage() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -977,7 +973,6 @@ test_clustering_storage() { # two-stage process required multi-node clusters, or directly with the normal # procedure for non-clustered daemons. test_clustering_storage_single_node() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1049,7 +1044,6 @@ test_clustering_storage_single_node() { } test_clustering_network() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1249,7 +1243,6 @@ test_clustering_network() { # Perform an upgrade of a 2-member cluster, then a join a third member and # perform one more upgrade test_clustering_upgrade() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_NETNS setup_clustering_bridge @@ -1345,7 +1338,6 @@ test_clustering_upgrade() { # Perform an upgrade of an 8-member cluster. test_clustering_upgrade_large() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_NETNS N setup_clustering_bridge @@ -1401,7 +1393,6 @@ test_clustering_upgrade_large() { } test_clustering_publish() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1453,7 +1444,6 @@ test_clustering_publish() { } test_clustering_profiles() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1527,7 +1517,6 @@ EOF } test_clustering_update_cert() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1606,7 +1595,6 @@ test_clustering_update_cert() { } test_clustering_update_cert_reversion() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1700,7 +1688,7 @@ test_clustering_update_cert_reversion() { } test_clustering_join_api() { - # shellcheck disable=2039,2034,3043 + # shellcheck disable=SC2034 local LXD_DIR LXD_NETNS setup_clustering_bridge @@ -1740,7 +1728,6 @@ test_clustering_join_api() { } test_clustering_shutdown_nodes() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1812,7 +1799,6 @@ test_clustering_shutdown_nodes() { } test_clustering_projects() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1877,7 +1863,6 @@ test_clustering_projects() { } test_clustering_address() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -1955,7 +1940,6 @@ test_clustering_address() { } test_clustering_image_replication() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -2118,12 +2102,12 @@ test_clustering_image_replication() { } test_clustering_dns() { - # shellcheck disable=2039,3043 - local LXD_DIR + local lxdDir # Because we do not want tests to only run on Ubuntu (due to cluster's fan network dependency) # instead we will just spawn forkdns directly and check DNS resolution. + # XXX: make a copy of the global LXD_DIR # shellcheck disable=SC2031 lxdDir="${LXD_DIR}" prefix="lxd$$" @@ -2203,7 +2187,7 @@ test_clustering_dns() { } test_clustering_recover() { - # shellcheck disable=2039,2034,3043 + # shellcheck disable=SC2034 local LXD_DIR setup_clustering_bridge @@ -2285,7 +2269,7 @@ test_clustering_recover() { # When a voter cluster member is shutdown, its role gets transferred to a spare # node. test_clustering_handover() { - # shellcheck disable=2039,2034,3043 + # shellcheck disable=SC2034 local LXD_DIR setup_clustering_bridge @@ -2403,7 +2387,7 @@ test_clustering_handover() { # If a voter node crashes and is detected as offline, its role is migrated to a # stand-by. test_clustering_rebalance() { - # shellcheck disable=2039,2034,3043 + # shellcheck disable=SC2034 local LXD_DIR setup_clustering_bridge @@ -2492,7 +2476,6 @@ test_clustering_rebalance() { # Recover a cluster where a raft node was removed from the nodes table but not # from the raft configuration. test_clustering_remove_raft_node() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -2613,7 +2596,6 @@ test_clustering_remove_raft_node() { } test_clustering_failure_domains() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -2717,7 +2699,6 @@ test_clustering_failure_domains() { } test_clustering_image_refresh() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -2947,7 +2928,6 @@ test_clustering_image_refresh() { } test_clustering_evacuation() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3096,7 +3076,6 @@ test_clustering_evacuation() { } test_clustering_edit_configuration() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3245,7 +3224,6 @@ test_clustering_edit_configuration() { } test_clustering_remove_members() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3383,7 +3361,6 @@ test_clustering_remove_members() { } test_clustering_autotarget() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3435,7 +3412,6 @@ test_clustering_autotarget() { } test_clustering_groups() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3624,7 +3600,6 @@ test_clustering_groups() { } test_clustering_events() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3826,7 +3801,6 @@ test_clustering_events() { } test_clustering_uuid() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge @@ -3883,7 +3857,6 @@ test_clustering_uuid() { } test_clustering_trust_add() { - # shellcheck disable=2039,3043 local LXD_DIR setup_clustering_bridge diff --git a/test/suites/clustering_instance_placement_scriptlet.sh b/test/suites/clustering_instance_placement_scriptlet.sh index 337c7964e254..28fedcaf7101 100644 --- a/test/suites/clustering_instance_placement_scriptlet.sh +++ b/test/suites/clustering_instance_placement_scriptlet.sh @@ -1,5 +1,5 @@ test_clustering_instance_placement_scriptlet() { - # shellcheck disable=2039,3043,SC2034 + # shellcheck disable=SC2034 local LXD_DIR setup_clustering_bridge diff --git a/test/suites/clustering_move.sh b/test/suites/clustering_move.sh index 3ac88099ff8e..7781b2800853 100644 --- a/test/suites/clustering_move.sh +++ b/test/suites/clustering_move.sh @@ -1,5 +1,5 @@ test_clustering_move() { - # shellcheck disable=2039,3043,SC2034 + # shellcheck disable=SC2034 local LXD_DIR setup_clustering_bridge diff --git a/test/suites/config.sh b/test/suites/config.sh index b3fd6251231b..1c98339bdcc4 100644 --- a/test/suites/config.sh +++ b/test/suites/config.sh @@ -325,7 +325,6 @@ test_property() { # Create a storage volume, create a volume snapshot and set its expiration timestamp - # shellcheck disable=2039,3043 local storage_pool storage_pool="lxdtest-$(basename "${LXD_DIR}")" storage_volume="${storage_pool}-vol" @@ -347,8 +346,8 @@ test_property() { } test_config_edit_container_snapshot_pool_config() { - # shellcheck disable=2034,2039,2155,3043 - local storage_pool="lxdtest-$(basename "${LXD_DIR}")" + local storage_pool + storage_pool="lxdtest-$(basename "${LXD_DIR}")" ensure_import_testimage diff --git a/test/suites/container_devices_disk.sh b/test/suites/container_devices_disk.sh index f09343744aab..ab1b89f629de 100644 --- a/test/suites/container_devices_disk.sh +++ b/test/suites/container_devices_disk.sh @@ -15,7 +15,6 @@ test_container_devices_disk() { } _container_devices_disk_shift() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -122,7 +121,6 @@ _container_devices_raw_mount_options() { } _container_devices_disk_ceph() { - # shellcheck disable=SC2039,3043 local LXD_BACKEND LXD_BACKEND=$(storage_backend "$LXD_DIR") @@ -147,7 +145,6 @@ _container_devices_disk_ceph() { } _container_devices_disk_cephfs() { - # shellcheck disable=SC2039,3043 local LXD_BACKEND LXD_BACKEND=$(storage_backend "$LXD_DIR") diff --git a/test/suites/container_devices_nic_bridged.sh b/test/suites/container_devices_nic_bridged.sh index 78d1c40fc3ba..201618aff2ba 100644 --- a/test/suites/container_devices_nic_bridged.sh +++ b/test/suites/container_devices_nic_bridged.sh @@ -14,20 +14,23 @@ test_container_devices_nic_bridged() { ctMAC="0a:92:a7:0d:b7:d9" ipRand=$(shuf -i 0-9 -n 1) brName="lxdt$$" + dnsDomain="blah" # Standard bridge with random subnet and a bunch of options lxc network create "${brName}" lxc network set "${brName}" dns.mode managed - lxc network set "${brName}" dns.domain blah + lxc network set "${brName}" dns.domain "${dnsDomain}" lxc network set "${brName}" ipv4.nat true lxc network set "${brName}" ipv4.routing false lxc network set "${brName}" ipv6.routing false + lxc network set "${brName}" ipv4.dhcp.ranges 192.0.2.100-192.0.2.200 + lxc network set "${brName}" ipv6.dhcp.ranges 2001:db8::100-2001:db8::f00 lxc network set "${brName}" ipv6.dhcp.stateful true lxc network set "${brName}" bridge.hwaddr 00:11:22:33:44:55 lxc network set "${brName}" ipv4.address 192.0.2.1/24 lxc network set "${brName}" ipv6.address 2001:db8::1/64 lxc network set "${brName}" ipv4.routes 192.0.3.0/24 - lxc network set "${brName}" ipv6.routes 2001:db7::/64 + lxc network set "${brName}" ipv6.routes 2001:db8::/64 [ "$(cat /sys/class/net/${brName}/address)" = "00:11:22:33:44:55" ] # Record how many nics we started with. @@ -429,6 +432,82 @@ test_container_devices_nic_bridged() { lxc exec "${ctName}" -- udhcpc6 -f -i eth0 -n -q -t5 2>&1 | grep 'IPv6 obtained' fi + # Check that dnsmasq will resolve on the lo + # If testImage can't request a dhcp6 lease, it won't have an ip6 addr, so just + # check the A record; we only care about access to dnsmasq here, not the + # record itself. + dig -4 +retry=0 +notcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + dig -6 +retry=0 +notcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + dig -4 +retry=0 +tcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + dig -6 +retry=0 +tcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + + # Check that dnsmasq will resolve from the bridge + # testImage doesn't have dig, so we create a netns with eth0 connected to the + # bridge instead + ip link add veth_left type veth peer veth_right + ip link set veth_left master "${brName}" up + + ip netns add testdns + ip link set dev veth_right netns testdns + + ip netns exec testdns ip link set veth_right name eth0 + ip netns exec testdns ip link set dev eth0 up + ip netns exec testdns ip addr add 192.0.2.20/24 dev eth0 + ip netns exec testdns ip addr add 2001:db8::20/64 dev eth0 + + ip addr + ip netns exec testdns ip addr + + # Give eth0 a chance to finish duplicate addr detection (ipv6) + while ip netns exec testdns ip a | grep "tentative"; do + sleep 0.5 + done + + ip netns exec testdns dig -4 +retry=0 +notcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ip netns exec testdns dig -6 +retry=0 +notcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ip netns exec testdns dig -4 +retry=0 +tcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ip netns exec testdns dig -6 +retry=0 +tcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + + ip netns exec testdns ip link delete eth0 + ip netns delete testdns + + # Ensure that dnsmasq is inaccessible from outside its managed bridge and the host lo + # This creates a new net namespace `testdns`, a bridge `testbr0`, and veths + # between; we need dns requests to come from an interface that isn't the + # lxd-managed bridge or the host's loopback, and `dig` doesn't let you specify + # the interface to use, only the source ip + testbr0Addr4=10.10.10.1 + testbr0Addr6=fc00:feed:beef::1 + + ip link add veth_left type veth peer veth_right + ip link add testbr0 type bridge + ip link set testbr0 up + ip addr add "${testbr0Addr4}/24" dev testbr0 + ip addr add "${testbr0Addr6}/64" dev testbr0 + ip link set veth_left master testbr0 up + + ip netns add testdns + ip link set dev veth_right netns testdns + + ip netns exec testdns ip link set veth_right name eth0 + ip netns exec testdns ip link set dev eth0 up + ip netns exec testdns ip addr add 10.10.10.2/24 dev eth0 + ip netns exec testdns ip addr add fc00:feed:beef::2/64 dev eth0 + ip netns exec testdns ip route add default via "${testbr0Addr4}" dev eth0 + ip netns exec testdns ip -6 route add default via "${testbr0Addr6}" dev eth0 + + ip addr + ip netns exec testdns ip addr + + ! ip netns exec testdns dig -4 +retry=0 +notcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ! ip netns exec testdns dig -6 +retry=0 +notcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ! ip netns exec testdns dig -4 +retry=0 +tcp @192.0.2.1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + ! ip netns exec testdns dig -6 +retry=0 +tcp @2001:db8::1 A "${ctName}.${dnsDomain}" | grep "${ctName}.${dnsDomain}.\\+0.\\+IN.\\+A.\\+192.0.2." + + ip netns exec testdns ip link delete eth0 + ip netns delete testdns + ip link delete testbr0 + # Delete container, check LXD releases lease. lxc delete "${ctName}" -f diff --git a/test/suites/container_devices_none.sh b/test/suites/container_devices_none.sh new file mode 100644 index 000000000000..2325a016a265 --- /dev/null +++ b/test/suites/container_devices_none.sh @@ -0,0 +1,20 @@ +test_container_devices_none() { + ensure_import_testimage + ensure_has_localhost_remote "${LXD_ADDR}" + ctName="ct$$" + lxc launch testimage "${ctName}" + + # Check eth0 interface exists. + lxc exec "${ctName}" -- stat /sys/class/net/eth0 + + # Add none device to remove eth0 interface (simulating network disruption). + lxc config device add "${ctName}" eth0 none + ! lxc exec "${ctName}" -- stat /sys/class/net/eth0 || false + + # Remove device and check eth0 interface is added back. + lxc config device rm "${ctName}" eth0 + lxc exec "${ctName}" -- stat /sys/class/net/eth0 + + # Clean up + lxc rm -f "${ctName}" +} diff --git a/test/suites/container_local_cross_pool_handling.sh b/test/suites/container_local_cross_pool_handling.sh index b0443733f58b..abce8ba0c9a4 100644 --- a/test/suites/container_local_cross_pool_handling.sh +++ b/test/suites/container_local_cross_pool_handling.sh @@ -1,7 +1,6 @@ test_container_local_cross_pool_handling() { ensure_import_testimage - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") LXD_STORAGE_DIR=$(mktemp -d -p "${TEST_DIR}" XXXXXXXXX) diff --git a/test/suites/database.sh b/test/suites/database.sh index 97a9664517a1..21270b4f449a 100644 --- a/test/suites/database.sh +++ b/test/suites/database.sh @@ -43,7 +43,6 @@ EOF } test_database_no_disk_space() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_NOSPACE_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) diff --git a/test/suites/filtering.sh b/test/suites/filtering.sh index ac9560f70155..d011d1993fe5 100644 --- a/test/suites/filtering.sh +++ b/test/suites/filtering.sh @@ -1,6 +1,5 @@ # Test API filtering. test_filtering() { - # shellcheck disable=2039,3043 local LXD_DIR LXD_FILTERING_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) diff --git a/test/suites/image.sh b/test/suites/image.sh index 644d3ccbb998..eb30848f081a 100644 --- a/test/suites/image.sh +++ b/test/suites/image.sh @@ -1,5 +1,4 @@ test_image_expiry() { - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD2_DIR}" @@ -78,8 +77,8 @@ test_image_expiry() { test_image_list_all_aliases() { ensure_import_testimage - # shellcheck disable=2039,2034,2155,3043 - local sum="$(lxc image info testimage | awk '/^Fingerprint/ {print $2}')" + local sum + sum="$(lxc image info testimage | awk '/^Fingerprint/ {print $2}')" lxc image alias create zzz "$sum" lxc image list | grep -vq zzz # both aliases are listed if the "aliases" column is included in output @@ -91,17 +90,17 @@ test_image_list_all_aliases() { test_image_import_dir() { ensure_import_testimage lxc image export testimage - # shellcheck disable=2039,2034,2155,3043 - local image="$(ls -1 -- *.tar.xz)" + local image + image="$(ls -1 -- *.tar.xz)" mkdir -p unpacked tar -C unpacked -xf "$image" - # shellcheck disable=2039,2034,2155,3043 - local fingerprint="$(lxc image import unpacked | awk '{print $NF;}')" + local fingerprint + fingerprint="$(lxc image import unpacked | awk '{print $NF;}')" rm -rf "$image" unpacked lxc image export "$fingerprint" - # shellcheck disable=2039,2034,2155,3043 - local exported="${fingerprint}.tar.xz" + local exported + exported="${fingerprint}.tar.xz" tar tvf "$exported" | grep -Fq metadata.yaml rm "$exported" @@ -122,7 +121,6 @@ test_image_import_existing_alias() { } test_image_refresh() { - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD2_DIR}" diff --git a/test/suites/image_auto_update.sh b/test/suites/image_auto_update.sh index e5c29b8af1ec..92e6d0aaf7ae 100644 --- a/test/suites/image_auto_update.sh +++ b/test/suites/image_auto_update.sh @@ -3,7 +3,6 @@ test_image_auto_update() { lxc image delete testimage fi - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD2_DIR}" diff --git a/test/suites/image_prefer_cached.sh b/test/suites/image_prefer_cached.sh index 90c1fe090b94..126f4326c1f0 100644 --- a/test/suites/image_prefer_cached.sh +++ b/test/suites/image_prefer_cached.sh @@ -6,7 +6,6 @@ test_image_prefer_cached() { lxc image delete testimage fi - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD2_DIR}" diff --git a/test/suites/incremental_copy.sh b/test/suites/incremental_copy.sh index 7a654412c1c5..fd3836b5c380 100644 --- a/test/suites/incremental_copy.sh +++ b/test/suites/incremental_copy.sh @@ -5,7 +5,6 @@ test_incremental_copy() { do_copy "" "" # cross-pool copy - # shellcheck disable=2039,3043 local source_pool source_pool="lxdtest-$(basename "${LXD_DIR}")-dir-pool" lxc storage create "${source_pool}" dir @@ -14,9 +13,7 @@ test_incremental_copy() { } do_copy() { - # shellcheck disable=2039,3043 local source_pool="${1}" - # shellcheck disable=2039,3043 local target_pool="${2}" # Make sure the containers don't exist diff --git a/test/suites/migration.sh b/test/suites/migration.sh index 9c8bd74f5386..96e271ff3a73 100644 --- a/test/suites/migration.sh +++ b/test/suites/migration.sh @@ -1,6 +1,5 @@ test_migration() { # setup a second LXD - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR lxd_backend # shellcheck disable=2153 lxd_backend=$(storage_backend "$LXD_DIR") @@ -26,7 +25,6 @@ test_migration() { if [ "${LXD_BACKEND}" = "lvm" ]; then # Test that non-thinpool lvm backends work fine with migration. - # shellcheck disable=2039,3043 local storage_pool1 storage_pool2 # shellcheck disable=2153 storage_pool1="lxdtest-$(basename "${LXD_DIR}")-non-thinpool-lvm-migration" @@ -54,7 +52,6 @@ test_migration() { continue fi - # shellcheck disable=2039,3043 local storage_pool1 storage_pool2 # shellcheck disable=2153 storage_pool1="lxdtest-$(basename "${LXD_DIR}")-block-mode" @@ -81,7 +78,6 @@ test_migration() { } migration() { - # shellcheck disable=2039,3043 local lxd2_dir lxd_backend lxd2_backend lxd2_dir="$1" lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/projects.sh b/test/suites/projects.sh index c7b211d65790..0fa40405dfe3 100644 --- a/test/suites/projects.sh +++ b/test/suites/projects.sh @@ -20,6 +20,10 @@ test_projects_crud() { lxc project show foo | grep -q 'features.images: "true"' lxc project get foo "features.profiles" | grep -q 'true' + # Set a limit + lxc project set foo limits.containers 10 + lxc project show foo | grep -q 'limits.containers: "10"' + # Trying to create a project with the same name fails ! lxc project create foo || false @@ -37,6 +41,13 @@ test_projects_crud() { lxc project show bar| sed 's/^description:.*/description: "Bar project"/' | lxc project edit bar lxc project show bar | grep -q "description: Bar project" + # Edit the project config via PATCH. Existing key/value pairs should remain or be updated. + lxc query -X PATCH -d '{\"config\" : {\"limits.memory\":\"5GiB\",\"features.images\":\"false\"}}' /1.0/projects/bar + lxc project show bar | grep -q 'limits.memory: 5GiB' + lxc project show bar | grep -q 'features.images: "false"' + lxc project show bar | grep -q 'features.profiles: "true"' + lxc project show bar | grep -q 'limits.containers: "10"' + # Create a second project lxc project create foo @@ -741,7 +752,6 @@ test_projects_limits() { # too small for resize2fs. if [ "${LXD_BACKEND}" = "dir" ] || [ "${LXD_BACKEND}" = "zfs" ]; then # Add a remote LXD to be used as image server. - # shellcheck disable=2039,3043 local LXD_REMOTE_DIR LXD_REMOTE_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD_REMOTE_DIR}" diff --git a/test/suites/remote.sh b/test/suites/remote.sh index 5240a5405a76..805b69987612 100644 --- a/test/suites/remote.sh +++ b/test/suites/remote.sh @@ -194,7 +194,6 @@ test_remote_admin() { } test_remote_usage() { - # shellcheck disable=2039,3043 local LXD2_DIR LXD2_ADDR LXD2_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD2_DIR}" diff --git a/test/suites/security.sh b/test/suites/security.sh index 7829f24bd817..7a09a46e8401 100644 --- a/test/suites/security.sh +++ b/test/suites/security.sh @@ -24,14 +24,14 @@ test_security() { lxc launch testimage test-priv -c security.privileged=true PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-priv") - UID=$(stat -L -c %u "${LXD_DIR}/containers/test-priv") + FUID=$(stat -L -c %u "${LXD_DIR}/containers/test-priv") if [ "${PERM}" != "100" ]; then echo "Bad container permissions: ${PERM}" false fi - if [ "${UID}" != "0" ]; then - echo "Bad container owner: ${UID}" + if [ "${FUID}" != "0" ]; then + echo "Bad container owner: ${FUID}" false fi @@ -41,14 +41,14 @@ test_security() { lxc restart test-priv --force PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-priv") - UID=$(stat -L -c %u "${LXD_DIR}/containers/test-priv") + FUID=$(stat -L -c %u "${LXD_DIR}/containers/test-priv") if [ "${PERM}" != "100" ]; then echo "Bad container permissions: ${PERM}" false fi - if [ "${UID}" != "0" ]; then - echo "Bad container owner: ${UID}" + if [ "${FUID}" != "0" ]; then + echo "Bad container owner: ${FUID}" false fi @@ -59,14 +59,14 @@ test_security() { lxc restart test-unpriv --force PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-unpriv") - UID=$(stat -L -c %u "${LXD_DIR}/containers/test-unpriv") + FUID=$(stat -L -c %u "${LXD_DIR}/containers/test-unpriv") if [ "${PERM}" != "100" ]; then echo "Bad container permissions: ${PERM}" false fi - if [ "${UID}" != "0" ]; then - echo "Bad container owner: ${UID}" + if [ "${FUID}" != "0" ]; then + echo "Bad container owner: ${FUID}" false fi @@ -74,20 +74,19 @@ test_security() { lxc restart test-unpriv --force PERM=$(stat -L -c %a "${LXD_DIR}/containers/test-unpriv") - UID=$(stat -L -c %u "${LXD_DIR}/containers/test-unpriv") + FUID=$(stat -L -c %u "${LXD_DIR}/containers/test-unpriv") if [ "${PERM}" != "100" ]; then echo "Bad container permissions: ${PERM}" false fi - if [ "${UID}" = "0" ]; then - echo "Bad container owner: ${UID}" + if [ "${FUID}" = "0" ]; then + echo "Bad container owner: ${FUID}" false fi lxc delete test-unpriv --force - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR LXD_STORAGE_DIR=$(mktemp -d -p "${TEST_DIR}" XXXXXXXXX) diff --git a/test/suites/serverconfig.sh b/test/suites/serverconfig.sh index e36b5876ff89..161b879bc851 100644 --- a/test/suites/serverconfig.sh +++ b/test/suites/serverconfig.sh @@ -33,7 +33,6 @@ _server_config_access() { } _server_config_storage() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/snapshots.sh b/test/suites/snapshots.sh index 0f3ff0686f18..9056f198f7db 100644 --- a/test/suites/snapshots.sh +++ b/test/suites/snapshots.sh @@ -17,7 +17,6 @@ test_snapshots() { } snapshots() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") pool="$1" @@ -126,7 +125,6 @@ test_snap_restore() { } snap_restore() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") pool="$1" @@ -343,7 +341,6 @@ restore_and_compare_fs() { } test_snap_expiry() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -369,7 +366,6 @@ test_snap_expiry() { } test_snap_schedule() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -396,7 +392,6 @@ test_snap_schedule() { } test_snap_volume_db_recovery() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage.sh b/test/suites/storage.sh index 5b60ae841a07..2788dfe118c9 100644 --- a/test/suites/storage.sh +++ b/test/suites/storage.sh @@ -1,7 +1,6 @@ test_storage() { ensure_import_testimage - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -10,7 +9,6 @@ test_storage() { spawn_lxd "${LXD_STORAGE_DIR}" false # edit storage and pool description - # shellcheck disable=2039,3043 local storage_pool storage_volume storage_pool="lxdtest-$(basename "${LXD_DIR}")-pool" storage_volume="${storage_pool}-vol" @@ -49,7 +47,6 @@ test_storage() { # Test btrfs resize if [ "$lxd_backend" = "lvm" ] || [ "$lxd_backend" = "ceph" ]; then - # shellcheck disable=2039,3043 local btrfs_storage_pool btrfs_storage_volume btrfs_storage_pool="lxdtest-$(basename "${LXD_DIR}")-pool-btrfs" btrfs_storage_volume="${storage_pool}-vol" diff --git a/test/suites/storage_buckets.sh b/test/suites/storage_buckets.sh index 2ed9daca89c1..483bae4661c2 100644 --- a/test/suites/storage_buckets.sh +++ b/test/suites/storage_buckets.sh @@ -1,5 +1,4 @@ s3cmdrun () { - # shellcheck disable=2039,3043 local backend accessKey secreyKey backend="${1}" accessKey="${2}" @@ -27,7 +26,6 @@ s3cmdrun () { } test_storage_buckets() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage_driver_btrfs.sh b/test/suites/storage_driver_btrfs.sh index 935be125f487..cbe670bea06a 100644 --- a/test/suites/storage_driver_btrfs.sh +++ b/test/suites/storage_driver_btrfs.sh @@ -1,5 +1,4 @@ test_storage_driver_btrfs() { - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage_driver_ceph.sh b/test/suites/storage_driver_ceph.sh index da7394230755..8825f4d38743 100644 --- a/test/suites/storage_driver_ceph.sh +++ b/test/suites/storage_driver_ceph.sh @@ -1,5 +1,4 @@ test_storage_driver_ceph() { - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage_driver_cephfs.sh b/test/suites/storage_driver_cephfs.sh index e7468aed4577..29d0418779bc 100644 --- a/test/suites/storage_driver_cephfs.sh +++ b/test/suites/storage_driver_cephfs.sh @@ -1,5 +1,4 @@ test_storage_driver_cephfs() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage_driver_dir.sh b/test/suites/storage_driver_dir.sh index a4a4440ffbdb..2a61bc3c9f9a 100644 --- a/test/suites/storage_driver_dir.sh +++ b/test/suites/storage_driver_dir.sh @@ -1,11 +1,10 @@ -#!/bin/sh +#!/bin/bash test_storage_driver_dir() { do_dir_on_empty_fs } do_dir_on_empty_fs() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "${LXD_DIR}") diff --git a/test/suites/storage_driver_zfs.sh b/test/suites/storage_driver_zfs.sh index cef162a1ba83..b8357ba8a616 100644 --- a/test/suites/storage_driver_zfs.sh +++ b/test/suites/storage_driver_zfs.sh @@ -8,7 +8,6 @@ test_storage_driver_zfs() { } do_zfs_delegate() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -46,7 +45,6 @@ do_zfs_delegate() { } do_zfs_cross_pool_copy() { - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -118,7 +116,6 @@ do_storage_driver_zfs() { return fi - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/storage_local_volume_handling.sh b/test/suites/storage_local_volume_handling.sh index b3012f88c01d..5631ebd518f8 100755 --- a/test/suites/storage_local_volume_handling.sh +++ b/test/suites/storage_local_volume_handling.sh @@ -1,7 +1,6 @@ test_storage_local_volume_handling() { ensure_import_testimage - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") LXD_STORAGE_DIR=$(mktemp -d -p "${TEST_DIR}" XXXXXXXXX) diff --git a/test/suites/storage_snapshots.sh b/test/suites/storage_snapshots.sh index adcacbe51a9b..5446ee861a35 100644 --- a/test/suites/storage_snapshots.sh +++ b/test/suites/storage_snapshots.sh @@ -1,7 +1,6 @@ test_storage_volume_snapshots() { ensure_import_testimage - # shellcheck disable=2039,3043 local LXD_STORAGE_DIR lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") @@ -10,7 +9,6 @@ test_storage_volume_snapshots() { spawn_lxd "${LXD_STORAGE_DIR}" false lxc remote add test "${LXD_ADDR}" --accept-certificate --password foo - # shellcheck disable=2039,3043 local storage_pool storage_volume storage_pool="lxdtest-$(basename "${LXD_STORAGE_DIR}")-pool" storage_pool2="${storage_pool}2" diff --git a/test/suites/template.sh b/test/suites/template.sh index 490bf9ec199a..1fdb20971a3d 100644 --- a/test/suites/template.sh +++ b/test/suites/template.sh @@ -1,5 +1,4 @@ test_template() { - # shellcheck disable=2039,3043 local lxd_backend lxd_backend=$(storage_backend "$LXD_DIR") diff --git a/test/suites/tls_restrictions.sh b/test/suites/tls_restrictions.sh index 5f807b83e9ed..d898d864b078 100644 --- a/test/suites/tls_restrictions.sh +++ b/test/suites/tls_restrictions.sh @@ -60,17 +60,34 @@ test_tls_restrictions() { ! lxc_remote storage volume list "localhost:${pool_name}" --project default || false ! lxc_remote storage bucket list "localhost:${pool_name}" --project default || false - # Can still list images as some may be public. There are no public images in the default project now, - # so the list should be empty - [ "$(lxc_remote image list localhost --project default --format csv)" = "" ] + ### Validate images. + test_image_fingerprint="$(lxc image info testimage --project default | awk '/^Fingerprint/ {print $2}')" - # Set up the test image in the blah project (ensure_import_testimage imports the image into the current project). - lxc project switch blah && ensure_import_testimage && lxc project switch default + # We can always list images, but there are no public images in the default project now, so the list should be empty. + [ "$(lxc_remote image list localhost: --project default --format csv)" = "" ] + ! lxc_remote image show localhost:testimage --project default || false + + # Set the image to public and ensure we can view it. + lxc image show testimage --project default | sed -e "s/public: false/public: true/" | lxc image edit testimage --project default + [ "$(lxc_remote image list localhost: --project default --format csv | wc -l)" = 1 ] + lxc_remote image show localhost:testimage --project default + + # Check we can export the public image: + lxc image export localhost:testimage "${LXD_DIR}/" --project default + [ "${test_image_fingerprint}" = "$(sha256sum "${LXD_DIR}/${test_image_fingerprint}.tar.xz" | cut -d' ' -f1)" ] + rm "${LXD_DIR}/${test_image_fingerprint}.tar.xz" + + # While the image is public, copy it to the blah project and create an alias for it. + lxc_remote image copy localhost:testimage localhost: --project default --target-project blah + lxc_remote image alias create localhost:testimage "${test_image_fingerprint}" --project blah + + # Restore privacy on the test image in the default project. + lxc image show testimage --project default | sed -e "s/public: true/public: false/" | lxc image edit testimage --project default # Set up a profile in the blah project. Additionally ensures restricted TLS clients can edit profiles in projects they have access to. lxc profile show default | lxc_remote profile edit localhost:default --project blah - # Create an instance. + # Create an instance (using the test image copied from the default project while it was public). lxc_remote init testimage localhost:blah-instance --project blah # Create a custom volume.