pax_global_header 0000666 0000000 0000000 00000000064 15232704312 0014511 g ustar 00root root 0000000 0000000 52 comment=90429bf42f08b41c091e799d81465ff24b5cee2b
incus-7.3.0/ 0000775 0000000 0000000 00000000000 15232704312 0012641 5 ustar 00root root 0000000 0000000 incus-7.3.0/.codespell-ignore 0000664 0000000 0000000 00000000064 15232704312 0016075 0 ustar 00root root 0000000 0000000 AtLeast
attachs
destOp
ECT
inport
renderD
requestor
incus-7.3.0/.deepsource.toml 0000664 0000000 0000000 00000000406 15232704312 0015752 0 ustar 00root root 0000000 0000000 version = 1
test_patterns = [
"test/**",
"*_test.go"
]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_paths = ["github.com/lxc/incus"]
incus-7.3.0/.devcontainer/ 0000775 0000000 0000000 00000000000 15232704312 0015400 5 ustar 00root root 0000000 0000000 incus-7.3.0/.devcontainer/Dockerfile 0000664 0000000 0000000 00000006035 15232704312 0017376 0 ustar 00root root 0000000 0000000 ARG GO_VERSION=1.25
ARG DEBIAN_VERSION=trixie
# Go development container
FROM golang:${GO_VERSION}-${DEBIAN_VERSION}
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000
# Install necessary tools.
RUN sed -r -i 's/^Components: main$/Components: main contrib/g' /etc/apt/sources.list.d/debian.sources && \
apt update && \
apt install -y \
acl \
aspell \
aspell-en \
attr \
autoconf \
automake \
bind9-dnsutils \
btrfs-progs \
busybox-static \
ceph-common \
curl \
dnsmasq-base \
ebtables \
flake8 \
gettext \
git \
jq \
less \
libacl1-dev \
libcap-dev \
# libcowsql-dev
libdbus-1-dev \
# liblxc-dev \
liblxc1 \
liblz4-dev \
libseccomp-dev \
libselinux1-dev \
libsqlite3-dev \
libtool \
libudev-dev \
libusb-1.0-0-dev \
libuv1-dev \
locales \
locales-all \
lvm2 \
lxc-dev \
lxc-templates \
make \
man-db \
pipx \
pkg-config \
protoc-gen-go \
python3-matplotlib \
python3.13-venv \
rsync \
ruby-mdl \
shellcheck \
socat \
sqlite3 \
squashfs-tools \
sudo \
tar \
tcl \
thin-provisioning-tools \
vim \
# Disabled for now, very slow to install.
# zfsutils-linux
xz-utils
# Globally install codespell
RUN pipx install --global codespell
# Add vscode user and add it to sudoers.
RUN groupadd -g 1000 $USERNAME && \
useradd -s /bin/bash -u $USER_UID -g $USER_GID -m $USERNAME && \
mkdir -p /etc/sudoers.d && \
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME && \
chmod 0440 /etc/sudoers.d/$USERNAME
# Setup for vscode user.
USER $USERNAME
ENV EDITOR=vi \
LANG=en_US.UTF-8 \
CGO_CFLAGS="-I/home/vscode/vendor/raft/include/ -I/home/vscode/vendor/cowsql/include/" \
CGO_LDFLAGS="-L/home/vscode/vendor/raft/.libs -L/home/vscode/vendor/cowsql/.libs/" \
LD_LIBRARY_PATH="/home/vscode/vendor/raft/.libs/:/home/vscode/vendor/cowsql/.libs/" \
CGO_LDFLAGS_ALLOW="(-Wl,-wrap,pthread_create)|(-Wl,-z,now)"
# Build Go tools with user vscode to ensure correct file and directory permissions for the build artifacts.
RUN go install -v github.com/google/go-licenses@latest && \
go install -v github.com/766b/go-outliner@latest && \
GOTOOLCHAIN="" go install -v golang.org/x/tools/gopls@latest && \
go install -v github.com/go-delve/delve/cmd/dlv@latest && \
go install -v golang.org/x/tools/cmd/goimports@latest && \
go install -v golang.org/x/vuln/cmd/govulncheck@latest && \
go install -v mvdan.cc/gofumpt@latest && \
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin
# Make dependencies
COPY Makefile /home/vscode
RUN cd /home/vscode && \
mkdir /home/vscode/vendor && \
make deps
USER root
# Since we use a volume for /go to persist the content between executions, we need to preserve the binaries.
RUN mv /go/bin/* /usr/local/bin
incus-7.3.0/.devcontainer/devcontainer.json 0000664 0000000 0000000 00000003446 15232704312 0020763 0 ustar 00root root 0000000 0000000 {
"name": "Incus",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"766b.go-outliner",
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools",
"github.vscode-github-actions",
"davidanson.vscode-markdownlint",
"shardulm94.trailing-spaces",
"Gruntfuggly.todo-tree"
],
"settings": {
"files.insertFinalNewline": true,
"go.goroot": "/usr/local/go",
"go.gopath": "/go",
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"go.lintFlags": [ "--fast" ],
"go.useLanguageServer": true,
"goOutliner.extendExplorerTab": true,
"gopls": {
"formatting.gofumpt": true,
"formatting.local": "github.com/lxc/incus",
"ui.diagnostic.staticcheck": false
},
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"search.exclude": {
"**/.git": true
}
}
}
},
"postCreateCommand": "go mod download",
"mounts": [
"source=incus_devcontainer_cache,target=/home/vscode/.cache,type=volume",
"source=incus_devcontainer_goroot,target=/go,type=volume"
],
"runArgs": [
"--privileged",
"-u",
"vscode",
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined",
"-v", "${env:HOME}/.ssh:/home/vscode/.ssh:ro",
"--name", "${localEnv:USER}_incus_devcontainer"
],
"remoteUser": "vscode"
}
incus-7.3.0/.github/ 0000775 0000000 0000000 00000000000 15232704312 0014201 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/CODEOWNERS 0000664 0000000 0000000 00000000014 15232704312 0015567 0 ustar 00root root 0000000 0000000 * @stgraber
incus-7.3.0/.github/FUNDING.yml 0000664 0000000 0000000 00000000255 15232704312 0016020 0 ustar 00root root 0000000 0000000 # Frequent committers who contribute to Incus on their own time can add
# themselves to the list here so users who feel like sponsoring can find
# them.
github:
- stgraber
incus-7.3.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15232704312 0016364 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/ISSUE_TEMPLATE/bug-reports.yml 0000664 0000000 0000000 00000004166 15232704312 0021367 0 ustar 00root root 0000000 0000000 name: Bug report
description: File a bug report.
type: bug
body:
- type: markdown
attributes:
value: |
> [!NOTE]
> Thank you for taking the time to fill out this bug report. As this issue will be read and debugged by humans, we kindly ask you to refrain from using AI tools to try to interpret your problem or suggest patches, as per our [contribution guidelines](https://github.com/lxc/incus/blob/main/CONTRIBUTING.md).
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: There is no existing issue for this bug
required: true
- type: checkboxes
attributes:
label: Is this happening on an up to date version of Incus?
description: Please make sure that your system has all updates applied and is running a current version of Incus or Incus LTS.
options:
- label: This is happening on a supported version of Incus
required: true
- type: textarea
attributes:
label: Incus system details
description: Output of `incus info`.
render: yaml
validations:
required: true
- type: textarea
attributes:
label: Instance details
description: If the issue affects an instance, please include the output of `incus config show NAME`.
validations:
required: false
- type: textarea
attributes:
label: Instance log
description: If the issue is related to an instance startup failure, please include `incus info --show-log NAME`.
validations:
required: false
- type: textarea
attributes:
label: Current behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps to reproduce
description: Step by step instructions to reproduce the behavior.
placeholder: |
1. Step one
2. Step two
3. Step three
validations:
required: true
incus-7.3.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000244 15232704312 0020354 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
contact_links:
- name: Support question
url: https://discuss.linuxcontainers.org
about: Please ask and answer questions here.
incus-7.3.0/.github/ISSUE_TEMPLATE/feature-requests.yml 0000664 0000000 0000000 00000001356 15232704312 0022420 0 ustar 00root root 0000000 0000000 name: Feature request
description: File a feature request.
type: feature
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you'd like to see added.
options:
- label: There is no existing issue for this feature
required: true
- type: textarea
attributes:
label: What are you currently unable to do
description: A concise description of the problem you're trying to solve.
validations:
required: true
- type: textarea
attributes:
label: What do you think would need to be added
description: A concise description of what you think should be added to Incus.
validations:
required: false
incus-7.3.0/.github/SUPPORT.md 0000664 0000000 0000000 00000000275 15232704312 0015703 0 ustar 00root root 0000000 0000000 The Incus team uses GitHub for issue and feature tracking, not for user support.
For information on how to get support, see [Support](https://linuxcontainers.org/incus/docs/main/support/).
incus-7.3.0/.github/dependabot.yml 0000664 0000000 0000000 00000000205 15232704312 0017026 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
labels: []
schedule:
interval: "weekly"
incus-7.3.0/.github/labeler.yml 0000664 0000000 0000000 00000000303 15232704312 0016326 0 ustar 00root root 0000000 0000000 API:
- changed-files:
- any-glob-to-any-file:
- doc/api-extensions.md
- doc/rest-api.yaml
- shared/api/**/*
Documentation:
- changed-files:
- any-glob-to-any-file:
- doc/**/*
incus-7.3.0/.github/packaging/ 0000775 0000000 0000000 00000000000 15232704312 0016125 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/packaging/macos/ 0000775 0000000 0000000 00000000000 15232704312 0017227 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/packaging/macos/build.sh 0000775 0000000 0000000 00000001602 15232704312 0020664 0 ustar 00root root 0000000 0000000 #!/bin/bash
# Build a universal Incus client PKG installer.
set -eu
VERSION="${VERSION:?VERSION must be set}"
here="$(dirname "$0")"
out="installers"
root="pkgroot"
res="pkgresources"
mkdir -p "${out}" "${root}/usr/local/bin" "${res}"
cp COPYING "${res}/LICENSE.txt"
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o incus.amd64 ./cmd/incus
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o incus.arm64 ./cmd/incus
lipo -create -output "${root}/usr/local/bin/incus" incus.amd64 incus.arm64
chmod 0755 "${root}/usr/local/bin/incus"
pkgbuild --root "${root}" \
--identifier org.linuxcontainers.incus \
--version "${VERSION}" \
--install-location / \
incus-component.pkg
productbuild --distribution "${here}/distribution.xml" \
--package-path . \
--resources "${res}" \
"${out}/incus.macos.pkg"
rm -rf incus.amd64 incus.arm64 incus-component.pkg "${root}" "${res}"
incus-7.3.0/.github/packaging/macos/distribution.xml 0000664 0000000 0000000 00000001230 15232704312 0022464 0 ustar 00root root 0000000 0000000
Incusincus-component.pkg
incus-7.3.0/.github/packaging/windows/ 0000775 0000000 0000000 00000000000 15232704312 0017617 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/packaging/windows/build.sh 0000775 0000000 0000000 00000001401 15232704312 0021251 0 ustar 00root root 0000000 0000000 #!/bin/bash
# Build per-architecture Incus client MSI installers.
set -eu
VERSION="${VERSION:?VERSION must be set}"
here="$(dirname "$0")"
out="installers"
mkdir -p "${out}"
bash "${here}/make-license-rtf.sh" COPYING "${out}/license.rtf"
build_one() {
goarch="$1"
wixarch="$2"
name="$3"
CGO_ENABLED=0 GOOS=windows GOARCH="${goarch}" go build -o "${out}/incus.exe" ./cmd/incus
wix build -arch "${wixarch}" \
-ext WixToolset.UI.wixext \
-d Version="${VERSION}" \
-d BinPath="${out}/incus.exe" \
-d LicenseRtf="${out}/license.rtf" \
-o "${out}/incus.windows.${name}.msi" \
"${here}/incus.wxs"
}
build_one amd64 x64 x86_64
build_one arm64 arm64 aarch64
rm -f "${out}/incus.exe" "${out}/license.rtf"
incus-7.3.0/.github/packaging/windows/incus.wxs 0000664 0000000 0000000 00000002356 15232704312 0021511 0 ustar 00root root 0000000 0000000
incus-7.3.0/.github/packaging/windows/make-license-rtf.sh 0000775 0000000 0000000 00000000447 15232704312 0023311 0 ustar 00root root 0000000 0000000 #!/bin/bash
# Wrap a plain-text license into a minimal RTF for the WiX license dialog.
set -eu
src="$1"
dst="$2"
{
printf '{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Courier New;}}\\fs16\n'
sed -e 's/\\/\\\\/g' -e 's/{/\\{/g' -e 's/}/\\}/g' -e 's/$/\\par/' "$src"
printf '}\n'
} > "$dst"
incus-7.3.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15232704312 0016236 5 ustar 00root root 0000000 0000000 incus-7.3.0/.github/workflows/build.yml 0000664 0000000 0000000 00000004263 15232704312 0020065 0 ustar 00root root 0000000 0000000 name: Build
on:
push:
branches:
- main
- stable-*
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build (${{ matrix.architecture }})
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
runs-on:
- self-hosted
- lxc-incus-build
- arch-${{ matrix.architecture }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: stable
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
autoconf \
automake \
curl \
git \
libacl1-dev \
libcap-dev \
libdbus-1-dev \
liblz4-dev \
libseccomp-dev \
libselinux-dev \
libsqlite3-dev \
libtool \
libudev-dev \
libuv1-dev \
lxc-dev \
make \
pkg-config \
zip
- name: Download go dependencies
run: |
go mod download
- name: Build cowsql and raft
run: |
set -x
make deps
raft_path="$(go env GOPATH)/deps/raft"
cowsql_path="$(go env GOPATH)/deps/cowsql"
{
echo "CGO_CFLAGS=-I${raft_path}/include/ -I${cowsql_path}/include/"
echo "CGO_LDFLAGS=-L${raft_path}/.libs -L${cowsql_path}/.libs/"
echo "LD_LIBRARY_PATH=${raft_path}/.libs/:${cowsql_path}/.libs/"
echo "CGO_LDFLAGS_ALLOW=(-Wl,-wrap,pthread_create)|(-Wl,-z,now)"
} >> "$GITHUB_ENV"
- name: Build incusd and incus
run: |
make
- name: Collect binaries
run: |
mkdir -p build
cp "$(go env GOPATH)/bin/incusd" build/
cp "$(go env GOPATH)/bin/incus" build/
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: build.${{ matrix.architecture }}
path: build/
incus-7.3.0/.github/workflows/commits.yml 0000664 0000000 0000000 00000004041 15232704312 0020433 0 ustar 00root root 0000000 0000000 name: Commits
on:
- pull_request
permissions:
contents: read
jobs:
dco-check:
permissions:
pull-requests: read # for tim-actions/get-pr-commits to get list of commits from the PR
name: Signed-off-by (DCO)
runs-on: ubuntu-24.04
steps:
- name: Check that all commits are signed-off
uses: KineticCafe/actions-dco@v3.2.0
llm-commit-policy:
permissions:
contents: none
name: LLM commit policy
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Check LLM commit policy
run: |
set -eu
# Inspired by https://github.com/yaml/go-yaml/pull/340
agents="(aider|anthropic|claude|codex|copilot|devin|gemini|grok|openai)"
commits=$(git rev-list ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
for commit in $commits; do
if git log -n 1 "$commit" | tr '[:upper:]' '[:lower:]' | grep -qE "(author|assisted-by|co-authored-by|signed-off-by):.*$agents"; then
echo "Error: The following commit appears to violate this repo's LLM/AI contribution policy:"
echo ""
git log -n 1 "$commit"
exit 1
fi
done
target-branch:
permissions:
contents: none
name: Branch target
runs-on: ubuntu-24.04
steps:
- name: Check branch target
env:
TARGET: ${{ github.event.pull_request.base.ref }}
TITLE: ${{ github.event.pull_request.title }}
run: |
set -eux
TARGET_FROM_PR_TITLE="$(echo "${TITLE}" | sed -n 's/.*(\(stable-[0-9]\.[0-9]\))$/\1/p')"
if [ -z "${TARGET_FROM_PR_TITLE}" ]; then
TARGET_FROM_PR_TITLE="main"
else
echo "Branch target overridden from PR title"
fi
[ "${TARGET}" = "${TARGET_FROM_PR_TITLE}" ] && exit 0
echo "Invalid branch target: ${TARGET} != ${TARGET_FROM_PR_TITLE}"
exit 1
incus-7.3.0/.github/workflows/release.yml 0000664 0000000 0000000 00000007205 15232704312 0020405 0 ustar 00root root 0000000 0000000 name: Release
on:
push:
tags:
- '*'
permissions:
contents: write
issues: write
id-token: write
attestations: write
jobs:
version:
name: Format version
runs-on: ubuntu-latest
outputs:
display: ${{ steps.version.outputs.display }}
full: ${{ steps.version.outputs.full }}
steps:
- name: Format version
id: version
run: |
raw="${GITHUB_REF_NAME#v}"
IFS='.' read -r major minor patch <<< "$raw"
echo "full=${major}.${minor}.${patch}" >> $GITHUB_OUTPUT
if [ "${patch}" = "0" ]; then
echo "display=${major}.${minor}" >> $GITHUB_OUTPUT
else
echo "display=${major}.${minor}.${patch}" >> $GITHUB_OUTPUT
fi
build-msi:
name: Build Windows installer
runs-on: windows-latest
needs: version
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: stable
# Pinned to v5, the last release before WiX v6/v7 moved to the Open
# Source Maintenance Fee license requiring EULA acceptance.
- name: Install WiX
shell: pwsh
run: |
dotnet tool install --global wix --version 5.0.2
echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Add WiX extensions
shell: pwsh
run: wix extension add -g WixToolset.UI.wixext/5.0.2
- name: Build installers
shell: bash
env:
VERSION: ${{ needs.version.outputs.full }}
run: bash .github/packaging/windows/build.sh
- name: Upload installers
uses: actions/upload-artifact@v7
with:
name: installers-windows
path: installers/*.msi
if-no-files-found: error
build-pkg:
name: Build macOS installer
runs-on: macos-latest
needs: version
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: stable
- name: Build installer
env:
VERSION: ${{ needs.version.outputs.full }}
run: bash .github/packaging/macos/build.sh
- name: Upload installer
uses: actions/upload-artifact@v7
with:
name: installers-macos
path: installers/*.pkg
if-no-files-found: error
goreleaser:
name: Release
runs-on: ubuntu-latest
needs: [version, build-msi, build-pkg]
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: stable
- name: Install syft
uses: anchore/sbom-action/download-syft@v0
- name: Download installers
uses: actions/download-artifact@v8
with:
pattern: installers-*
path: installers
merge-multiple: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INCUS_VERSION: ${{ needs.version.outputs.display }}
- name: Handle attestation
uses: actions/attest@v4
with:
subject-checksums: ./dist/checksums.txt
- name: Handle installer attestation
uses: actions/attest@v4
with:
subject-path: |
installers/*.msi
installers/*.pkg
incus-7.3.0/.github/workflows/slop.yml 0000664 0000000 0000000 00000002027 15232704312 0017737 0 ustar 00root root 0000000 0000000 name: Cleanup slop
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
close-untyped:
name: Close issue if type is missing
if: ${{ !github.event.issue.pull_request && !github.event.issue.type && github.event.issue.author_association != 'MEMBER' && github.event.issue.author_association != 'OWNER' }}
runs-on: ubuntu-latest
steps:
- name: Close issue
uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: "Issues must be created through the [GitHub web interface](https://github.com/lxc/incus/issues/new/choose). Closing automatically."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
});
incus-7.3.0/.github/workflows/tests.yml 0000664 0000000 0000000 00000050156 15232704312 0020132 0 ustar 00root root 0000000 0000000 name: Tests
on:
push:
branches:
- main
- stable-*
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
code-tests:
name: Code
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go:
- oldstable
- stable
- tip
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# Differential ShellCheck requires full git history
fetch-depth: 0
- name: Dependency Review
uses: actions/dependency-review-action@v5
if: github.event_name == 'pull_request'
with:
allow-ghsas: GHSA-4p9m-8gc4-rw2h
- id: ShellCheck
name: Differential ShellCheck
uses: redhat-plumbers-in-action/differential-shellcheck@v5
env:
SHELLCHECK_OPTS: --shell sh
with:
token: ${{ secrets.GITHUB_TOKEN }}
exclude-path: internal/server/instance/drivers/agent-loader/rc.d/incus-agent
if: github.event_name == 'pull_request' && matrix.go == 'stable'
- name: Upload artifact with ShellCheck defects in SARIF format
uses: actions/upload-artifact@v7
with:
name: Differential ShellCheck SARIF
path: ${{ steps.ShellCheck.outputs.sarif }}
if: github.event_name == 'pull_request' && matrix.go == 'stable'
- name: Install Go (${{ matrix.go }})
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
if: matrix.go != 'tip'
- name: Install Go (stable)
uses: actions/setup-go@v7
with:
go-version: stable
if: matrix.go == 'tip'
- name: Install Go (tip)
run: |
go install golang.org/dl/gotip@latest
gotip download
~/sdk/gotip/bin/go version
echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV
if: matrix.go == 'tip'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
curl \
gettext \
git \
libacl1-dev \
libcap-dev \
libdbus-1-dev \
libcowsql-dev \
liblxc-dev \
lxc-templates \
libseccomp-dev \
libselinux-dev \
libsqlite3-dev \
libtool \
libudev-dev \
make \
pipx \
pkg-config \
shellcheck
# With pipx >= 1.5.0, we could use pipx --global instead.
PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin \
pipx install codespell flake8
- name: Fix repository permissions
run: |
sudo chown -R runner:docker .
- name: Check compatible min Go version
run: |
go mod tidy
- name: Download go dependencies
run: |
go mod download
- name: Run Incus build
run: |
make
- name: Run static analysis
env:
GITHUB_BEFORE: ${{ github.event.before }}
run: |
make static-analysis
- name: Unit tests (all)
run: |
sudo --preserve-env=CGO_CFLAGS,CGO_LDFLAGS,CGO_LDFLAGS_ALLOW,LD_LIBRARY_PATH LD_LIBRARY_PATH=${LD_LIBRARY_PATH} env "PATH=${PATH}" go test ./...
system-tests:
name: System
strategy:
fail-fast: false
matrix:
go:
- oldstable
- stable
- tip
suite:
- cluster
- standalone_core
- standalone_container
- standalone_network
- standalone_storage
backend:
- dir
os:
- ubuntu-24.04
- ubuntu-24.04-arm
include:
# Run standalone storage tests on all storage drivers but only on Ubuntu 24.04 with stable Go
- os: ubuntu-24.04
backend: btrfs
go: stable
suite: standalone_storage
- os: ubuntu-24.04
backend: ceph
go: stable
suite: standalone_storage
- os: ubuntu-24.04
backend: linstor
go: stable
suite: standalone_storage
- os: ubuntu-24.04
backend: lvm
go: stable
suite: standalone_storage
- os: ubuntu-24.04
backend: random
go: stable
suite: standalone_storage
- os: ubuntu-24.04
backend: zfs
go: stable
suite: standalone_storage
# Run cluster tests on all storage drivers but only on Ubuntu 24.04 with stable Go
- os: ubuntu-24.04
backend: btrfs
go: stable
suite: cluster
- os: ubuntu-24.04
backend: ceph
go: stable
suite: cluster
- os: ubuntu-24.04
backend: linstor
go: stable
suite: cluster
- os: ubuntu-24.04
backend: lvm
go: stable
suite: cluster
- os: ubuntu-24.04
backend: random
go: stable
suite: cluster
- os: ubuntu-24.04
backend: zfs
go: stable
suite: cluster
runs-on: ${{ matrix.os }}
steps:
- name: Performance tuning
run: |
set -eux
# optimize ext4 FSes for performance, not reliability
for fs in $(findmnt --noheading --type ext4 --list --uniq | awk '{print $1}'); do
# nombcache and data=writeback cannot be changed on remount
sudo mount -o remount,noatime,barrier=0,commit=6000 "${fs}" || true
done
# disable dpkg from calling sync()
echo "force-unsafe-io" | sudo tee /etc/dpkg/dpkg.cfg.d/force-unsafe-io
- name: Reclaim some space
run: |
set -eux
sudo snap remove lxd --purge
# Purge older snap revisions that are disabled/superseded by newer revisions of the same snap
snap list --all | while read -r name _ rev _ _ notes _; do
[ "${notes}" = "disabled" ] && snap remove "${name}" --revision "${rev}" --purge
done || true
# This was inspired from https://github.com/easimon/maximize-build-space
df -h /
# dotnet
sudo rm -rf /usr/share/dotnet
# android
sudo rm -rf /usr/local/lib/android
# haskell
sudo rm -rf /opt/ghc
df -h /
- name: Remove docker
run: |
set -eux
sudo apt-get autopurge -y moby-containerd docker uidmap
sudo ip link delete docker0
sudo nft flush ruleset
- name: Remove pre-installed Java
run: |
set -eux
sudo apt-get update
sudo apt-get remove --yes --purge temurin.*
- name: Checkout
uses: actions/checkout@v7
- name: Install Go (${{ matrix.go }})
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
if: matrix.go != 'tip'
- name: Install Go (stable)
uses: actions/setup-go@v7
with:
go-version: stable
if: matrix.go == 'tip'
- name: Install Go (tip)
run: |
go install golang.org/dl/gotip@latest
gotip download
~/sdk/gotip/bin/go version
echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV
if: matrix.go == 'tip'
- name: Install dependencies
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -x
# Configure ppa:ubuntu-lxc/daily directly
# (apt-add-repository relies on the Launchpad API which is unreliable).
sudo install -d -m 0755 /etc/apt/keyrings
codename="$(lsb_release -cs)"
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&search=0xE9C00C1B1B59A86C2CFEE6990CE27B8C4122B4B7" | sudo tee /etc/apt/keyrings/ubuntu-lxc-daily.asc > /dev/null
sudo tee /etc/apt/sources.list.d/ubuntu-lxc-daily.sources > /dev/null <> "$GITHUB_ENV"
- name: Run Incus build
run: |
make
- name: Setup scratch space
if: "matrix.backend == 'ceph' || matrix.backend == 'linstor'"
run: |
set -eux
if mountpoint -q /mnt; then
[ -e /mnt/swapfile ] && sudo swapoff /mnt/swapfile
block_path="$(findmnt --noheadings --output SOURCE --target /mnt | sed 's/[0-9]\+$//')"
sudo umount /mnt
sudo wipefs -a "${block_path}"
sudo ln -s "${block_path}" "/dev/scratch"
else
sudo truncate -s 20G /scratch.img
block_path="$(sudo losetup --show -f /scratch.img)"
sudo ln -s "${block_path}" "/dev/scratch"
fi
- name: Setup MicroCeph
if: matrix.backend == 'ceph'
run: |
set -x
sudo apt-get install --no-install-recommends -y snapd
sudo snap install microceph --channel=quincy/stable
sudo apt-get install --no-install-recommends -y ceph-common
sudo microceph cluster bootstrap
sudo microceph.ceph config set global osd_pool_default_size 1
sudo microceph.ceph config set global mon_allow_pool_delete true
sudo microceph.ceph config set global osd_memory_target 939524096
sudo microceph.ceph osd crush rule rm replicated_rule
sudo microceph.ceph osd crush rule create-replicated replicated default osd
for flag in nosnaptrim noscrub nobackfill norebalance norecover noscrub nodeep-scrub; do
sudo microceph.ceph osd set $flag
done
# Repurpose the ephemeral disk for ceph OSD.
sudo microceph disk add --wipe "$(readlink -f /dev/scratch)"
sudo rm -rf /etc/ceph
sudo ln -s /var/snap/microceph/current/conf/ /etc/ceph
sudo microceph enable rgw
sudo microceph.ceph osd pool create cephfs_meta 32
sudo microceph.ceph osd pool create cephfs_data 32
sudo microceph.ceph fs new cephfs cephfs_meta cephfs_data
sudo microceph.ceph fs ls
sleep 30
sudo microceph.ceph status
# Wait until there are no more "unkowns" pgs
for _ in $(seq 60); do
if sudo microceph.ceph pg stat | grep -wF unknown; then
sleep 1
else
break
fi
done
sudo microceph.ceph status
sudo rm -f /snap/bin/rbd
- name: Setup LINSTOR
if: matrix.backend == 'linstor'
run: |
set -x
# Configure ppa:linbit/linbit-drbd9-stack directly
# (apt-add-repository relies on the Launchpad API which is unreliable).
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&search=0xCC1B5A793C04BB3905AD837734893610CEAA9512" | sudo tee /etc/apt/keyrings/linbit-drbd9-stack.asc > /dev/null
sudo tee /etc/apt/sources.list.d/linbit-drbd9-stack.sources > /dev/null <> "$GITHUB_ENV"
- name: "Ensure offline mode (block image server)"
run: |
sudo nft add table inet filter
sudo nft add chain 'inet filter output { type filter hook output priority 10 ; }'
sudo nft add rule inet filter output ip daddr 45.45.148.8 reject
sudo nft add rule inet filter output ip6 daddr 2602:fc62:a:1::8 reject
- name: "Run system tests (${{ matrix.go }}, ${{ matrix.suite }}, ${{ matrix.backend }})"
env:
CGO_LDFLAGS_ALLOW: "(-Wl,-wrap,pthread_create)|(-Wl,-z,now)"
INCUS_CEPH_CLUSTER: "ceph"
INCUS_CEPH_CEPHFS: "cephfs"
INCUS_CEPH_CEPHOBJECT_RADOSGW: "http://127.0.0.1"
INCUS_LINSTOR_LOCAL_SATELLITE: "local"
INCUS_CONCURRENT: "1"
INCUS_VERBOSE: "1"
INCUS_OFFLINE: "1"
INCUS_TMPFS: "1"
INCUS_REQUIRED_TESTS: "test_storage_buckets"
run: |
chmod +x ~
echo "root:1000000:1000000000" | sudo tee /etc/subuid /etc/subgid
cd test
sudo --preserve-env=PATH,GOPATH,GITHUB_ACTIONS,INCUS_VERBOSE,INCUS_BACKEND,INCUS_CEPH_CLUSTER,INCUS_CEPH_CEPHFS,INCUS_CEPH_CEPHOBJECT_RADOSGW,INCUS_LINSTOR_LOCAL_SATELLITE,INCUS_LINSTOR_CLUSTER,INCUS_OFFLINE,INCUS_SKIP_TESTS,INCUS_REQUIRED_TESTS, INCUS_BACKEND=${{ matrix.backend }} env LD_LIBRARY_PATH=${LD_LIBRARY_PATH} JAVA_HOME= ./main.sh ${{ matrix.suite }}
client:
name: Client
strategy:
fail-fast: false
matrix:
go:
- oldstable
- stable
os:
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
- name: Create build directory
run: |
mkdir bin
- name: Build static incus (x86_64)
env:
CGO_ENABLED: 0
GOARCH: amd64
run: |
go build -o bin/incus.x86_64 ./cmd/incus
- name: Build static incus (aarch64)
env:
CGO_ENABLED: 0
GOARCH: arm64
run: |
go build -o bin/incus.aarch64 ./cmd/incus
- name: Build static incus-agent (x86_64)
env:
CGO_ENABLED: 0
GOARCH: amd64
run: |
go build -o bin/incus-agent.x86_64 ./cmd/incus-agent
- name: Build static incus-agent (aarch64)
env:
CGO_ENABLED: 0
GOARCH: arm64
run: |
go build -o bin/incus-agent.aarch64 ./cmd/incus-agent
- name: Build static incus-migrate
if: runner.os == 'Linux'
env:
CGO_ENABLED: 0
run: |
GOARCH=amd64 go build -o bin/incus-migrate.x86_64 ./cmd/incus-migrate
GOARCH=arm64 go build -o bin/incus-migrate.aarch64 ./cmd/incus-migrate
- name: Unit tests (client)
env:
CGO_ENABLED: 0
run: go test -v ./client/...
- name: Unit tests (incus)
env:
CGO_ENABLED: 0
run: go test -v ./cmd/incus/...
- name: Unit tests (shared)
env:
CGO_ENABLED: 0
run: go test -v ./shared/...
- name: Upload incus client artifacts
if: matrix.go == 'stable'
uses: actions/upload-artifact@v7
continue-on-error: true
with:
name: ${{ runner.os }}
path: bin/
documentation:
name: Documentation
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Install Go
uses: actions/setup-go@v7
with:
go-version: stable
- name: Install dependencies
run: |
sudo apt-get install -y aspell aspell-en ruby
sudo gem install --no-document mdl
- name: Run markdown linter
run: |
make doc-lint
- name: Run spell checker
run: |
make doc-spellcheck
- name: Run inclusive naming checker
uses: get-woke/woke-action@v0
with:
fail-on-error: true
woke-args: "*.md **/*.md -c https://github.com/canonical/Inclusive-naming/raw/main/config.yml"
- name: Run link checker
run: |
make doc-linkcheck
- name: Build docs (Sphinx)
run: make doc
- name: Print warnings (Sphinx)
run: if [ -s doc/.sphinx/warnings.txt ]; then cat doc/.sphinx/warnings.txt; exit 1; fi
- name: Upload documentation artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: documentation
path: doc/html
incus-7.3.0/.github/workflows/triage.yml 0000664 0000000 0000000 00000000650 15232704312 0020235 0 ustar 00root root 0000000 0000000 name: Triaging
on:
- pull_request_target
permissions:
contents: read
jobs:
label:
permissions:
contents: read # for actions/labeler to determine modified files
pull-requests: write # for actions/labeler to add labels to PRs
name: PR labels
runs-on: ubuntu-24.04
steps:
- uses: actions/labeler@v7
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true
incus-7.3.0/.gitignore 0000664 0000000 0000000 00000001337 15232704312 0014635 0 ustar 00root root 0000000 0000000 *.swp
po/*.mo
po/*.po~
incus-*.tar.xz
installers
.vagrant
*~
tags
# Potential binaries
cmd/fuidshift/fuidshift
cmd/incus/incus
cmd/lxc-to-incus/lxc-to-incus
cmd/incus-agent/incus-agent
cmd/incus-benchmark/incus-benchmark
cmd/incus-migrate/incus-migrate
cmd/incus-user/incus-user
test/dev_incus-client/dev_incus-client
test/syscall/sysinfo/sysinfo
test/mini-oidc/mini-oidc
test/mini-oidc/user.data
test/tls2jwt/tls2jwt
# Sphinx
doc/html/
doc/reference/manpages/**/*.md
doc/.sphinx/deps/
doc/.sphinx/.doctrees/
doc/.sphinx/themes/
doc/.sphinx/venv/
doc/.sphinx/warnings.txt
doc/.sphinx/.wordlist.dic
doc/.sphinx/_static/swagger-ui
doc/.sphinx/_static/download
doc/__pycache__
# For Atom ctags
.tags
.tags1
# For JetBrains IDEs
.idea
incus-7.3.0/.golangci.yml 0000664 0000000 0000000 00000005407 15232704312 0015233 0 ustar 00root root 0000000 0000000 version: "2"
linters:
enable:
- godot
- misspell
- revive
- whitespace
settings:
errcheck:
exclude-functions:
- (io.ReadCloser).Close
- (io.WriteCloser).Close
- (io.ReadWriteCloser).Close
- (*os.File).Close
- (*github.com/gorilla/websocket.Conn).Close
- (*github.com/mdlayher/vsock.Listener).Close
- os.Remove
- (*compress/gzip.Writer).Close
- (*github.com/fatih/color.Color).Printf
- (*github.com/fatih/color.Color).Println
revive:
rules:
- name: exported
arguments:
- checkPrivateReceivers
- disableStutteringCheck
- name: import-shadowing
- name: unchecked-type-assertion
- name: var-naming
arguments:
- []
- []
- - upperCaseConst: true
- name: early-return
- name: redundant-import-alias
- name: redefines-builtin-id
- name: struct-tag
- name: receiver-naming
- name: deep-exit
- name: defer
- name: bool-literal-in-expr
- name: comment-spacings
- name: use-any
- name: bare-return
- name: empty-block
- name: range-val-address
- name: range-val-in-closure
- name: var-declaration
- name: useless-break
- name: error-naming
- name: indent-error-flow
- name: datarace
- name: modifies-value-receiver
- name: empty-lines
- name: duplicated-imports
- name: error-return
exclusions:
generated: lax
rules:
- linters:
- revive
source: '^//generate-database:mapper '
- linters:
- revive
text: "avoid package names that conflict with Go standard library package names"
path: "^internal/io/"
- linters:
- revive
- godot
path: "^test/mini-oidc/storage/"
- linters:
- staticcheck
text: "ST1005:"
- linters:
- godot
text: "Comment should end in a period"
source: '^// Example:'
- path: internal/util/
text: "avoid meaningless package names"
- path: internal/server/util/
text: "avoid meaningless package names"
- path: shared/uefi/guid.go
linters:
- revive
text: '^(var-naming|exported):'
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/lxc/incus)
goimports:
local-prefixes:
- github.com/lxc/incus
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
incus-7.3.0/.goreleaser.yaml 0000664 0000000 0000000 00000007226 15232704312 0015742 0 ustar 00root root 0000000 0000000 version: 2
archives:
- id: incus
ids:
- incus
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.incus.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
- id: incus-agent
ids:
- incus-agent
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.incus-agent.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
- id: incus-benchmark
ids:
- incus-benchmark
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.incus-benchmark.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
- id: incus-migrate
ids:
- incus-migrate
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.incus-migrate.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
- id: incus-simplestreams
ids:
- incus-simplestreams
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.incus-simplestreams.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
before:
hooks:
- go mod download
- go mod vendor
- sh -c "git show-ref HEAD | cut -d' ' -f1 > .gitref"
- git clone --depth=1 https://github.com/cowsql/cowsql vendor/cowsql
- sh -c "cd vendor/cowsql && git show-ref HEAD | cut -d' ' -f1 > .gitref"
- rm -Rf vendor/cowsql/.git
- git clone --depth=1 https://github.com/cowsql/raft vendor/raft
- sh -c "cd vendor/raft && git show-ref HEAD | cut -d' ' -f1 > .gitref"
- rm -Rf vendor/raft/.git
- make doc
builds:
- id: incus
main: ./cmd/incus
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- amd64
- arm64
- id: incus-agent
main: ./cmd/incus-agent
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- amd64
- arm64
- id: incus-benchmark
main: ./cmd/incus-benchmark
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- amd64
- arm64
- id: incus-migrate
main: ./cmd/incus-migrate
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
- id: incus-simplestreams
main: ./cmd/incus-simplestreams
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
changelog:
use: "github-native"
checksum:
name_template: "checksums.txt"
gomod:
proxy: true
mod: mod
milestones:
- repo:
owner: lxc
name: incus
close: true
fail_on_error: false
name_template: "incus-{{ .Env.INCUS_VERSION }}"
release:
github:
owner: lxc
name: incus
name_template: "Incus {{ .Env.INCUS_VERSION }}"
extra_files:
- glob: ./installers/*.msi
- glob: ./installers/*.pkg
sboms:
- id: archive
artifacts: archive
ids:
- incus
- incus-agent
- incus-benchmark
- incus-migrate
- incus-simplestreams
- id: source
artifacts: source
source:
enabled: true
format: "tar.gz"
prefix_template: "{{ .ProjectName }}-{{ .Env.INCUS_VERSION }}/"
files:
- ".gitref"
- "doc/html"
- "vendor/*"
incus-7.3.0/.vscode/ 0000775 0000000 0000000 00000000000 15232704312 0014202 5 ustar 00root root 0000000 0000000 incus-7.3.0/.vscode/launch.json 0000664 0000000 0000000 00000003716 15232704312 0016356 0 ustar 00root root 0000000 0000000 {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// if the incusd is running, this will attach to it.
"name": "Attach to Incusd",
"type": "go",
"request": "attach",
"mode": "local",
"processId": "incusd",
"asRoot":true,
"console": "integratedTerminal"
},
{
// after running `make` to install incusd, assuming that your go/bin is in your home directory, this should launch incusd if its not a service.
// if it is an active service, you actually need to restart the service, and then attach to it.
"name": "Launch Incusd",
"type":"go",
"request": "launch",
"mode": "exec",
"asRoot": true,
"program": "${userHome}/go/bin/incusd",
"env": {
"PATH": "${env:PATH}:${userHome}/go/bin/",
"LD_LIBRARY_PATH": "${userHome}/go/deps/raft/.libs/:${userHome}/go/deps/cowsql/.libs/"
},
"args": [
"--group",
"sudo"
],
"console": "integratedTerminal",
},
{
"name": "Launch Incusd --debug",
"type":"go",
"request": "launch",
"mode": "exec",
"asRoot": true,
"program": "${userHome}/go/bin/incusd",
"env": {
"PATH": "${env:PATH}:${userHome}/go/bin/",
"LD_LIBRARY_PATH": "${userHome}/go/deps/raft/.libs/:${userHome}/go/deps/cowsql/.libs/"
},
"args": [
"--group",
"sudo",
"--debug"
],
"console": "integratedTerminal",
}
]
} incus-7.3.0/AGENTS.md 0000664 0000000 0000000 00000002245 15232704312 0014147 0 ustar 00root root 0000000 0000000 # Legal
- All contributions to this repository must be compatible with the Apache 2.0 license.
- Specifically (but not limited to), contributions cannot include code licensed under the terms of the GPL, AGPL or LGPL licenses.
- Only human beings are allowed to sign the Developer Certificate of Ownership (DCO / Signed-off-by).
- Only human beings can ever be credited within commit messages.
# Formatting
- Code comments should be no longer than one line, unless they are required to cover complex unintuitive logic.
- Commit messages should similarly be kept as short and to the point as possible, no need to summarize the whole issue.
- We don't use the define and test one line `if` syntax, instead splitting defintion and testing across two lines.
# Testing / validation
- The commit structure described in `CONTRIBUTING.md` should generally be followed.
- All branches are expected to pass `make static-analysis` and `go test -v ./...`.
- Excessive unit tests are generally discouraged.
- When possible, existing system tests should be extended to cover new features.
- A full local system test run isn't required prior to contribution, all tests get run in our CI.
incus-7.3.0/AUTHORS 0000664 0000000 0000000 00000000363 15232704312 0013713 0 ustar 00root root 0000000 0000000 Unless mentioned otherwise in a specific file's header, all code in this
project is released under the Apache 2.0 license.
The list of authors and contributors can be retrieved from the git
commit history and in some cases, the file headers.
incus-7.3.0/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006436 15232704312 0015451 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at coc@linuxcontainers.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
incus-7.3.0/CONTRIBUTING.md 0000664 0000000 0000000 00000017550 15232704312 0015102 0 ustar 00root root 0000000 0000000 # Contributing
The Incus team appreciates contributions to the project, through pull requests, issues on the [GitHub repository](https://github.com/lxc/incus/issues), or discussions or questions on the [forum](https://discuss.linuxcontainers.org).
Check the following guidelines before contributing to the project.
## Code of Conduct
When contributing, you must adhere to the Code of Conduct, which is available at: [`https://github.com/lxc/incus/blob/main/CODE_OF_CONDUCT.md`](https://github.com/lxc/incus/blob/main/CODE_OF_CONDUCT.md)
## License and copyright
By default, any contribution to this project is made under the Apache
2.0 license.
The author of a change remains the copyright holder of their code
(no copyright assignment).
## Policy on the use of Large Language Models (LLMs) and AI tooling
### For issue reporting
We do NOT allow direct filing of issues by LLMs.
We REQUIRE a human being to go through our issue reporting form on
Github and accurately describe their issue and provide all needed
information.
The more concise and to the point the issue is, the more likely it is to
be understood, tracked down and resolved quickly.
Long winded AI written essays can easily look overwhelming and cause our
maintainers and other contributors to just entirely skip the issue to
focus their energy on something else.
We also don't benefit from AI generated root cause analysis or proposed
fixes in those issues. If you yourself understand the code base well
enough to go through that content and suggested fix, then turn it into a
pull request and submit it yourself. Otherwise, please limit your report
to describing the issue at hand and we'll take it from there.
### For contributions
We REQUIRE all contributions to Incus to be submitted by human beings who
can assert full copyright ownership of their contribution or have been
allowed by their employer to contribute. This is what the DCO (see below)
requires of all contributors.
AI tools can sometimes be beneficial, particularly when it comes to
finding patterns among a large data set (entire code base), performing
tedious repetitive changes or large refactoring/re-organization.
While we now tolerate the use of such tools, they must abide by our
instructions (`AGENTS.md`) and their operators cannot override those
instructions.
We expect everyone contributing to Incus to fully own their
contribution, be able to reason about it, be able to explain why things
were done a particular way and act as the full owner of that code. AI
tools are treated the same as traditional tooling like `sed`, `awk` or
`coccinelle`.
For the purpose of this project, AI tools CANNOT be treated as author,
co-author or be credited in any way that would suggest any ownership
over the contribution.
The contributor should have done all the thinking, planning and
understanding of the changes needed to resolve an issue or implement a
new feature prior to using automated tooling to perform the grunt work.
Unguided use of those tools or the inability to prove understanding of
the code contributed will result in a loss of trust in that contributor
by project maintainers which can then lead to exclusion from any further
contribution to the project.
It's also worth pointing out that while those tools are good at
implementing the more boring/repetitive/grunt work. We've generally
found that you only really understand the project and its structure by
having done such work yourself a few times.
### For anyone with write access to the repository
Anyone with write access to this repository must ensure to NEVER run an
AI agent or similar tool on a system which holds repository credentials
(SSH key, GPG key, web browser cookies, ...).
Any use of AI tooling should be done inside of a clean VM/container that
itself cannot directly push to or alter this repository in any way.
The safest approach is to SSH into that environment and then extract the
changes using `git format-patch`, then review and apply them to your
actual tree, tweak them as needed, sign them off and then push and open
the pull request.
Any potential credential compromise or loss of control should be
immediately reported to `security@linuxcontainers.org`.
## Pull requests
Changes to this project should be proposed as pull requests on GitHub
at: [`https://github.com/lxc/incus`](https://github.com/lxc/incus)
Proposed changes will then go through review there and once approved,
be merged in the main branch.
### Commit structure
Separate commits should be used for:
- API extension (`api: Add XYZ extension`, contains `doc/api-extensions.md` and `internal/version/api.go`)
- Documentation (`doc: Update XYZ` for files in `doc/`)
- API structure (`shared/api: Add XYZ` for changes to `shared/api/`)
- Go client package (`client: Add XYZ` for changes to `client/`)
- CLI (`cmd/: Change XYZ` for changes to `cmd/`)
- Incus daemon (`incus/: Add support for XYZ` for changes to `incus/`)
- Tests (`tests: Add test for XYZ` for changes to `tests/`)
The same kind of pattern extends to the other tools in the Incus code tree
and depending on complexity, things may be split into even smaller chunks.
When updating strings in the CLI tool (`cmd/`), you may need a commit to update the templates:
make i18n
git commit -a -s -m "i18n: Update translation templates" po/
When updating API (`shared/api`), you may need a commit to update the swagger YAML:
make update-api
git commit -s -m "doc/rest-api: Refresh swagger YAML" doc/rest-api.yaml
This structure makes it easier for contributions to be reviewed and also
greatly simplifies the process of back-porting fixes to stable branches.
### Developer Certificate of Origin
To improve tracking of contributions to this project we use the DCO 1.1
and use a "sign-off" procedure for all changes going into the branch.
The sign-off is a simple line at the end of the explanation for the
commit which certifies that you wrote it or otherwise have the right
to pass it on as an open-source contribution.
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
An example of a valid sign-off line is:
```
Signed-off-by: Random J Developer
```
Use a known identity and a valid e-mail address.
Sorry, no anonymous contributions are allowed.
We also require each commit be individually signed-off by their author,
even when part of a larger set. You may find `git commit -s` useful.
## More information
For more information, see [Contributing](https://linuxcontainers.org/incus/docs/main/contributing/) in the documentation.
incus-7.3.0/COPYING 0000664 0000000 0000000 00000026136 15232704312 0013704 0 ustar 00root root 0000000 0000000
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
incus-7.3.0/Makefile 0000664 0000000 0000000 00000034455 15232704312 0014314 0 ustar 00root root 0000000 0000000 GO ?= go
DOMAIN=incus
POFILES=$(wildcard po/*.po)
MOFILES=$(patsubst %.po,%.mo,$(POFILES))
LINGUAS=$(basename $(POFILES))
POTFILE=po/$(DOMAIN).pot
VERSION=$(or ${CUSTOM_VERSION},$(shell grep "var Version" internal/version/flex.go | cut -d'"' -f2))
ARCHIVE=incus-$(VERSION).tar
HASH := \#
TAG_SQLITE3=$(shell printf "$(HASH)include \nvoid main(){cowsql_node_id n = 1;}" | $(CC) ${CGO_CFLAGS} -o /dev/null -xc - >/dev/null 2>&1 && echo "libsqlite3")
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
OVN_MINVER=23.03.0
OVS_MINVER=2.15.0
ifneq "$(wildcard vendor)" ""
RAFT_PATH=$(CURDIR)/vendor/raft
COWSQL_PATH=$(CURDIR)/vendor/cowsql
else
RAFT_PATH=$(GOPATH)/deps/raft
COWSQL_PATH=$(GOPATH)/deps/cowsql
endif
# section(Build): Build Incus
.PHONY: default
default: build
.PHONY: build
# doc: Build all Incus binaries (same as make and make default)
build:
ifeq "$(TAG_SQLITE3)" ""
@echo "Missing cowsql, run \"make deps\" to setup."
exit 1
endif
CC="$(CC)" CGO_LDFLAGS_ALLOW="$(CGO_LDFLAGS_ALLOW)" $(GO) install -v -tags "$(TAG_SQLITE3)" $(DEBUG) ./...
CGO_ENABLED=0 $(GO) install -v -tags netgo ./cmd/incus-migrate
CGO_ENABLED=0 $(GO) install -v -tags agent,netgo ./cmd/incus-agent
@echo "Incus built successfully"
.PHONY: client
# doc: Build the Incus client
client:
$(GO) install -v -tags "$(TAG_SQLITE3)" $(DEBUG) ./cmd/incus
@echo "Incus client built successfully"
.PHONY: incus-agent
# doc: Build the Incus agent
incus-agent:
CGO_ENABLED=0 $(GO) install -v -tags agent,netgo ./cmd/incus-agent
@echo "Incus agent built successfully"
.PHONY: incus-migrate
# doc: Build the Incus migration tool
incus-migrate:
CGO_ENABLED=0 $(GO) install -v -tags netgo ./cmd/incus-migrate
@echo "Incus migration tool built successfully"
.PHONY: debug
# doc: Build Incus in debug mode
debug:
ifeq "$(TAG_SQLITE3)" ""
@echo "Missing custom libsqlite3, run \"make deps\" to setup."
exit 1
endif
CC="$(CC)" CGO_LDFLAGS_ALLOW="$(CGO_LDFLAGS_ALLOW)" $(GO) install -v -tags "$(TAG_SQLITE3) logdebug" $(DEBUG) ./...
CGO_ENABLED=0 $(GO) install -v -tags "netgo,logdebug" ./cmd/incus-migrate
CGO_ENABLED=0 $(GO) install -v -tags "agent,netgo,logdebug" ./cmd/incus-agent
@echo "Incus built successfully"
.PHONY: nocache
# doc: Build Incus ignoring the local Go cache
nocache:
ifeq "$(TAG_SQLITE3)" ""
@echo "Missing custom libsqlite3, run \"make deps\" to setup."
exit 1
endif
CC="$(CC)" CGO_LDFLAGS_ALLOW="$(CGO_LDFLAGS_ALLOW)" $(GO) install -a -v -tags "$(TAG_SQLITE3)" $(DEBUG) ./...
CGO_ENABLED=0 $(GO) install -a -v -tags netgo ./cmd/incus-migrate
CGO_ENABLED=0 $(GO) install -a -v -tags agent,netgo ./cmd/incus-agent
@echo "Incus built successfully"
.PHONY: race
# doc: Build Incus in race condition detection mode
race:
ifeq "$(TAG_SQLITE3)" ""
@echo "Missing custom libsqlite3, run \"make deps\" to setup."
exit 1
endif
CC="$(CC)" CGO_LDFLAGS_ALLOW="$(CGO_LDFLAGS_ALLOW)" $(GO) install -race -v -tags "$(TAG_SQLITE3)" $(DEBUG) ./...
CGO_ENABLED=0 $(GO) install -v -tags netgo ./cmd/incus-migrate
CGO_ENABLED=0 $(GO) install -v -tags agent,netgo ./cmd/incus-agent
@echo "Incus built successfully"
# section(Dependencies): Manage Incus dependencies
.PHONY: deps
# doc: Build Incus dependencies
deps:
@if [ ! -e "$(RAFT_PATH)" ]; then \
git clone --depth=1 "https://github.com/cowsql/raft" "$(RAFT_PATH)"; \
elif [ -e "$(RAFT_PATH)/.git" ]; then \
cd "$(RAFT_PATH)"; git pull; \
fi
cd "$(RAFT_PATH)" && \
autoreconf -i && \
./configure && \
make
# cowsql
@if [ ! -e "$(COWSQL_PATH)" ]; then \
git clone --depth=1 "https://github.com/cowsql/cowsql" "$(COWSQL_PATH)"; \
elif [ -e "$(COWSQL_PATH)/.git" ]; then \
cd "$(COWSQL_PATH)"; git pull; \
fi
cd "$(COWSQL_PATH)" && \
autoreconf -i && \
PKG_CONFIG_PATH="$(RAFT_PATH)" ./configure && \
make CFLAGS="-I$(RAFT_PATH)/include/" LDFLAGS="-L$(RAFT_PATH)/.libs/"
# environment
@echo ""
@echo "Please set the following in your environment (possibly ~/.bashrc)"
@echo "export CGO_CFLAGS=\"-I$(RAFT_PATH)/include/ -I$(COWSQL_PATH)/include/\""
@echo "export CGO_LDFLAGS=\"-L$(RAFT_PATH)/.libs -L$(COWSQL_PATH)/.libs/\""
@echo "export LD_LIBRARY_PATH=\"$(RAFT_PATH)/.libs/:$(COWSQL_PATH)/.libs/\""
@echo "export CGO_LDFLAGS_ALLOW=\"(-Wl,-wrap,pthread_create)|(-Wl,-z,now)\""
.PHONY: update-gomod
# doc: Update Go dependencies
update-gomod:
ifneq "$(INCUS_OFFLINE)" ""
@echo "The update-gomod target cannot be run in offline mode."
exit 1
endif
$(GO) get -t -v -u ./...
$(GO) mod tidy --go=1.25.12
$(GO) get toolchain@none
@echo "Dependencies updated"
# section(Schemas): Update Incus data schemas
.PHONY: update-ovsdb
# doc: Update OVSDB schema
update-ovsdb:
go install github.com/ovn-kubernetes/libovsdb/cmd/modelgen@main
rm -Rf internal/server/network/ovs/schema
mkdir internal/server/network/ovs/schema
curl -s https://raw.githubusercontent.com/openvswitch/ovs/v$(OVS_MINVER)/vswitchd/vswitch.ovsschema -o internal/server/network/ovs/schema/ovs.json
modelgen -o internal/server/network/ovs/schema/ovs internal/server/network/ovs/schema/ovs.json
rm internal/server/network/ovs/schema/*.json
rm -Rf internal/server/network/ovn/schema
mkdir internal/server/network/ovn/schema
curl -s https://raw.githubusercontent.com/ovn-org/ovn/v$(OVN_MINVER)/ovn-nb.ovsschema -o internal/server/network/ovn/schema/ovn-nb.json
curl -s https://raw.githubusercontent.com/ovn-org/ovn/v$(OVN_MINVER)/ovn-sb.ovsschema -o internal/server/network/ovn/schema/ovn-sb.json
curl -s https://raw.githubusercontent.com/ovn-org/ovn/v$(OVN_MINVER)/ovn-ic-nb.ovsschema -o internal/server/network/ovn/schema/ovn-ic-nb.json
curl -s https://raw.githubusercontent.com/ovn-org/ovn/v$(OVN_MINVER)/ovn-ic-sb.ovsschema -o internal/server/network/ovn/schema/ovn-ic-sb.json
modelgen -o internal/server/network/ovn/schema/ovn-nb internal/server/network/ovn/schema/ovn-nb.json
modelgen -o internal/server/network/ovn/schema/ovn-sb internal/server/network/ovn/schema/ovn-sb.json
modelgen -o internal/server/network/ovn/schema/ovn-ic-nb internal/server/network/ovn/schema/ovn-ic-nb.json
modelgen -o internal/server/network/ovn/schema/ovn-ic-sb internal/server/network/ovn/schema/ovn-ic-sb.json
rm internal/server/network/ovn/schema/*.json
.PHONY: update-protobuf
# doc: Update Protobuf schema
update-protobuf:
protoc --go_out=. ./internal/migration/migrate.proto
.PHONY: update-schema
# doc: Update database schema
update-schema:
cd cmd/generate-database && $(GO) build -o $(GOPATH)/bin/generate-database -tags "$(TAG_SQLITE3)" $(DEBUG) && cd -
$(GO) generate ./...
gofumpt -w ./internal/server/db/
goimports -w ./internal/server/db/
@echo "Code generation completed"
.PHONY: update-api
# doc: Update API schema
update-api:
ifeq "$(INCUS_OFFLINE)" ""
(cd / ; $(GO) install -v -x github.com/go-swagger/go-swagger/cmd/swagger@master)
endif
swagger generate spec -o doc/rest-api.yaml -w ./cmd/incusd -m
.PHONY: update-metadata
# doc: Update configuration metadata
update-metadata: build
@echo "Generating golang documentation metadata"
cd cmd/generate-config && CGO_ENABLED=0 $(GO) build -o $(GOPATH)/bin/generate-config
$(GOPATH)/bin/generate-config . --json ./internal/server/metadata/configuration.json --txt ./doc/config_options.txt
# OpenFGA Syntax Transformer: https://github.com/openfga/syntax-transformer
.PHONY: update-openfga
# doc: Update OpenFGA schema
update-openfga:
ifeq ($(shell command -v fga),)
(cd / ; $(GO) install -v -x github.com/openfga/cli/cmd/fga@latest)
endif
@printf 'package auth\n\n// Code generated by Makefile; DO NOT EDIT.\n\nvar authModel = `%s`\n' '$(shell fga model transform --file=./internal/server/auth/driver_openfga_model.openfga | jq -c)' > ./internal/server/auth/driver_openfga_model.go
# section(Documentation): Build Incus documentation
.PHONY: doc
# doc: Setup the build environment and build the documentation
doc: doc-setup doc-incremental
.PHONY: doc-setup
# doc: Setup a documentation build environment
doc-setup: client
@echo "Setting up documentation build environment"
python3 -m venv doc/.sphinx/venv
. $(SPHINXENV) ; pip install --require-virtualenv --upgrade -r doc/.sphinx/requirements.txt --log doc/.sphinx/venv/pip_install.log
@test ! -f doc/.sphinx/venv/pip_list.txt || \
mv doc/.sphinx/venv/pip_list.txt doc/.sphinx/venv/pip_list.txt.bak
$(SPHINXPIPPATH) list --local --format=freeze > doc/.sphinx/venv/pip_list.txt
find doc/reference/manpages/ -name "*.md" -type f -delete
rm -Rf doc/html
rm -Rf doc/.sphinx/.doctrees
.PHONY: doc-incremental
# doc: Build the documentation
doc-incremental:
@echo "Build the documentation"
. $(SPHINXENV) ; NO_COLOR=1 sphinx-build -c doc/ -b dirhtml doc/ doc/html/ -d doc/.sphinx/.doctrees -w doc/.sphinx/warnings.txt
.PHONY: doc-serve
# doc: Serve the documentation on localhost:8001
doc-serve:
cd doc/html; python3 -m http.server 8001
.PHONY: doc-spellcheck
# doc: Check spelling errors on the documentation
doc-spellcheck: doc
. $(SPHINXENV) ; python3 -m pyspelling -c doc/.sphinx/spellingcheck.yaml
.PHONY: doc-spellcheck-incremental
# doc: Check spelling errors on the documentation, building the documentation only
doc-spellcheck-incremental: doc-incremental
. $(SPHINXENV) ; python3 -m pyspelling -c doc/.sphinx/spellingcheck.yaml
.PHONY: doc-linkcheck
# doc: Check broken links on the documentation
doc-linkcheck: doc-setup
. $(SPHINXENV) ; LOCAL_SPHINX_BUILD=True sphinx-build -c doc/ -b linkcheck doc/ doc/html/ -d doc/.sphinx/.doctrees
.PHONY: doc-lint
# doc: Lint the documentation
doc-lint:
doc/.sphinx/.markdownlint/doc-lint.sh
.PHONY: woke-install
# doc: Install the inclusive checker
woke-install:
@type woke >/dev/null 2>&1 || \
{ echo "Installing \"woke\" snap... \n"; sudo snap install woke; }
.PHONY: doc-woke
# doc: Check for non-inclusive phrasing
doc-woke: woke-install
woke *.md **/*.md -c https://github.com/canonical/Inclusive-naming/raw/main/config.yml
# section(Tests): Run the tests
.PHONY: check
# doc: Run the test suite
check: default
ifeq "$(INCUS_OFFLINE)" ""
(cd / ; $(GO) install -v -x github.com/rogpeppe/godeps@latest)
(cd / ; $(GO) install -v -x github.com/tsenart/deadcode@latest)
(cd / ; $(GO) install -v -x golang.org/x/lint/golint@latest)
endif
CGO_LDFLAGS_ALLOW="$(CGO_LDFLAGS_ALLOW)" $(GO) test -v -tags "$(TAG_SQLITE3)" $(DEBUG) ./...
cd test && ./main.sh
.PHONY: static-analysis
# doc: Run static analysis
static-analysis:
ifeq ($(shell command -v go-licenses),)
(cd / ; $(GO) install -v -x github.com/google/go-licenses@latest)
endif
ifeq ($(shell command -v govulncheck),)
go install golang.org/x/vuln/cmd/govulncheck@latest
endif
ifeq ($(shell command -v golangci-lint),)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $$($(GO) env GOPATH)/bin
endif
ifeq ($(shell command -v shellcheck),)
echo "Please install shellcheck"
exit 1
endif
ifeq ($(shell command -v flake8),)
echo "Please install flake8"
exit 1
endif
ifeq ($(shell command -v codespell),)
echo "Please install codespell"
exit 1
endif
ifeq ($(shell command -v run-parts),)
echo "Please install run-parts"
exit 1
endif
flake8 test/deps/import-busybox
shellcheck --shell sh test/*.sh test/includes/*.sh test/suites/*.sh test/backends/*.sh test/lint/*.sh
shellcheck test/extras/*.sh
run-parts $(shell run-parts -V >/dev/null 2>&1 && echo -n "--verbose --exit-on-error --regex '.sh'") test/lint
.PHONY: staticcheck
# doc: Run static checks
staticcheck:
ifeq ($(shell command -v staticcheck),)
(cd / ; $(GO) install -v -x honnef.co/go/tools/cmd/staticcheck@latest)
endif
# To get advance notice of deprecated function usage, consider running:
# sed -i 's/^go 1\.[0-9]\+$/go 1.18/' go.mod
# before 'make staticcheck'.
# Run staticcheck against all the dirs containing Go files.
staticcheck $$(git ls-files *.go | sed 's|^|./|; s|/[^/]\+\.go$$||' | sort -u)
.PHONY: unit-test
# doc: Run unit tests
unit-test:
sudo --preserve-env=CGO_CFLAGS,CGO_LDFLAGS,CGO_LDFLAGS_ALLOW,LD_LIBRARY_PATH LD_LIBRARY_PATH=${LD_LIBRARY_PATH} env "PATH=${PATH}" $(GO) test ./...
# section(Internationalization): Generate internationalization files
.PHONY: i18n
# doc: Generate internationalization files
i18n: update-pot update-po
po/%.mo: po/%.po
msgfmt --statistics -o $@ $<
po/%.po: po/$(DOMAIN).pot
msgmerge -U po/$*.po po/$(DOMAIN).pot
.PHONY: update-po
# doc: Update PO files
update-po:
set -eu; \
for lang in $(LINGUAS); do\
msgmerge --backup=none -U $$lang.po po/$(DOMAIN).pot; \
done
.PHONY: update-pot
# doc: Update POT file
update-pot:
ifeq "$(INCUS_OFFLINE)" ""
(cd / ; $(GO) install -v -x github.com/snapcore/snapd/i18n/xgettext-go@2.57.1)
endif
xgettext-go -o po/$(DOMAIN).pot --add-comments-tag=TRANSLATORS: --sort-output --package-name=$(DOMAIN) --msgid-bugs-address=lxc-devel@lists.linuxcontainers.org --keyword=i18n.G --keyword-plural=i18n.NG cmd/incus/*.go cmd/incus/color/*.go cmd/incus/usage/*.go shared/cliconfig/*.go
sed -i s/CHARSET/UTF-8/ po/$(DOMAIN).pot
.PHONY: build-mo
# doc! Build MO files
build-mo: $(MOFILES)
# section(Miscellaneous): Targets that don’t fit in any category
.PHONY: dist
# doc: Prepare a release tarball
dist: doc
# Cleanup
rm -Rf $(ARCHIVE).xz
# Create build dir
$(eval TMP := $(shell mktemp -d))
git archive --prefix=incus-$(VERSION)/ HEAD | tar -x -C $(TMP)
git show-ref HEAD | cut -d' ' -f1 > $(TMP)/incus-$(VERSION)/.gitref
# Download dependencies
(cd $(TMP)/incus-$(VERSION) ; $(GO) mod vendor)
# Download the cowsql libraries
git clone --depth=1 https://github.com/cowsql/cowsql $(TMP)/incus-$(VERSION)/vendor/cowsql
(cd $(TMP)/incus-$(VERSION)/vendor/cowsql ; git show-ref HEAD | cut -d' ' -f1 > .gitref)
git clone --depth=1 https://github.com/cowsql/raft $(TMP)/incus-$(VERSION)/vendor/raft
(cd $(TMP)/incus-$(VERSION)/vendor/raft ; git show-ref HEAD | cut -d' ' -f1 > .gitref)
# Copy doc output
cp -r doc/html $(TMP)/incus-$(VERSION)/doc/html/
# Assemble tarball
tar --exclude-vcs -C $(TMP) -Jcf $(ARCHIVE).xz incus-$(VERSION)/
# Cleanup
rm -Rf $(TMP)
.PHONY: help
# doc: Show this help
help:
@echo The following targets are supported:
@sed -En 's/^#\s*section\(([^)]*)\):\s*(.*)$$/\n\x1b[1m\1:\x1b[0m \2/p;/^\.PHONY:/{N;N;s/^\.PHONY:\s*([^[:space:]]+)\n#\s*doc(:\s*(.*)\n\1:\s*$$|!\s*(.*)\n\1:[^\n]*)/ \1!\3\4/p;s/^\.PHONY:\s*([^[:space:]]+)\s*\n#\s*doc:\s*(.*)\n\1:\s*(.+)$$/ \1!\2 (runs \3)/p}' Makefile | awk -F! '{if(NF<2)print$$1;else{s=$$1;if(length(s)%2)s=s" ";while(length(s)<28)s=s" .";print s" "$$2}}'
incus-7.3.0/README.md 0000664 0000000 0000000 00000012216 15232704312 0014122 0 ustar 00root root 0000000 0000000 # Incus
Incus is a modern, secure and powerful system container and virtual machine manager.
It provides a unified experience for running and managing full Linux systems inside containers or virtual machines. Incus supports images for a large number of Linux distributions (official Ubuntu images and images provided by the community) and is built around a very powerful, yet pretty simple, REST API. Incus scales from one instance on a single machine to a cluster in a full data center rack, making it suitable for running workloads both for development and in production.
Incus allows you to easily set up a system that feels like a small private cloud. You can run any type of workload in an efficient way while keeping your resources optimized.
You should consider using Incus if you want to containerize different environments or run virtual machines, or in general run and manage your infrastructure in a cost-effective way.
You can try Incus online at: [`https://linuxcontainers.org/incus/try-it/`](https://linuxcontainers.org/incus/try-it/)
## Project history
Incus, which is named after the [Cumulonimbus incus](https://en.wikipedia.org/wiki/Cumulonimbus_incus) or anvil cloud
started as a community fork of Canonical's LXD following [Canonical's takeover](https://linuxcontainers.org/lxd/) of the LXD project from the
Linux Containers community.
The project was then adopted by the Linux Containers community, taking back the spot left empty by LXD's departure.
Incus is a true open source community project, free of any [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement) and
remains released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
It's maintained by the same team of developers that first created LXD.
## Get started
See [Getting started](https://linuxcontainers.org/incus/docs/main/tutorial/first_steps/) in the Incus documentation for installation instructions and first steps.
- Release announcements: [`https://discuss.linuxcontainers.org/c/news/`](https://discuss.linuxcontainers.org/c/news/)
- Release tarballs: [`https://github.com/lxc/incus/releases/`](https://github.com/lxc/incus/releases/)
- Documentation: [`https://linuxcontainers.org/incus/docs/main/`](https://linuxcontainers.org/incus/docs/main/)
## Status
Type | Service | Status
--- | --- | ---
Tests | GitHub | [](https://github.com/lxc/incus/actions?query=event%3Apush+branch%3Amain)
Go documentation | Godoc | [](https://godoc.org/github.com/lxc/incus/v7/client)
Static analysis | GoReport | [](https://goreportcard.com/report/github.com/lxc/incus)
Translations | Weblate | [](https://hosted.weblate.org/projects/incus/)
## Security
Consider the following aspects to ensure that your Incus installation is secure:
- Keep your operating system up-to-date and install all available security patches.
- Use only supported Incus versions.
- Restrict access to the Incus daemon and the remote API.
- Do not use privileged containers unless required. If you use privileged containers, put appropriate security measures in place. See the [LXC security page](https://linuxcontainers.org/lxc/security/) for more information.
- Configure your network interfaces to be secure.
See [Security](https://github.com/lxc/incus/blob/main/doc/explanation/security.md) for detailed information.
**IMPORTANT:**
Local access to Incus through the Unix socket always grants full access to Incus.
This includes the ability to attach file system paths or devices to any instance as well as tweak the security features on any instance.
Therefore, you should only give such access to users who you'd trust with root access to your system.
## Support and community
The following channels are available for you to interact with the Incus community.
### Bug reports
You can file bug reports and feature requests at: [`https://github.com/lxc/incus/issues/new`](https://github.com/lxc/incus/issues/new)
### Community support
Community support is handled at: [`https://discuss.linuxcontainers.org`](https://discuss.linuxcontainers.org)
### Commercial support
Commercial support is currently available from [Zabbly](https://zabbly.com) for users of their [Debian or Ubuntu packages](https://github.com/zabbly/incus).
## Documentation
The official documentation is available at: [`https://github.com/lxc/incus/tree/main/doc`](https://github.com/lxc/incus/tree/main/doc)
## Contributing
Fixes and new features are greatly appreciated. Make sure to read our [contributing guidelines](CONTRIBUTING.md) first!
incus-7.3.0/SECURITY.md 0000664 0000000 0000000 00000002254 15232704312 0014435 0 ustar 00root root 0000000 0000000 # Security policy
## Supported versions
Incus has two types of releases:
- Feature releases
- LTS releases
For feature releases, only the latest one is supported, and we usually
don't do point releases. Instead, users are expected to wait until the
next release.
For LTS releases, we do periodic bugfix releases that include an
accumulation of bugfixes from the feature releases. Such bugfix releases
do not include new features.
## What qualifies as a security issue
We don't consider privileged containers to be root safe, so any exploit
allowing someone to escape them will not qualify as a security issue.
This doesn't mean that we're not interested in preventing such escapes,
but we simply do not consider such containers to be root safe.
Unprivileged container escapes are certainly something we'd consider a
security issue, especially if somehow facilitated by Incus.
## Reporting security issues
Security issues can be reported by e-mail to security@linuxcontainers.org.
Alternatively security issues can also be reported through Github at: https://github.com/lxc/incus/security/advisories/new
incus-7.3.0/client/ 0000775 0000000 0000000 00000000000 15232704312 0014117 5 ustar 00root root 0000000 0000000 incus-7.3.0/client/connection.go 0000664 0000000 0000000 00000031231 15232704312 0016605 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/simplestreams"
"github.com/lxc/incus/v7/shared/util"
)
// ConnectionArgs represents a set of common connection properties.
type ConnectionArgs struct {
// TLS certificate of the remote server. If not specified, the system CA is used.
TLSServerCert string
// TLS certificate to use for client authentication.
TLSClientCert string
// TLS key to use for client authentication.
TLSClientKey string
// TLS CA to validate against when in PKI mode.
TLSCA string
// User agent string
UserAgent string
// Authentication type
AuthType string
// Custom proxy
Proxy func(*http.Request) (*url.URL, error)
// Custom HTTP Client (used as base for the connection)
HTTPClient *http.Client
// TransportWrapper wraps the *http.Transport set by Incus
TransportWrapper func(*http.Transport) HTTPTransporter
// Controls whether a client verifies the server's certificate chain and host name.
InsecureSkipVerify bool
// Controls whether to perform an exact certificate match (will ignore expiry).
IdenticalCertificate bool
// Cookie jar
CookieJar http.CookieJar
// OpenID Connect tokens
OIDCTokens *oidc.Tokens[*oidc.IDTokenClaims]
// Do not block for OIDC authentication
OIDCNonInteractive bool
// Skip the event listener endpoint
SkipGetEvents bool
// Skip automatic GetServer request upon connection
SkipGetServer bool
// Caching support for image servers
CachePath string
CacheExpiry time.Duration
// Temp storage.
TempPath string
}
// ConnectIncus lets you connect to a remote Incus daemon over HTTPs.
//
// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
//
// If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectIncus(uri string, args *ConnectionArgs) (InstanceServer, error) {
return ConnectIncusWithContext(context.Background(), uri, args)
}
// ConnectIncusWithContext lets you connect to a remote Incus daemon over HTTPs with context.Context.
//
// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
//
// If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (InstanceServer, error) {
// Cleanup URL
uri = strings.TrimSuffix(uri, "/")
logger.Debug("Connecting to a remote Incus over HTTPS", logger.Ctx{"url": uri})
return httpsIncus(ctx, uri, args)
}
// ConnectIncusHTTP lets you connect to a VM agent over a VM socket.
func ConnectIncusHTTP(args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
return ConnectIncusHTTPWithContext(context.Background(), args, client)
}
// ConnectIncusHTTPWithContext lets you connect to a VM agent over a VM socket with context.Context.
func ConnectIncusHTTPWithContext(ctx context.Context, args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
logger.Debug("Connecting to a VM agent over a VM socket")
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse("https://custom.socket")
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
// Initialize the client struct
server := ProtocolIncus{
ctx: ctx,
httpBaseURL: *httpBaseURL,
httpProtocol: "custom",
httpUserAgent: args.UserAgent,
ctxConnected: ctxConnected,
ctxConnectedCancel: ctxConnectedCancel,
eventConns: make(map[string]*websocket.Conn),
eventListeners: make(map[string][]*EventListener),
skipEvents: args.SkipGetEvents,
tempPath: args.TempPath,
}
// Setup the HTTP client
server.http = client
// Test the connection and seed the server information
if !args.SkipGetServer {
serverStatus, _, err := server.GetServer()
if err != nil {
return nil, err
}
// Record the server certificate
server.httpCertificate = serverStatus.Environment.Certificate
}
return &server, nil
}
// ConnectIncusUnix lets you connect to a remote Incus daemon over a local unix socket.
//
// If the path argument is empty, then $INCUS_SOCKET will be used, if
// unset $INCUS_DIR/unix.socket will be used and if that one isn't set
// either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket.
func ConnectIncusUnix(path string, args *ConnectionArgs) (InstanceServer, error) {
return ConnectIncusUnixWithContext(context.Background(), path, args)
}
// ConnectIncusUnixWithContext lets you connect to a remote Incus daemon over a local unix socket with context.Context.
//
// If the path argument is empty, then $INCUS_SOCKET will be used, if
// unset $INCUS_DIR/unix.socket will be used and if that one isn't set
// either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket.
func ConnectIncusUnixWithContext(ctx context.Context, path string, args *ConnectionArgs) (InstanceServer, error) {
logger.Debug("Connecting to a local Incus over a Unix socket")
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse("http://unix.socket")
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
// Determine the socket path
var projectName string
if path == "" {
path = os.Getenv("INCUS_SOCKET")
if path == "" {
incusDir := os.Getenv("INCUS_DIR")
if incusDir == "" {
_, err := os.Lstat("/run/incus/unix.socket")
if err == nil {
incusDir = "/run/incus"
} else {
incusDir = "/var/lib/incus"
}
}
path = filepath.Join(incusDir, "unix.socket")
userPath := filepath.Join(incusDir, "unix.socket.user")
if !util.PathIsWritable(path) && util.PathIsWritable(userPath) {
// Handle the use of incus-user.
path = userPath
// When using incus-user, the project list is typically restricted.
// So let's try to be smart about the project we're using.
projectName = fmt.Sprintf("user-%d", os.Geteuid())
}
}
}
// Initialize the client struct
server := ProtocolIncus{
ctx: ctx,
httpBaseURL: *httpBaseURL,
httpUnixPath: path,
httpProtocol: "unix",
httpUserAgent: args.UserAgent,
ctxConnected: ctxConnected,
ctxConnectedCancel: ctxConnectedCancel,
eventConns: make(map[string]*websocket.Conn),
eventListeners: make(map[string][]*EventListener),
skipEvents: args.SkipGetEvents,
project: projectName,
tempPath: args.TempPath,
}
// Setup the HTTP client
httpClient, err := unixHTTPClient(args, path)
if err != nil {
return nil, err
}
server.http = httpClient
// Test the connection and seed the server information
if !args.SkipGetServer {
serverStatus, _, err := server.GetServer()
if err != nil {
return nil, err
}
// Record the server certificate
server.httpCertificate = serverStatus.Environment.Certificate
}
return &server, nil
}
// ConnectPublicIncus lets you connect to a remote public Incus daemon over HTTPs.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectPublicIncus(uri string, args *ConnectionArgs) (ImageServer, error) {
return ConnectPublicIncusWithContext(context.Background(), uri, args)
}
// ConnectPublicIncusWithContext lets you connect to a remote public Incus daemon over HTTPs with context.Context.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectPublicIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote public Incus over HTTPS")
// Cleanup URL
uri = strings.TrimSuffix(uri, "/")
return httpsIncus(ctx, uri, args)
}
// ConnectSimpleStreams lets you connect to a remote SimpleStreams image server over HTTPs.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectSimpleStreams(uri string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote simplestreams server", logger.Ctx{"URL": uri})
// Cleanup URL
uri = strings.TrimSuffix(uri, "/")
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
}
// Initialize the client struct
server := ProtocolSimpleStreams{
httpHost: uri,
httpUserAgent: args.UserAgent,
httpCertificate: args.TLSServerCert,
tempPath: args.TempPath,
}
// Setup the HTTP client
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper)
if err != nil {
return nil, err
}
server.http = httpClient
// Get simplestreams client
ssClient := simplestreams.NewClient(uri, *httpClient, args.UserAgent)
server.ssClient = ssClient
// Setup the cache
if args.CachePath != "" {
if !util.PathExists(args.CachePath) {
return nil, fmt.Errorf("Cache directory %q doesn't exist", args.CachePath)
}
hashedURL := fmt.Sprintf("%x", sha256.Sum256([]byte(uri)))
cachePath := filepath.Join(args.CachePath, hashedURL)
cacheExpiry := args.CacheExpiry
if cacheExpiry == 0 {
cacheExpiry = time.Hour
}
if !util.PathExists(cachePath) {
err := os.Mkdir(cachePath, 0o755)
if err != nil {
return nil, err
}
}
ssClient.SetCache(cachePath, cacheExpiry)
}
return &server, nil
}
// ConnectOCI lets you connect to a remote OCI image registry over HTTPs.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectOCI(uri string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote OCI server", logger.Ctx{"URL": uri})
// Cleanup URL
uri = strings.TrimSuffix(uri, "/")
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
}
// Initialize the client struct
server := ProtocolOCI{
httpHost: uri,
httpUserAgent: args.UserAgent,
httpCertificate: args.TLSServerCert,
cache: map[string]ociInfo{},
errors: map[string]error{},
tempPath: args.TempPath,
}
// Setup the HTTP client
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper)
if err != nil {
return nil, err
}
server.http = httpClient
return &server, nil
}
// Internal function called by ConnectIncus and ConnectPublicIncus.
func httpsIncus(ctx context.Context, requestURL string, args *ConnectionArgs) (InstanceServer, error) {
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse(requestURL)
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
// Initialize the client struct
server := ProtocolIncus{
ctx: ctx,
httpCertificate: args.TLSServerCert,
httpBaseURL: *httpBaseURL,
httpProtocol: "https",
httpUserAgent: args.UserAgent,
ctxConnected: ctxConnected,
ctxConnectedCancel: ctxConnectedCancel,
eventConns: make(map[string]*websocket.Conn),
eventListeners: make(map[string][]*EventListener),
skipEvents: args.SkipGetEvents,
tempPath: args.TempPath,
}
if slices.Contains([]string{api.AuthenticationMethodOIDC}, args.AuthType) {
server.RequireAuthenticated(true)
}
// Setup the HTTP client
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper)
if err != nil {
return nil, err
}
if args.CookieJar != nil {
httpClient.Jar = args.CookieJar
}
server.http = httpClient
if args.AuthType == api.AuthenticationMethodOIDC {
server.setupOIDCClient(args.OIDCTokens, args.OIDCNonInteractive)
}
// Test the connection and seed the server information
if !args.SkipGetServer {
_, _, err := server.GetServer()
if err != nil {
return nil, err
}
}
return &server, nil
}
incus-7.3.0/client/doc.go 0000664 0000000 0000000 00000006736 15232704312 0015227 0 ustar 00root root 0000000 0000000 // Package incus implements a client for the Incus API
//
// # Overview
//
// This package lets you connect to Incus daemons or SimpleStream image
// servers over a Unix socket or HTTPs. You can then interact with those
// remote servers, creating instances, images, moving them around, ...
//
// The following examples make use of several imports:
//
// import (
// "github.com/lxc/incus/client"
// "github.com/lxc/incus/shared/api"
// "github.com/lxc/incus/shared/termios"
// )
//
// # Example - instance creation
//
// This creates a container on a local Incus daemon and then starts it.
//
// // Connect to Incus over the Unix socket
// c, err := incus.ConnectIncusUnix("", nil)
// if err != nil {
// return err
// }
//
// // Instance creation request
// name := "my-container"
// req := api.InstancesPost{
// Name: name,
// Source: api.InstanceSource{
// Type: "image",
// Alias: "my-image", # e.g. alpine/3.20
// Server: "https://images.linuxcontainers.org",
// Protocol: "simplestreams",
// },
// Type: "container"
// }
//
// // Get Incus to create the instance (background operation)
// op, err := c.CreateInstance(req)
// if err != nil {
// return err
// }
//
// // Wait for the operation to complete
// err = op.Wait()
// if err != nil {
// return err
// }
//
// // Get Incus to start the instance (background operation)
// reqState := api.InstanceStatePut{
// Action: "start",
// Timeout: -1,
// }
//
// op, err = c.UpdateInstanceState(name, reqState, "")
// if err != nil {
// return err
// }
//
// // Wait for the operation to complete
// err = op.Wait()
// if err != nil {
// return err
// }
//
// # Example - command execution
//
// This executes an interactive bash terminal
//
// // Connect to Incus over the Unix socket
// c, err := incus.ConnectIncusUnix("", nil)
// if err != nil {
// return err
// }
//
// // Setup the exec request
// req := api.InstanceExecPost{
// Command: []string{"bash"},
// WaitForWS: true,
// Interactive: true,
// Width: 80,
// Height: 15,
// }
//
// // Setup the exec arguments (fds)
// args := incus.InstanceExecArgs{
// Stdin: os.Stdin,
// Stdout: os.Stdout,
// Stderr: os.Stderr,
// }
//
// // Setup the terminal (set to raw mode)
// if req.Interactive {
// cfd := int(syscall.Stdin)
// oldttystate, err := termios.MakeRaw(cfd)
// if err != nil {
// return err
// }
//
// defer termios.Restore(cfd, oldttystate)
// }
//
// // Get the current state
// op, err := c.ExecInstance(name, req, &args)
// if err != nil {
// return err
// }
//
// // Wait for it to complete
// err = op.Wait()
// if err != nil {
// return err
// }
//
// # Example - image copy
//
// This copies an image from a simplestreams server to a local Incus daemon
//
// // Connect to Incus over the Unix socket
// c, err := incus.ConnectIncusUnix("", nil)
// if err != nil {
// return err
// }
//
// // Connect to the remote SimpleStreams server
// d, err = incus.ConnectSimpleStreams("https://images.linuxcontainers.org", nil)
// if err != nil {
// return err
// }
//
// // Resolve the alias
// alias, _, err := d.GetImageAlias("centos/7")
// if err != nil {
// return err
// }
//
// // Get the image information
// image, _, err := d.GetImage(alias.Target)
// if err != nil {
// return err
// }
//
// // Ask Incus to copy the image from the remote server
// op, err := d.CopyImage(*image, c, nil)
// if err != nil {
// return err
// }
//
// // And wait for it to finish
// err = op.Wait()
// if err != nil {
// return err
// }
package incus
incus-7.3.0/client/events.go 0000664 0000000 0000000 00000005413 15232704312 0015755 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"errors"
"sync"
"github.com/lxc/incus/v7/shared/api"
)
// The EventListener struct is used to interact with an Incus event stream.
type EventListener struct {
r *ProtocolIncus
ctx context.Context
ctxCancel context.CancelFunc
err error
// projectName stores which project this event listener is associated with (empty for all projects).
projectName string
targets []*EventTarget
targetsLock sync.Mutex
}
// The EventTarget struct is returned to the caller of AddHandler and used in RemoveHandler.
type EventTarget struct {
function func(api.Event)
types []string
}
// AddHandler adds a function to be called whenever an event is received.
func (e *EventListener) AddHandler(types []string, function func(api.Event)) (*EventTarget, error) {
if function == nil {
return nil, errors.New("A valid function must be provided")
}
// Handle locking
e.targetsLock.Lock()
defer e.targetsLock.Unlock()
// Create a new target
target := EventTarget{
function: function,
types: types,
}
// And add it to the targets
e.targets = append(e.targets, &target)
return &target, nil
}
// RemoveHandler removes a function to be called whenever an event is received.
func (e *EventListener) RemoveHandler(target *EventTarget) error {
if target == nil {
return errors.New("A valid event target must be provided")
}
// Handle locking
e.targetsLock.Lock()
defer e.targetsLock.Unlock()
// Locate and remove the function from the list
for i, entry := range e.targets {
if entry == target {
copy(e.targets[i:], e.targets[i+1:])
e.targets[len(e.targets)-1] = nil
e.targets = e.targets[:len(e.targets)-1]
return nil
}
}
return errors.New("Couldn't find this function and event types combination")
}
// Disconnect must be used once done listening for events.
func (e *EventListener) Disconnect() {
// Handle locking
e.r.eventListenersLock.Lock()
defer e.r.eventListenersLock.Unlock()
if e.ctx.Err() != nil {
return
}
// Locate and remove it from the global list
for i, listener := range e.r.eventListeners[e.projectName] {
if listener == e {
copy(e.r.eventListeners[e.projectName][i:], e.r.eventListeners[e.projectName][i+1:])
e.r.eventListeners[e.projectName][len(e.r.eventListeners[e.projectName])-1] = nil
e.r.eventListeners[e.projectName] = e.r.eventListeners[e.projectName][:len(e.r.eventListeners[e.projectName])-1]
break
}
}
// Turn off the handler
e.err = nil
e.ctxCancel()
}
// Wait blocks until the server disconnects the connection or Disconnect() is called.
func (e *EventListener) Wait() error {
<-e.ctx.Done()
return e.err
}
// IsActive returns true if this listener is still connected, false otherwise.
func (e *EventListener) IsActive() bool {
return e.ctx.Err() == nil
}
incus-7.3.0/client/incus.go 0000664 0000000 0000000 00000040211 15232704312 0015565 0 ustar 00root root 0000000 0000000 package incus
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
neturl "net/url"
"slices"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/tcp"
)
// ProtocolIncus represents an Incus API server.
type ProtocolIncus struct {
ctx context.Context
server *api.Server
ctxConnected context.Context
ctxConnectedCancel context.CancelFunc
// eventConns contains event listener connections associated to a project name (or empty for all projects).
eventConns map[string]*websocket.Conn
// eventConnsLock controls write access to the eventConns.
eventConnsLock sync.Mutex
// eventListeners is a slice of event listeners associated to a project name (or empty for all projects).
eventListeners map[string][]*EventListener
eventListenersLock sync.Mutex
// skipEvents tracks whether we were configured not to connect to the events endpoint
skipEvents bool
http *http.Client
httpCertificate string
httpBaseURL neturl.URL
httpUnixPath string
httpProtocol string
httpUserAgent string
requireAuthenticated bool
clusterTarget string
project string
oidcClient *oidcClient
tempPath string
}
// Disconnect gets rid of any background goroutines.
func (r *ProtocolIncus) Disconnect() {
if r.ctxConnected.Err() != nil {
r.ctxConnectedCancel()
}
}
// GetConnectionInfo returns the basic connection information used to interact with the server.
func (r *ProtocolIncus) GetConnectionInfo() (*ConnectionInfo, error) {
info := ConnectionInfo{}
info.Certificate = r.httpCertificate
info.Protocol = "incus"
info.URL = r.httpBaseURL.String()
info.SocketPath = r.httpUnixPath
info.Project = r.project
if info.Project == "" {
info.Project = api.ProjectDefaultName
}
info.Target = r.clusterTarget
if info.Target == "" && r.server != nil {
info.Target = r.server.Environment.ServerName
}
urls := []string{}
if r.httpProtocol == "https" {
urls = append(urls, r.httpBaseURL.String())
}
if r.server != nil && len(r.server.Environment.Addresses) > 0 {
for _, addr := range r.server.Environment.Addresses {
if strings.HasPrefix(addr, ":") {
continue
}
url := fmt.Sprintf("https://%s", addr)
if !slices.Contains(urls, url) {
urls = append(urls, url)
}
}
}
info.Addresses = urls
return &info, nil
}
// isSameServer compares the calling ProtocolIncus object with the provided server object to check if they are the same server.
// It verifies the equality based on their connection information (Protocol, Certificate, Project, and Target).
func (r *ProtocolIncus) isSameServer(server Server) bool {
// Short path checking if the two structs are identical.
if r == server {
return true
}
// Short path if either of the structs are nil.
if r == nil || server == nil {
return false
}
// When dealing with uninitialized servers, we can't safely compare.
if r.server == nil {
return false
}
// Get the connection info from both servers.
srcInfo, err := r.GetConnectionInfo()
if err != nil {
return false
}
dstInfo, err := server.GetConnectionInfo()
if err != nil {
return false
}
// Check whether we're dealing with the same server.
return srcInfo.Protocol == dstInfo.Protocol && srcInfo.Certificate == dstInfo.Certificate &&
srcInfo.Project == dstInfo.Project && srcInfo.Target == dstInfo.Target
}
// GetHTTPClient returns the http client used for the connection. This can be used to set custom http options.
func (r *ProtocolIncus) GetHTTPClient() (*http.Client, error) {
if r.http == nil {
return nil, errors.New("HTTP client isn't set, bad connection")
}
return r.http, nil
}
// DoHTTP performs a Request, using OIDC authentication if set.
func (r *ProtocolIncus) DoHTTP(req *http.Request) (*http.Response, error) {
r.addClientHeaders(req)
if r.oidcClient != nil {
return r.oidcClient.do(req)
}
resp, err := r.http.Do(req)
if resp != nil && resp.StatusCode == http.StatusUseProxy && req.GetBody != nil {
// Reset the request body.
body, err := req.GetBody()
if err != nil {
return nil, err
}
req.Body = body
// Retry the request.
return r.http.Do(req)
}
return resp, err
}
// DoWebsocket performs a websocket connection, using OIDC authentication if set.
func (r *ProtocolIncus) DoWebsocket(dialer websocket.Dialer, uri string, req *http.Request) (*websocket.Conn, *http.Response, error) {
r.addClientHeaders(req)
if r.oidcClient != nil {
return r.oidcClient.dial(dialer, uri, req)
}
return dialer.Dial(uri, req.Header)
}
// addClientHeaders sets headers from client settings.
// User-Agent (if r.httpUserAgent is set).
// X-Incus-authenticated (if r.requireAuthenticated is set).
// OIDC Authorization header (if r.oidcClient is set).
func (r *ProtocolIncus) addClientHeaders(req *http.Request) {
if r.httpUserAgent != "" {
req.Header.Set("User-Agent", r.httpUserAgent)
}
if r.requireAuthenticated {
req.Header.Set("X-Incus-authenticated", "true")
}
if r.oidcClient != nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.oidcClient.getAccessToken()))
}
}
// RequireAuthenticated sets whether we expect to be authenticated with the server.
func (r *ProtocolIncus) RequireAuthenticated(authenticated bool) {
r.requireAuthenticated = authenticated
}
// RawQuery allows directly querying the Incus API
//
// This should only be used by internal Incus tools.
func (r *ProtocolIncus) RawQuery(method string, path string, data any, ETag string) (*api.Response, string, error) {
// Generate the URL
url := fmt.Sprintf("%s%s", r.httpBaseURL.String(), path)
return r.rawQuery(method, url, data, ETag)
}
// RawWebsocket allows directly connection to Incus API websockets
//
// This should only be used by internal Incus tools.
func (r *ProtocolIncus) RawWebsocket(path string) (*websocket.Conn, error) {
return r.websocket(path)
}
// RawOperation allows direct querying of an Incus API endpoint returning
// background operations.
func (r *ProtocolIncus) RawOperation(method string, path string, data any, ETag string) (Operation, string, error) {
return r.queryOperation(method, path, data, ETag)
}
// Internal functions.
func incusParseResponse(resp *http.Response) (*api.Response, string, error) {
// Get the ETag
etag := resp.Header.Get("ETag")
// Decode the response
decoder := json.NewDecoder(resp.Body)
response := api.Response{}
err := decoder.Decode(&response)
if err != nil {
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("Failed to fetch %s: %s", resp.Request.URL.String(), resp.Status)
}
return nil, "", err
}
// Handle errors
if response.Type == api.ErrorResponse {
return &response, "", api.StatusErrorf(resp.StatusCode, "%v", response.Error)
}
return &response, etag, nil
}
// rawQuery is a method that sends an HTTP request to the Incus server with the provided method, URL, data, and ETag.
// It processes the request based on the data's type and handles the HTTP response, returning parsed results or an error if it occurs.
func (r *ProtocolIncus) rawQuery(method string, url string, data any, ETag string) (*api.Response, string, error) {
var req *http.Request
var err error
// Log the request
logger.Debug("Sending request to Incus", logger.Ctx{
"method": method,
"url": url,
"etag": ETag,
})
// Get a new HTTP request setup
if data != nil {
switch data := data.(type) {
case io.Reader:
// Some data to be sent along with the request
req, err = http.NewRequestWithContext(r.ctx, method, url, io.NopCloser(data))
if err != nil {
return nil, "", err
}
req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(data), nil }
// Set the encoding accordingly
req.Header.Set("Content-Type", "application/octet-stream")
default:
// Encode the provided data
buf := bytes.Buffer{}
err := json.NewEncoder(&buf).Encode(data)
if err != nil {
return nil, "", err
}
// Some data to be sent along with the request
// Use a reader since the request body needs to be seekable
req, err = http.NewRequestWithContext(r.ctx, method, url, bytes.NewReader(buf.Bytes()))
if err != nil {
return nil, "", err
}
req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf.Bytes())), nil }
// Set the encoding accordingly
req.Header.Set("Content-Type", "application/json")
// Log the data
logger.Debugf("%s", logger.Pretty(data))
}
} else {
// No data to be sent along with the request
req, err = http.NewRequestWithContext(r.ctx, method, url, nil)
if err != nil {
return nil, "", err
}
}
// Set the ETag
if ETag != "" {
req.Header.Set("If-Match", ETag)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, "", err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
return incusParseResponse(resp)
}
// setURLQueryAttributes modifies the supplied URL's query string with the client's current target and project.
func (r *ProtocolIncus) setURLQueryAttributes(apiURL *neturl.URL) {
// Extract query fields and update for cluster targeting or project
values := apiURL.Query()
if r.clusterTarget != "" {
if values.Get("target") == "" {
values.Set("target", r.clusterTarget)
}
}
if r.project != "" {
if values.Get("project") == "" && values.Get("all-projects") == "" {
values.Set("project", r.project)
}
}
apiURL.RawQuery = values.Encode()
}
func (r *ProtocolIncus) setQueryAttributes(uri string) (string, error) {
// Parse the full URI
fields, err := neturl.Parse(uri)
if err != nil {
return "", err
}
r.setURLQueryAttributes(fields)
return fields.String(), nil
}
func (r *ProtocolIncus) query(method string, path string, data any, ETag string) (*api.Response, string, error) {
// Generate the URL
url := fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path)
// Add project/target
url, err := r.setQueryAttributes(url)
if err != nil {
return nil, "", err
}
// Run the actual query
return r.rawQuery(method, url, data, ETag)
}
// queryStruct sends a query to the Incus server, then converts the response metadata into the specified target struct.
// The function logs the retrieved data, returns the etag of the response, and handles any errors during this process.
func (r *ProtocolIncus) queryStruct(method string, path string, data any, ETag string, target any) (string, error) {
resp, etag, err := r.query(method, path, data, ETag)
if err != nil {
return "", err
}
err = resp.MetadataAsStruct(&target)
if err != nil {
return "", err
}
// Log the data
logger.Debugf("Got response struct from Incus")
logger.Debugf("%s", logger.Pretty(target))
return etag, nil
}
// queryOperation sends a query to the Incus server and then converts the response metadata into an Operation object.
// It sets up an early event listener, performs the query, processes the response, and manages the lifecycle of the event listener.
func (r *ProtocolIncus) queryOperation(method string, path string, data any, ETag string) (Operation, string, error) {
// Attempt to setup an early event listener
var listener *EventListener
skipListener := r.skipEvents
if !skipListener {
var err error
listener, err = r.GetEvents()
if err != nil {
if api.StatusErrorCheck(err, http.StatusForbidden) {
skipListener = true
}
listener = nil
}
}
// Send the query
resp, etag, err := r.query(method, path, data, ETag)
if err != nil {
if listener != nil {
listener.Disconnect()
}
return nil, "", err
}
// Get to the operation
respOperation, err := resp.MetadataAsOperation()
if err != nil {
if listener != nil {
listener.Disconnect()
}
return nil, "", err
}
// Setup an Operation wrapper
op := operation{
Operation: *respOperation,
r: r,
listener: listener,
skipListener: skipListener,
chActive: make(chan bool),
}
// Log the data
logger.Debugf("Got operation from Incus")
logger.Debugf("%s", logger.Pretty(op.Operation))
return &op, etag, nil
}
// rawWebsocket creates a websocket connection to the provided URL using the underlying HTTP transport of the ProtocolIncus receiver.
// It sets up the request headers, manages the connection handshake, sets TCP timeouts, and handles any errors that may occur during these operations.
func (r *ProtocolIncus) rawWebsocket(url string) (*websocket.Conn, error) {
// Grab the http transport handler
httpTransport, err := r.getUnderlyingHTTPTransport()
if err != nil {
return nil, err
}
// Setup a new websocket dialer based on it
dialer := websocket.Dialer{
NetDialTLSContext: httpTransport.DialTLSContext,
NetDialContext: httpTransport.DialContext,
TLSClientConfig: httpTransport.TLSClientConfig,
Proxy: httpTransport.Proxy,
HandshakeTimeout: time.Second * 5,
}
// Create temporary http.Request using the http url, not the ws one, so that we can add the client headers
// for the websocket request.
req := &http.Request{URL: &r.httpBaseURL, Header: http.Header{}}
// Establish the connection
conn, resp, err := r.DoWebsocket(dialer, url, req)
if err != nil {
if resp != nil {
apiResp, _, parseErr := incusParseResponse(resp)
if parseErr != nil {
err = errors.Join(err, parseErr)
}
if apiResp != nil && apiResp.Error != "" {
err = errors.Join(err, errors.New(apiResp.Error))
}
}
return nil, err
}
// Set TCP timeout options.
remoteTCP, _ := tcp.ExtractConn(conn.UnderlyingConn())
if remoteTCP != nil {
err = tcp.SetTimeouts(remoteTCP, 0)
if err != nil {
logger.Warn("Failed setting TCP timeouts on remote connection", logger.Ctx{"err": err})
}
}
// Log the data
logger.Debugf("Connected to the websocket: %v", url)
return conn, nil
}
// websocket generates a websocket URL based on the provided path and the base URL of the ProtocolIncus receiver.
// It then leverages the rawWebsocket method to establish and return a websocket connection to the generated URL.
func (r *ProtocolIncus) websocket(path string) (*websocket.Conn, error) {
// Generate the URL
var url string
if r.httpBaseURL.Scheme == "https" {
url = fmt.Sprintf("wss://%s/1.0%s", r.httpBaseURL.Host, path)
} else {
url = fmt.Sprintf("ws://%s/1.0%s", r.httpBaseURL.Host, path)
}
return r.rawWebsocket(url)
}
// WithContext returns a client that will add context.Context.
func (r *ProtocolIncus) WithContext(ctx context.Context) InstanceServer {
rr := r
rr.ctx = ctx
return rr
}
// getUnderlyingHTTPTransport returns the *http.Transport used by the http client. If the http
// client was initialized with a HTTPTransporter, it returns the wrapped *http.Transport.
func (r *ProtocolIncus) getUnderlyingHTTPTransport() (*http.Transport, error) {
switch t := r.http.Transport.(type) {
case *http.Transport:
return t, nil
case HTTPTransporter:
return t.Transport(), nil
default:
return nil, fmt.Errorf("Unexpected http.Transport type, %T", r)
}
}
// getSourceImageConnectionInfo returns the connection information for the source image.
// The returned `info` is nil if the source image is local. In this process, the `instSrc`
// is also updated with the minimal source fields.
func (r *ProtocolIncus) getSourceImageConnectionInfo(source ImageServer, image api.Image, instSrc *api.InstanceSource) (info *ConnectionInfo, err error) {
// Set the minimal source fields
instSrc.Type = "image"
// Optimization for the local image case
if r.isSameServer(source) {
// Always use fingerprints for local case
instSrc.Fingerprint = image.Fingerprint
instSrc.Alias = ""
return nil, nil
}
// Minimal source fields for remote image
instSrc.Mode = "pull"
// If we have an alias and the image is public, use that
if instSrc.Alias != "" && image.Public {
instSrc.Fingerprint = ""
} else {
instSrc.Fingerprint = image.Fingerprint
instSrc.Alias = ""
}
// Get source server connection information
info, err = source.GetConnectionInfo()
if err != nil {
return nil, err
}
instSrc.Protocol = info.Protocol
instSrc.Certificate = info.Certificate
// Generate secret token if needed
if !image.Public {
secret, err := source.GetImageSecret(image.Fingerprint)
if err != nil {
return nil, err
}
instSrc.Secret = secret
}
return info, nil
}
incus-7.3.0/client/incus_certificates.go 0000664 0000000 0000000 00000006536 15232704312 0020326 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// Certificate handling functions
// GetCertificateFingerprints returns a list of certificate fingerprints.
func (r *ProtocolIncus) GetCertificateFingerprints() ([]string, error) {
// Fetch the raw URL values.
urls := []string{}
baseURL := "/certificates"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetCertificates returns a list of certificates.
func (r *ProtocolIncus) GetCertificates() ([]api.Certificate, error) {
certificates := []api.Certificate{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/certificates?recursion=1", nil, "", &certificates)
if err != nil {
return nil, err
}
return certificates, nil
}
// GetCertificatesWithFilter returns a filtered list of certificates.
func (r *ProtocolIncus) GetCertificatesWithFilter(filters []string) ([]api.Certificate, error) {
certificates := []api.Certificate{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/certificates?%s", v.Encode()), nil, "", &certificates)
if err != nil {
return nil, err
}
return certificates, nil
}
// GetCertificate returns the certificate entry for the provided fingerprint.
func (r *ProtocolIncus) GetCertificate(fingerprint string) (*api.Certificate, string, error) {
certificate := api.Certificate{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/certificates/%s", url.PathEscape(fingerprint)), nil, "", &certificate)
if err != nil {
return nil, "", err
}
return &certificate, etag, nil
}
// CreateCertificate adds a new certificate to the Incus trust store.
func (r *ProtocolIncus) CreateCertificate(certificate api.CertificatesPost) error {
// Send the request
_, _, err := r.query("POST", "/certificates", certificate, "")
if err != nil {
return err
}
return nil
}
// UpdateCertificate updates the certificate definition.
func (r *ProtocolIncus) UpdateCertificate(fingerprint string, certificate api.CertificatePut, ETag string) error {
if !r.HasExtension("certificate_update") {
return errors.New("The server is missing the required \"certificate_update\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/certificates/%s", url.PathEscape(fingerprint)), certificate, ETag)
if err != nil {
return err
}
return nil
}
// DeleteCertificate removes a certificate from the Incus trust store.
func (r *ProtocolIncus) DeleteCertificate(fingerprint string) error {
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/certificates/%s", url.PathEscape(fingerprint)), nil, "")
if err != nil {
return err
}
return nil
}
// CreateCertificateToken requests a certificate add token.
func (r *ProtocolIncus) CreateCertificateToken(certificate api.CertificatesPost) (Operation, error) {
if !r.HasExtension("certificate_token") {
return nil, errors.New("The server is missing the required \"certificate_token\" API extension")
}
if !certificate.Token {
return nil, errors.New("Token needs to be true if requesting a token")
}
// Send the request
op, _, err := r.queryOperation("POST", "/certificates", certificate, "")
if err != nil {
return nil, err
}
return op, nil
}
incus-7.3.0/client/incus_cluster.go 0000664 0000000 0000000 00000024277 15232704312 0017344 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetCluster returns information about a cluster.
func (r *ProtocolIncus) GetCluster() (*api.Cluster, string, error) {
if !r.HasExtension("clustering") {
return nil, "", errors.New("The server is missing the required \"clustering\" API extension")
}
cluster := &api.Cluster{}
etag, err := r.queryStruct("GET", "/cluster", nil, "", &cluster)
if err != nil {
return nil, "", err
}
return cluster, etag, nil
}
// UpdateCluster requests to bootstrap a new cluster or join an existing one.
func (r *ProtocolIncus) UpdateCluster(cluster api.ClusterPut, ETag string) (Operation, error) {
if !r.HasExtension("clustering") {
return nil, errors.New("The server is missing the required \"clustering\" API extension")
}
if cluster.ServerAddress != "" || cluster.ClusterToken != "" || len(cluster.MemberConfig) > 0 {
if !r.HasExtension("clustering_join") {
return nil, errors.New("The server is missing the required \"clustering_join\" API extension")
}
}
op, _, err := r.queryOperation("PUT", "/cluster", cluster, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// DeleteClusterMember makes the given member leave the cluster (gracefully or not,
// depending on the force flag).
func (r *ProtocolIncus) DeleteClusterMember(name string, force bool) error {
if !r.HasExtension("clustering") {
return errors.New("The server is missing the required \"clustering\" API extension")
}
params := ""
if force {
params += "?force=1"
}
_, _, err := r.query("DELETE", fmt.Sprintf("/cluster/members/%s%s", name, params), nil, "")
if err != nil {
return err
}
return nil
}
// DeletePendingClusterMember makes the given pending member leave the cluster (gracefully or not,
// depending on the force flag).
func (r *ProtocolIncus) DeletePendingClusterMember(name string, force bool) error {
if !r.HasExtension("clustering") {
return errors.New("The server is missing the required \"clustering\" API extension")
}
params := "?pending=1"
if force {
params += "&force=1"
}
_, _, err := r.query("DELETE", fmt.Sprintf("/cluster/members/%s%s", name, params), nil, "")
if err != nil {
return err
}
return nil
}
// GetClusterMemberNames returns the URLs of the current members in the cluster.
func (r *ProtocolIncus) GetClusterMemberNames() ([]string, error) {
if !r.HasExtension("clustering") {
return nil, errors.New("The server is missing the required \"clustering\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/cluster/members"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetClusterMembersWithFilter returns a filtered list of cluster members as ClusterMember structs.
func (r *ProtocolIncus) GetClusterMembersWithFilter(filters []string) ([]api.ClusterMember, error) {
if !r.HasExtension("clustering") {
return nil, errors.New("The server is missing the required \"clustering\" API extension")
}
members := []api.ClusterMember{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/cluster/members?%s", v.Encode()), nil, "", &members)
if err != nil {
return nil, err
}
return members, nil
}
// GetClusterMembers returns the current members of the cluster.
func (r *ProtocolIncus) GetClusterMembers() ([]api.ClusterMember, error) {
if !r.HasExtension("clustering") {
return nil, errors.New("The server is missing the required \"clustering\" API extension")
}
members := []api.ClusterMember{}
_, err := r.queryStruct("GET", "/cluster/members?recursion=1", nil, "", &members)
if err != nil {
return nil, err
}
return members, nil
}
// GetClusterMember returns information about the given member.
func (r *ProtocolIncus) GetClusterMember(name string) (*api.ClusterMember, string, error) {
if !r.HasExtension("clustering") {
return nil, "", errors.New("The server is missing the required \"clustering\" API extension")
}
member := api.ClusterMember{}
etag, err := r.queryStruct("GET", fmt.Sprintf("/cluster/members/%s", name), nil, "", &member)
if err != nil {
return nil, "", err
}
return &member, etag, nil
}
// UpdateClusterMember updates information about the given member.
func (r *ProtocolIncus) UpdateClusterMember(name string, member api.ClusterMemberPut, ETag string) error {
if !r.HasExtension("clustering_edit_roles") {
return errors.New("The server is missing the required \"clustering_edit_roles\" API extension")
}
if member.FailureDomain != "" {
if !r.HasExtension("clustering_failure_domains") {
return errors.New("The server is missing the required \"clustering_failure_domains\" API extension")
}
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/cluster/members/%s", name), member, ETag)
if err != nil {
return err
}
return nil
}
// RenameClusterMember changes the name of an existing member.
func (r *ProtocolIncus) RenameClusterMember(name string, member api.ClusterMemberPost) error {
if !r.HasExtension("clustering") {
return errors.New("The server is missing the required \"clustering\" API extension")
}
_, _, err := r.query("POST", fmt.Sprintf("/cluster/members/%s", name), member, "")
if err != nil {
return err
}
return nil
}
// CreateClusterMember generates a join token to add a cluster member.
func (r *ProtocolIncus) CreateClusterMember(member api.ClusterMembersPost) (Operation, error) {
if !r.HasExtension("clustering_join_token") {
return nil, errors.New("The server is missing the required \"clustering_join_token\" API extension")
}
op, _, err := r.queryOperation("POST", "/cluster/members", member, "")
if err != nil {
return nil, err
}
return op, nil
}
// UpdateClusterCertificate updates the cluster certificate for every node in the cluster.
func (r *ProtocolIncus) UpdateClusterCertificate(certs api.ClusterCertificatePut, ETag string) error {
if !r.HasExtension("clustering_update_cert") {
return errors.New("The server is missing the required \"clustering_update_cert\" API extension")
}
_, _, err := r.query("PUT", "/cluster/certificate", certs, ETag)
if err != nil {
return err
}
return nil
}
// GetClusterMemberState gets state information about a cluster member.
func (r *ProtocolIncus) GetClusterMemberState(name string) (*api.ClusterMemberState, string, error) {
err := r.CheckExtension("cluster_member_state")
if err != nil {
return nil, "", err
}
state := api.ClusterMemberState{}
u := api.NewURL().Path("cluster", "members", name, "state")
etag, err := r.queryStruct("GET", u.String(), nil, "", &state)
if err != nil {
return nil, "", err
}
return &state, etag, err
}
// UpdateClusterMemberState evacuates or restores a cluster member.
func (r *ProtocolIncus) UpdateClusterMemberState(name string, state api.ClusterMemberStatePost) (Operation, error) {
if !r.HasExtension("clustering_evacuation") {
return nil, errors.New("The server is missing the required \"clustering_evacuation\" API extension")
}
op, _, err := r.queryOperation("POST", fmt.Sprintf("/cluster/members/%s/state", name), state, "")
if err != nil {
return nil, err
}
return op, nil
}
// GetClusterGroups returns the cluster groups.
func (r *ProtocolIncus) GetClusterGroups() ([]api.ClusterGroup, error) {
if !r.HasExtension("clustering_groups") {
return nil, errors.New("The server is missing the required \"clustering_groups\" API extension")
}
groups := []api.ClusterGroup{}
_, err := r.queryStruct("GET", "/cluster/groups?recursion=1", nil, "", &groups)
if err != nil {
return nil, err
}
return groups, nil
}
// GetClusterGroupNames returns the cluster group names.
func (r *ProtocolIncus) GetClusterGroupNames() ([]string, error) {
if !r.HasExtension("clustering_groups") {
return nil, errors.New("The server is missing the required \"clustering_groups\" API extension")
}
urls := []string{}
_, err := r.queryStruct("GET", "/cluster/groups", nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames("/1.0/cluster/groups", urls...)
}
// RenameClusterGroup changes the name of an existing cluster group.
func (r *ProtocolIncus) RenameClusterGroup(name string, group api.ClusterGroupPost) error {
if !r.HasExtension("clustering_groups") {
return errors.New("The server is missing the required \"clustering_groups\" API extension")
}
_, _, err := r.query("POST", fmt.Sprintf("/cluster/groups/%s", name), group, "")
if err != nil {
return err
}
return nil
}
// CreateClusterGroup creates a new cluster group.
func (r *ProtocolIncus) CreateClusterGroup(group api.ClusterGroupsPost) error {
if !r.HasExtension("clustering_groups") {
return errors.New("The server is missing the required \"clustering_groups\" API extension")
}
_, _, err := r.query("POST", "/cluster/groups", group, "")
if err != nil {
return err
}
return nil
}
// DeleteClusterGroup deletes an existing cluster group.
func (r *ProtocolIncus) DeleteClusterGroup(name string) error {
if !r.HasExtension("clustering_groups") {
return errors.New("The server is missing the required \"clustering_groups\" API extension")
}
_, _, err := r.query("DELETE", fmt.Sprintf("/cluster/groups/%s", name), nil, "")
if err != nil {
return err
}
return nil
}
// UpdateClusterGroup updates information about the given cluster group.
func (r *ProtocolIncus) UpdateClusterGroup(name string, group api.ClusterGroupPut, ETag string) error {
if !r.HasExtension("clustering_groups") {
return errors.New("The server is missing the required \"clustering_groups\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/cluster/groups/%s", name), group, ETag)
if err != nil {
return err
}
return nil
}
// GetClusterGroup returns information about the given cluster group.
func (r *ProtocolIncus) GetClusterGroup(name string) (*api.ClusterGroup, string, error) {
if !r.HasExtension("clustering_groups") {
return nil, "", errors.New("The server is missing the required \"clustering_groups\" API extension")
}
group := api.ClusterGroup{}
etag, err := r.queryStruct("GET", fmt.Sprintf("/cluster/groups/%s", name), nil, "", &group)
if err != nil {
return nil, "", err
}
return &group, etag, nil
}
incus-7.3.0/client/incus_events.go 0000664 0000000 0000000 00000013446 15232704312 0017163 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"encoding/json"
"errors"
"net/url"
"slices"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/shared/api"
)
// Event handling functions
// getEvents connects to the Incus monitoring interface.
func (r *ProtocolIncus) getEvents(allProjects bool, eventTypes []string) (*EventListener, error) {
// Prevent anything else from interacting with the listeners
r.eventListenersLock.Lock()
defer r.eventListenersLock.Unlock()
ctx, cancel := context.WithCancel(context.Background())
// Clear skipGetEvents once we've been directly called.
r.skipEvents = false
// Setup a new listener
listener := EventListener{
r: r,
ctx: ctx,
ctxCancel: cancel,
}
connInfo, _ := r.GetConnectionInfo()
if connInfo.Project == "" {
return nil, errors.New("Unexpected empty project in connection info")
}
if !allProjects {
listener.projectName = connInfo.Project
}
// There is an existing Go routine for the required project filter, so just add another target.
if r.eventListeners[listener.projectName] != nil {
r.eventListeners[listener.projectName] = append(r.eventListeners[listener.projectName], &listener)
return &listener, nil
}
// Setup a new connection with Incus
var queryParams []string
if allProjects {
queryParams = append(queryParams, "all-projects=true")
}
if len(eventTypes) > 0 {
for i := range len(eventTypes) {
eventTypes[i] = url.QueryEscape(eventTypes[i])
}
queryParams = append(queryParams, "type="+strings.Join(eventTypes, ","))
}
eventsURL := "/events"
if len(queryParams) > 0 {
eventsURL += "?" + strings.Join(queryParams, "&")
}
eventsURL, err := r.setQueryAttributes(eventsURL)
if err != nil {
return nil, err
}
// Connect websocket and save.
wsConn, err := r.websocket(eventsURL)
if err != nil {
return nil, err
}
r.eventConnsLock.Lock()
r.eventConns[listener.projectName] = wsConn // Save for others to use.
r.eventConnsLock.Unlock()
// Initialize the event listener list if we were able to connect to the events websocket.
r.eventListeners[listener.projectName] = []*EventListener{&listener}
// Spawn a watcher that will close the websocket connection after all
// listeners are gone.
stopCh := make(chan struct{})
go func() {
for {
select {
case <-time.After(time.Minute):
case <-r.ctxConnected.Done():
case <-stopCh:
}
r.eventListenersLock.Lock()
r.eventConnsLock.Lock()
if len(r.eventListeners[listener.projectName]) == 0 {
// We don't need the connection anymore, disconnect and clear.
if r.eventListeners[listener.projectName] != nil {
_ = r.eventConns[listener.projectName].Close()
delete(r.eventConns, listener.projectName)
}
r.eventListeners[listener.projectName] = nil
r.eventListenersLock.Unlock()
r.eventConnsLock.Unlock()
return
}
r.eventListenersLock.Unlock()
r.eventConnsLock.Unlock()
}
}()
// Spawn the listener
go func() {
for {
_, data, err := wsConn.ReadMessage()
if err != nil {
// Prevent anything else from interacting with the listeners
r.eventListenersLock.Lock()
defer r.eventListenersLock.Unlock()
// Tell all the current listeners about the failure
for _, listener := range r.eventListeners[listener.projectName] {
listener.err = err
listener.ctxCancel()
}
// And remove them all from the list so that when watcher routine runs it will
// close the websocket connection.
r.eventListeners[listener.projectName] = nil
close(stopCh) // Instruct watcher go routine to cleanup.
return
}
// Attempt to unpack the message
event := api.Event{}
err = json.Unmarshal(data, &event)
if err != nil {
continue
}
// Extract the message type
if event.Type == "" {
continue
}
// Send the message to all handlers
r.eventListenersLock.Lock()
for _, listener := range r.eventListeners[listener.projectName] {
listener.targetsLock.Lock()
for _, target := range listener.targets {
if target.types != nil && !slices.Contains(target.types, event.Type) {
continue
}
go target.function(event)
}
listener.targetsLock.Unlock()
}
r.eventListenersLock.Unlock()
}
}()
return &listener, nil
}
// GetEvents gets the events for the project defined on the client.
func (r *ProtocolIncus) GetEvents() (*EventListener, error) {
return r.getEvents(false, nil)
}
// GetEventsByType gets the events filtered by the provided list of types
// for the project defined on the client.
func (r *ProtocolIncus) GetEventsByType(eventTypes []string) (listener *EventListener, err error) {
return r.getEvents(false, eventTypes)
}
// GetEventsAllProjects gets events for all projects.
func (r *ProtocolIncus) GetEventsAllProjects() (*EventListener, error) {
return r.getEvents(true, nil)
}
// GetEventsAllProjectsByType gets the events filtered by the provided list of
// types for all projects.
func (r *ProtocolIncus) GetEventsAllProjectsByType(eventTypes []string) (listener *EventListener, err error) {
return r.getEvents(true, eventTypes)
}
// SendEvent send an event to the server via the client's event listener connection.
func (r *ProtocolIncus) SendEvent(event api.Event) error {
r.eventConnsLock.Lock()
defer r.eventConnsLock.Unlock()
// Find an available event listener connection.
// It doesn't matter which project the event listener connection is using, as this only affects which
// events are received from the server, not which events we can send to it.
var eventConn *websocket.Conn
for _, eventConn = range r.eventConns {
break
}
if eventConn == nil {
return errors.New("No available event listener connection")
}
deadline, ok := r.ctx.Deadline()
if !ok {
deadline = time.Now().Add(5 * time.Second)
}
_ = eventConn.SetWriteDeadline(deadline)
return eventConn.WriteJSON(event)
}
incus-7.3.0/client/incus_images.go 0000664 0000000 0000000 00000065576 15232704312 0017137 0 ustar 00root root 0000000 0000000 package incus
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/url"
"os"
"slices"
"strings"
"time"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/cancel"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
// Image handling functions
// GetImages returns a list of available images as Image structs.
func (r *ProtocolIncus) GetImages() ([]api.Image, error) {
images := []api.Image{}
_, err := r.queryStruct("GET", "/images?recursion=1", nil, "", &images)
if err != nil {
return nil, err
}
return images, nil
}
// GetImagesAllProjects returns a list of images across all projects as Image structs.
func (r *ProtocolIncus) GetImagesAllProjects() ([]api.Image, error) {
images := []api.Image{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")
if !r.HasExtension("images_all_projects") {
return nil, errors.New("The server is missing the required \"images_all_projects\" API extension")
}
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
if err != nil {
return nil, err
}
return images, nil
}
// GetImagesAllProjectsWithFilter returns a filtered list of images across all projects as Image structs.
func (r *ProtocolIncus) GetImagesAllProjectsWithFilter(filters []string) ([]api.Image, error) {
images := []api.Image{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("images_all_projects") {
return nil, errors.New("The server is missing the required \"images_all_projects\" API extension")
}
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
if err != nil {
return nil, err
}
return images, nil
}
// GetImagesWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolIncus) GetImagesWithFilter(filters []string) ([]api.Image, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
images := []api.Image{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
if err != nil {
return nil, err
}
return images, nil
}
// GetImageFingerprints returns a list of available image fingerprints.
func (r *ProtocolIncus) GetImageFingerprints() ([]string, error) {
// Fetch the raw URL values.
urls := []string{}
baseURL := "/images"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetImage returns an Image struct for the provided fingerprint.
func (r *ProtocolIncus) GetImage(fingerprint string) (*api.Image, string, error) {
return r.GetPrivateImage(fingerprint, "")
}
// GetImageFile downloads an image from the server, returning an ImageFileRequest struct.
func (r *ProtocolIncus) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
return r.GetPrivateImageFile(fingerprint, "", req)
}
// GetImageSecret is a helper around CreateImageSecret that returns a secret for the image.
func (r *ProtocolIncus) GetImageSecret(fingerprint string) (string, error) {
op, err := r.CreateImageSecret(fingerprint)
if err != nil {
return "", err
}
opAPI := op.Get()
secret, ok := opAPI.Metadata["secret"].(string)
if !ok {
return "", errors.New("Bad secret type")
}
return secret, nil
}
// GetPrivateImage is similar to GetImage but allows passing a secret download token.
func (r *ProtocolIncus) GetPrivateImage(fingerprint string, secret string) (*api.Image, string, error) {
image := api.Image{}
// Build the API path
path := fmt.Sprintf("/images/%s", url.PathEscape(fingerprint))
var err error
path, err = r.setQueryAttributes(path)
if err != nil {
return nil, "", err
}
if secret != "" {
path, err = setQueryParam(path, "secret", secret)
if err != nil {
return nil, "", err
}
}
// Fetch the raw value
etag, err := r.queryStruct("GET", path, nil, "", &image)
if err != nil {
return nil, "", err
}
return &image, etag, nil
}
// GetPrivateImageFile is similar to GetImageFile but allows passing a secret download token.
func (r *ProtocolIncus) GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (*ImageFileResponse, error) {
// Quick checks.
if req.MetaFile == nil && req.RootfsFile == nil {
return nil, errors.New("No file requested")
}
uri := fmt.Sprintf("/1.0/images/%s/export", url.PathEscape(fingerprint))
var err error
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
// Attempt to download from host
if secret == "" && util.PathExists("/dev/incus/sock") && os.Geteuid() == 0 {
unixURI := fmt.Sprintf("http://unix.socket%s", uri)
// Setup the HTTP client
devIncusHTTP, err := unixHTTPClient(nil, "/dev/incus/sock")
if err == nil {
resp, err := incusDownloadImage(fingerprint, unixURI, r.httpUserAgent, devIncusHTTP.Do, req)
if err == nil {
return resp, nil
}
}
}
// Build the URL
uri = fmt.Sprintf("%s%s", r.httpBaseURL.String(), uri)
if secret != "" {
uri, err = setQueryParam(uri, "secret", secret)
if err != nil {
return nil, err
}
}
// Use relatively short response header timeout so as not to hold the image lock open too long.
// Deference client and transport in order to clone them so as to not modify timeout of base client.
httpClient := *r.http
httpTransport := httpClient.Transport.(*http.Transport).Clone()
httpTransport.ResponseHeaderTimeout = 30 * time.Second
httpClient.Transport = httpTransport
return incusDownloadImage(fingerprint, uri, r.httpUserAgent, r.DoHTTP, req)
}
func incusDownloadImage(fingerprint string, uri string, userAgent string, do func(*http.Request) (*http.Response, error), req ImageFileRequest) (*ImageFileResponse, error) {
// Prepare the response
resp := ImageFileResponse{}
// Prepare the download request
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
if userAgent != "" {
request.Header.Set("User-Agent", userAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, do, request)
if err != nil {
return nil, err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(response)
if err != nil {
return nil, err
}
}
ctype, ctypeParams, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
if err != nil {
ctype = "application/octet-stream"
}
// Check the image type.
imageType := response.Header.Get("X-Incus-Type")
if imageType == "" {
imageType = "incus"
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
reader := &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Length: response.ContentLength,
},
}
if response.ContentLength > 0 {
reader.Tracker.Handler = func(percent int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
}
} else {
reader.Tracker.Handler = func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
}
}
body = reader
}
// Hashing
hash256 := sha256.New()
// Deal with split images
if ctype == "multipart/form-data" {
if req.MetaFile == nil || req.RootfsFile == nil {
return nil, errors.New("Multi-part image but only one target file provided")
}
// Parse the POST data
mr := multipart.NewReader(body, ctypeParams["boundary"])
// Get the metadata tarball
part, err := mr.NextPart()
if err != nil {
return nil, err
}
if part.FormName() != "metadata" {
return nil, errors.New("Invalid multipart image")
}
size, err := util.SafeCopy(io.MultiWriter(req.MetaFile, hash256), part)
if err != nil {
return nil, err
}
resp.MetaSize = size
resp.MetaName = part.FileName()
// Get the rootfs tarball
part, err = mr.NextPart()
if err != nil {
return nil, err
}
if !slices.Contains([]string{"rootfs", "rootfs.img"}, part.FormName()) {
return nil, errors.New("Invalid multipart image")
}
size, err = util.SafeCopy(io.MultiWriter(req.RootfsFile, hash256), part)
if err != nil {
return nil, err
}
resp.RootfsSize = size
resp.RootfsName = part.FileName()
// Check the hash
hash := fmt.Sprintf("%x", hash256.Sum(nil))
if imageType != "oci" && !strings.HasPrefix(hash, fingerprint) {
return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
}
return &resp, nil
}
// Deal with unified images
_, cdParams, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
if err != nil {
return nil, err
}
filename, ok := cdParams["filename"]
if !ok {
return nil, errors.New("No filename in Content-Disposition header")
}
size, err := util.SafeCopy(io.MultiWriter(req.MetaFile, hash256), body)
if err != nil {
return nil, err
}
resp.MetaSize = size
resp.MetaName = filename
// Check the hash
hash := fmt.Sprintf("%x", hash256.Sum(nil))
if imageType != "oci" && !strings.HasPrefix(hash, fingerprint) {
return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
}
return &resp, nil
}
// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs.
func (r *ProtocolIncus) GetImageAliases() ([]api.ImageAliasesEntry, error) {
aliases := []api.ImageAliasesEntry{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/images/aliases?recursion=1", nil, "", &aliases)
if err != nil {
return nil, err
}
return aliases, nil
}
// GetImageAliasNames returns the list of available alias names.
func (r *ProtocolIncus) GetImageAliasNames() ([]string, error) {
// Fetch the raw URL values.
urls := []string{}
baseURL := "/images/aliases"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetImageAlias returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolIncus) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
alias := api.ImageAliasesEntry{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), nil, "", &alias)
if err != nil {
return nil, "", err
}
return &alias, etag, nil
}
// GetImageAliasType returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolIncus) GetImageAliasType(imageType string, name string) (*api.ImageAliasesEntry, string, error) {
alias, etag, err := r.GetImageAlias(name)
if err != nil {
return nil, "", err
}
if imageType != "" {
if alias.Type == "" {
alias.Type = "container"
}
if alias.Type != imageType {
return nil, "", errors.New("Alias doesn't exist for the specified type")
}
}
return alias, etag, nil
}
// GetImageAliasArchitectures returns a map of architectures / targets.
func (r *ProtocolIncus) GetImageAliasArchitectures(imageType string, name string) (map[string]*api.ImageAliasesEntry, error) {
alias, _, err := r.GetImageAliasType(imageType, name)
if err != nil {
return nil, err
}
img, _, err := r.GetImage(alias.Target)
if err != nil {
return nil, err
}
return map[string]*api.ImageAliasesEntry{img.Architecture: alias}, nil
}
// CreateImage requests that Incus creates, copies or import a new image.
func (r *ProtocolIncus) CreateImage(image api.ImagesPost, args *ImageCreateArgs) (Operation, error) {
if image.CompressionAlgorithm != "" {
if !r.HasExtension("image_compression_algorithm") {
return nil, errors.New("The server is missing the required \"image_compression_algorithm\" API extension")
}
}
// Send the JSON based request
if args == nil {
op, _, err := r.queryOperation("POST", "/images", image, "")
if err != nil {
return nil, err
}
return op, nil
}
// Prepare an image upload
if args.MetaFile == nil {
return nil, errors.New("Metadata file is required")
}
// Prepare the body
var body io.Reader
var contentType string
if args.RootfsFile == nil {
// If unified image, just pass it through
body = args.MetaFile
contentType = "application/octet-stream"
} else {
pr, pw := io.Pipe()
// Setup the multipart writer
w := multipart.NewWriter(pw)
go func() {
var ioErr error
defer func() {
cerr := w.Close()
if ioErr == nil && cerr != nil {
ioErr = cerr
}
_ = pw.CloseWithError(ioErr)
}()
// Metadata file
fw, ioErr := w.CreateFormFile("metadata", args.MetaName)
if ioErr != nil {
return
}
_, ioErr = util.SafeCopy(fw, args.MetaFile)
if ioErr != nil {
return
}
// Rootfs file
if args.Type == "virtual-machine" {
fw, ioErr = w.CreateFormFile("rootfs.img", args.RootfsName)
} else {
fw, ioErr = w.CreateFormFile("rootfs", args.RootfsName)
}
if ioErr != nil {
return
}
_, ioErr = util.SafeCopy(fw, args.RootfsFile)
if ioErr != nil {
return
}
// Done writing to multipart
ioErr = w.Close()
if ioErr != nil {
return
}
ioErr = pw.Close()
if ioErr != nil {
return
}
}()
// Setup progress handler
if args.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: pr,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
args.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
} else {
body = pr
}
contentType = w.FormDataContentType()
}
// Prepare the HTTP request
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0/images", r.httpBaseURL.String()))
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", reqURL, body)
if err != nil {
return nil, err
}
// Setup the headers
req.Header.Set("Content-Type", contentType)
if image.Public {
req.Header.Set("X-Incus-public", "true")
}
if image.Filename != "" {
req.Header.Set("X-Incus-filename", image.Filename)
}
if len(image.Properties) > 0 {
imgProps := url.Values{}
for k, v := range image.Properties {
imgProps.Set(k, v)
}
req.Header.Set("X-Incus-properties", imgProps.Encode())
}
if len(image.Profiles) > 0 {
imgProfiles := url.Values{}
for _, v := range image.Profiles {
imgProfiles.Add("profile", v)
}
req.Header.Set("X-Incus-profiles", imgProfiles.Encode())
}
if len(image.Aliases) > 0 {
imgProfiles := url.Values{}
for _, v := range image.Aliases {
imgProfiles.Add("alias", v.Name)
}
req.Header.Set("X-Incus-aliases", imgProfiles.Encode())
}
// Set the user agent
if image.Source != nil && image.Source.Fingerprint != "" && image.Source.Secret != "" && image.Source.Mode == "push" {
// Set fingerprint
req.Header.Set("X-Incus-fingerprint", image.Source.Fingerprint)
// Set secret
req.Header.Set("X-Incus-secret", image.Source.Secret)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
// Handle errors
response, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
// Get to the operation
respOperation, err := response.MetadataAsOperation()
if err != nil {
return nil, err
}
// Setup an Operation wrapper
op := operation{
Operation: *respOperation,
r: r,
chActive: make(chan bool),
}
return &op, nil
}
// tryCopyImage iterates through the source server URLs until one lets it download the image.
func (r *ProtocolIncus) tryCopyImage(req api.ImagesPost, urls []string) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
// For older servers, apply the aliases after copy
if !r.HasExtension("image_create_aliases") && req.Aliases != nil {
rop.chPost = make(chan bool)
go func() {
defer close(rop.chPost)
// Wait for the main operation to finish
<-rop.chDone
if rop.err != nil {
return
}
var errs []remoteOperationResult
// Get the operation data
op, err := rop.GetTarget()
if err != nil {
errs = append(errs, remoteOperationResult{Error: err})
rop.err = remoteOperationError("Failed to get operation data", errs)
return
}
// Extract the fingerprint
fingerprint, ok := op.Metadata["fingerprint"].(string)
if !ok {
errs = append(errs, remoteOperationResult{Error: errors.New("Bad fingerprint")})
rop.err = remoteOperationError("Failed to get operation data", errs)
return
}
// Add the aliases
for _, entry := range req.Aliases {
alias := api.ImageAliasesPost{}
alias.Name = entry.Name
alias.Target = fingerprint
err := r.CreateImageAlias(alias)
if err != nil {
errs = append(errs, remoteOperationResult{Error: err})
rop.err = remoteOperationError("Failed to create image alias", errs)
return
}
}
}()
}
// Forward targetOp to remote op
go func() {
success := false
var errs []remoteOperationResult
for _, serverURL := range urls {
req.Source.Server = serverURL
op, err := r.CreateImage(req, nil)
if err != nil {
errs = append(errs, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errs = append(errs, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed remote image download", errs)
}
close(rop.chDone)
}()
return &rop, nil
}
// CopyImage copies an image from a remote server. Additional options can be passed using ImageCopyArgs.
func (r *ProtocolIncus) CopyImage(source ImageServer, image api.Image, args *ImageCopyArgs) (RemoteOperation, error) {
// Quick checks.
if r.isSameServer(source) {
return nil, errors.New("The source and target servers must be different")
}
// Handle profile list overrides.
if args != nil && args.Profiles != nil {
if !r.HasExtension("image_copy_profile") {
return nil, errors.New("The server is missing the required \"image_copy_profile\" API extension")
}
image.Profiles = args.Profiles
} else {
// If profiles aren't provided, clear the list on the source to
// avoid requiring the destination to have them all.
image.Profiles = nil
}
// Get source server connection information
info, err := source.GetConnectionInfo()
if err != nil {
return nil, err
}
// Push mode
if args != nil && args.Mode == "push" {
// Get certificate and URL
info, err := r.GetConnectionInfo()
if err != nil {
return nil, err
}
imagesPost := api.ImagesPost{
Source: &api.ImagesPostSource{
Fingerprint: image.Fingerprint,
Mode: args.Mode,
},
}
imagesPost.Aliases = args.Aliases
if args.CopyAliases {
imagesPost.Aliases = image.Aliases
if args.Aliases != nil {
imagesPost.Aliases = append(imagesPost.Aliases, args.Aliases...)
}
}
imagesPost.ExpiresAt = image.ExpiresAt
imagesPost.Properties = image.Properties
imagesPost.Public = args.Public
// Receive token from target server. This token is later passed to the source which will use
// it, together with the URL and certificate, to connect to the target.
tokenOp, err := r.CreateImage(imagesPost, nil)
if err != nil {
return nil, err
}
opAPI := tokenOp.Get()
secret, ok := opAPI.Metadata["secret"]
if !ok {
return nil, errors.New("No token provided")
}
req := api.ImageExportPost{
Target: info.URL,
Certificate: info.Certificate,
Secret: secret.(string),
Project: info.Project,
Profiles: image.Profiles,
}
exportOp, err := source.ExportImage(image.Fingerprint, req)
if err != nil {
_ = tokenOp.Cancel()
return nil, err
}
rop := remoteOperation{
targetOp: exportOp,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
_ = tokenOp.Cancel()
close(rop.chDone)
}()
return &rop, nil
}
// Relay mode
if args != nil && args.Mode == "relay" {
metaFile, err := os.CreateTemp(r.tempPath, "incus_image_")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.Remove(metaFile.Name()) }, "Failed to remove temporary file")
rootfsFile, err := os.CreateTemp(r.tempPath, "incus_image_")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.Remove(rootfsFile.Name()) }, "Failed to remove temporary file")
// Import image
req := ImageFileRequest{
MetaFile: metaFile,
RootfsFile: rootfsFile,
}
resp, err := source.GetImageFile(image.Fingerprint, req)
if err != nil {
return nil, err
}
// Export image
_, err = metaFile.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
_, err = rootfsFile.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
imagePost := api.ImagesPost{}
imagePost.Public = args.Public
imagePost.Profiles = image.Profiles
imagePost.Aliases = args.Aliases
if args.CopyAliases {
imagePost.Aliases = image.Aliases
if args.Aliases != nil {
imagePost.Aliases = append(imagePost.Aliases, args.Aliases...)
}
}
createArgs := &ImageCreateArgs{
MetaFile: metaFile,
MetaName: image.Filename,
Type: image.Type,
}
if resp.RootfsName != "" {
// Deal with split images
createArgs.RootfsFile = rootfsFile
createArgs.RootfsName = image.Filename
}
rop := remoteOperation{
chDone: make(chan bool),
}
go func() {
defer close(rop.chDone)
op, err := r.CreateImage(imagePost, createArgs)
if err != nil {
rop.err = remoteOperationError("Failed to copy image", nil)
return
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
rop.err = remoteOperationError("Failed to copy image", nil)
return
}
// Apply the aliases.
for _, entry := range imagePost.Aliases {
alias := api.ImageAliasesPost{}
alias.Name = entry.Name
alias.Target = image.Fingerprint
err := r.CreateImageAlias(alias)
if err != nil {
rop.err = remoteOperationError("Failed to add alias", nil)
return
}
}
}()
return &rop, nil
}
// Prepare the copy request
req := api.ImagesPost{
Source: &api.ImagesPostSource{
ImageSource: api.ImageSource{
Certificate: info.Certificate,
Protocol: info.Protocol,
},
Fingerprint: image.Fingerprint,
Mode: "pull",
Type: "image",
Project: info.Project,
},
ImagePut: api.ImagePut{
Profiles: image.Profiles,
},
}
if args != nil {
req.Source.ImageType = args.Type
}
// Generate secret token if needed
if !image.Public {
secret, err := source.GetImageSecret(image.Fingerprint)
if err != nil {
return nil, err
}
req.Source.Secret = secret
}
// Process the arguments
if args != nil {
req.Aliases = args.Aliases
req.AutoUpdate = args.AutoUpdate
req.Public = args.Public
if args.CopyAliases {
req.Aliases = image.Aliases
if args.Aliases != nil {
req.Aliases = append(req.Aliases, args.Aliases...)
}
}
}
return r.tryCopyImage(req, info.Addresses)
}
// UpdateImage updates the image definition.
func (r *ProtocolIncus) UpdateImage(fingerprint string, image api.ImagePut, ETag string) error {
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/images/%s", url.PathEscape(fingerprint)), image, ETag)
if err != nil {
return err
}
return nil
}
// DeleteImage requests that Incus removes an image from the store.
func (r *ProtocolIncus) DeleteImage(fingerprint string) (Operation, error) {
// Send the request
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/images/%s", url.PathEscape(fingerprint)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// RefreshImage requests that Incus issues an image refresh.
func (r *ProtocolIncus) RefreshImage(fingerprint string) (Operation, error) {
if !r.HasExtension("image_force_refresh") {
return nil, errors.New("The server is missing the required \"image_force_refresh\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/refresh", url.PathEscape(fingerprint)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// CreateImageSecret requests that Incus issues a temporary image secret.
func (r *ProtocolIncus) CreateImageSecret(fingerprint string) (Operation, error) {
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/secret", url.PathEscape(fingerprint)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// CreateImageAlias sets up a new image alias.
func (r *ProtocolIncus) CreateImageAlias(alias api.ImageAliasesPost) error {
// Send the request
_, _, err := r.query("POST", "/images/aliases", alias, "")
if err != nil {
return err
}
return nil
}
// UpdateImageAlias updates the image alias definition.
func (r *ProtocolIncus) UpdateImageAlias(name string, alias api.ImageAliasesEntryPut, ETag string) error {
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), alias, ETag)
if err != nil {
return err
}
return nil
}
// RenameImageAlias renames an existing image alias.
func (r *ProtocolIncus) RenameImageAlias(name string, alias api.ImageAliasesEntryPost) error {
// Send the request
_, _, err := r.query("POST", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), alias, "")
if err != nil {
return err
}
return nil
}
// DeleteImageAlias removes an alias from the Incus image store.
func (r *ProtocolIncus) DeleteImageAlias(name string) error {
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/images/aliases/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
// ExportImage exports (copies) an image to a remote server.
func (r *ProtocolIncus) ExportImage(fingerprint string, image api.ImageExportPost) (Operation, error) {
if !r.HasExtension("images_push_relay") {
return nil, errors.New("The server is missing the required \"images_push_relay\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/export", url.PathEscape(fingerprint)), &image, "")
if err != nil {
return nil, err
}
return op, nil
}
incus-7.3.0/client/incus_instances.go 0000664 0000000 0000000 00000270234 15232704312 0017646 0 ustar 00root root 0000000 0000000 package incus
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/gorilla/websocket"
"github.com/pkg/sftp"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/cancel"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/tcp"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
"github.com/lxc/incus/v7/shared/ws"
)
// Instance handling functions.
// instanceTypeToPath converts the instance type to a URL path prefix and query string values.
func (r *ProtocolIncus) instanceTypeToPath(instanceType api.InstanceType) (string, url.Values, error) {
v := url.Values{}
// If a specific instance type has been requested, add the instance-type filter parameter
// to the returned URL values so that it can be used in the final URL if needed to filter
// the result set being returned.
if instanceType != api.InstanceTypeAny {
v.Set("instance-type", string(instanceType))
}
return "/instances", v, nil
}
// GetInstanceNames returns a list of instance names.
func (r *ProtocolIncus) GetInstanceNames(instanceType api.InstanceType) ([]string, error) {
baseURL, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", baseURL, v.Encode()), nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceNamesAllProjects returns a list of instance names from all projects.
func (r *ProtocolIncus) GetInstanceNamesAllProjects(instanceType api.InstanceType) (map[string][]string, error) {
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("all-projects", "true")
// Fetch the raw URL values.
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
names := map[string][]string{}
for _, instance := range instances {
names[instance.Project] = append(names[instance.Project], instance.Name)
}
return names, nil
}
// GetInstances returns a list of instances.
func (r *ProtocolIncus) GetInstances(instanceType api.InstanceType) ([]api.Instance, error) {
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesWithFilter returns a filtered list of instances.
func (r *ProtocolIncus) GetInstancesWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesAllProjects returns a list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjects(instanceType api.InstanceType) ([]api.Instance, error) {
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("all-projects", "true")
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesAllProjectsWithFilter returns a filtered list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// UpdateInstances updates all instances to match the requested state.
func (r *ProtocolIncus) UpdateInstances(state api.InstancesPut, ETag string) (Operation, error) {
path, v, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s?%s", path, v.Encode()), state, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// rebuildInstance initiates a rebuild of a given instance on the Incus Protocol server and returns the corresponding operation or an error.
func (r *ProtocolIncus) rebuildInstance(instanceName string, instance api.InstanceRebuildPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/rebuild", path, url.PathEscape(instanceName)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// tryRebuildInstance attempts to rebuild a specific instance on multiple target servers identified by their URLs.
// It runs the rebuild process asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryRebuildInstance(instanceName string, req api.InstanceRebuildPost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Source.Operation
// Forward targetOp to remote op
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
if operation == "" {
req.Source.Server = serverURL
} else {
req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
}
op, err := r.rebuildInstance(instanceName, req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed instance rebuild", errors)
if op != nil {
_ = op.Cancel()
}
}
close(rop.chDone)
}()
return &rop, nil
}
// RebuildInstanceFromImage rebuilds an instance from an image.
func (r *ProtocolIncus) RebuildInstanceFromImage(source ImageServer, image api.Image, instanceName string, req api.InstanceRebuildPost) (RemoteOperation, error) {
err := r.CheckExtension("instances_rebuild")
if err != nil {
return nil, err
}
info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
if err != nil {
return nil, err
}
if info == nil {
op, err := r.rebuildInstance(instanceName, req)
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
return r.tryRebuildInstance(instanceName, req, info.Addresses, nil)
}
// RebuildInstance rebuilds an instance as empty.
func (r *ProtocolIncus) RebuildInstance(instanceName string, instance api.InstanceRebuildPost) (op Operation, err error) {
err = r.CheckExtension("instances_rebuild")
if err != nil {
return nil, err
}
return r.rebuildInstance(instanceName, instance)
}
// GetInstancesFull returns a list of instances including snapshots, backups and state.
func (r *ProtocolIncus) GetInstancesFull(instanceType api.InstanceType) ([]api.InstanceFull, error) {
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesFullWithFilter returns a filtered list of instances including snapshots, backups and state.
func (r *ProtocolIncus) GetInstancesFullWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesFullAllProjects returns a list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjects(instanceType api.InstanceType) ([]api.InstanceFull, error) {
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
v.Set("all-projects", "true")
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstancesFullAllProjectsWithFilter returns a filtered list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
return instances, nil
}
// GetInstance returns the instance entry for the provided name.
func (r *ProtocolIncus) GetInstance(name string) (*api.Instance, string, error) {
instance := api.Instance{}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "", &instance)
if err != nil {
return nil, "", err
}
return &instance, etag, nil
}
// GetInstanceFull returns the instance entry for the provided name along with snapshot information.
func (r *ProtocolIncus) GetInstanceFull(name string) (*api.InstanceFull, string, error) {
instance := api.InstanceFull{}
if !r.HasExtension("instance_get_full") {
// Backward compatibility.
ct, _, err := r.GetInstance(name)
if err != nil {
return nil, "", err
}
cs, _, err := r.GetInstanceState(name)
if err != nil {
return nil, "", err
}
snaps, err := r.GetInstanceSnapshots(name)
if err != nil {
return nil, "", err
}
backups, err := r.GetInstanceBackups(name)
if err != nil {
return nil, "", err
}
instance.Instance = *ct
instance.State = cs
instance.Snapshots = snaps
instance.Backups = backups
return &instance, "", nil
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s?recursion=1", path, url.PathEscape(name)), nil, "", &instance)
if err != nil {
return nil, "", err
}
return &instance, etag, nil
}
// CreateInstanceFromBackup is a convenience function to make it easier to
// create a instance from a backup.
func (r *ProtocolIncus) CreateInstanceFromBackup(args InstanceBackupArgs) (Operation, error) {
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if args.PoolName == "" && args.Name == "" && args.Config == nil && args.Devices == nil {
// Send the request
op, _, err := r.queryOperation("POST", path, args.BackupFile, "")
if err != nil {
return nil, err
}
return op, nil
}
if args.PoolName != "" && !r.HasExtension("container_backup_override_pool") {
return nil, errors.New(`The server is missing the required "container_backup_override_pool" API extension`)
}
if args.Name != "" && !r.HasExtension("backup_override_name") {
return nil, errors.New(`The server is missing the required "backup_override_name" API extension`)
}
if (args.Config != nil || args.Devices != nil) && !r.HasExtension("backup_override_config") {
return nil, errors.New(`The server is missing the required "backup_override_config" API extension`)
}
// Prepare the HTTP request
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", reqURL, args.BackupFile)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
if args.PoolName != "" {
req.Header.Set("X-Incus-pool", args.PoolName)
}
if args.Name != "" {
req.Header.Set("X-Incus-name", args.Name)
}
if args.Config != nil {
configOverride := strings.Join(args.Config, " ")
req.Header.Set("X-Incus-config", configOverride)
}
if args.Devices != nil {
devicesOverride := strings.Join(args.Devices, " ")
req.Header.Set("X-Incus-devices", devicesOverride)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
// Handle errors
response, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
// Get to the operation
respOperation, err := response.MetadataAsOperation()
if err != nil {
return nil, err
}
// Setup an Operation wrapper
op := operation{
Operation: *respOperation,
r: r,
chActive: make(chan bool),
}
return &op, nil
}
// CreateInstance requests that Incus creates a new instance.
func (r *ProtocolIncus) CreateInstance(instance api.InstancesPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(instance.Type)
if err != nil {
return nil, err
}
if instance.Source.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("The server is missing the required \"container_only_migration\" API extension")
}
}
// Send the request
op, _, err := r.queryOperation("POST", path, instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// tryCreateInstance attempts to create a new instance on multiple target servers specified by their URLs.
// It runs the instance creation asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryCreateInstance(req api.InstancesPost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Source.Operation
// Forward targetOp to remote op
chConnect := make(chan error, 1)
chWait := make(chan error, 1)
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
if operation == "" {
req.Source.Server = serverURL
} else {
req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
}
op, err := r.CreateInstance(req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if success {
chConnect <- nil
close(chConnect)
} else {
chConnect <- remoteOperationError("Failed instance creation", errors)
close(chConnect)
if op != nil {
_ = op.Cancel()
}
}
}()
if op != nil {
go func() {
chWait <- op.Wait()
close(chWait)
}()
}
go func() {
var err error
select {
case err = <-chConnect:
case err = <-chWait:
}
rop.err = err
close(rop.chDone)
}()
return &rop, nil
}
// CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
func (r *ProtocolIncus) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
if err != nil {
return nil, err
}
// If the source server is the same as the target server, create the instance directly.
if info == nil {
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
return r.tryCreateInstance(req, info.Addresses, nil)
}
// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
func (r *ProtocolIncus) CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (RemoteOperation, error) {
// Base request
req := api.InstancesPost{
Name: instance.Name,
InstancePut: instance.Writable(),
Type: api.InstanceType(instance.Type),
}
req.Source.BaseImage = instance.Config["volatile.base_image"]
// Process the copy arguments
if args != nil {
// Quick checks.
if args.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("The target server is missing the required \"container_only_migration\" API extension")
}
if !source.HasExtension("container_only_migration") {
return nil, errors.New("The source server is missing the required \"container_only_migration\" API extension")
}
}
if slices.Contains([]string{"push", "relay"}, args.Mode) {
if !r.HasExtension("container_push") {
return nil, errors.New("The target server is missing the required \"container_push\" API extension")
}
if !source.HasExtension("container_push") {
return nil, errors.New("The source server is missing the required \"container_push\" API extension")
}
}
if args.Mode == "push" && !source.HasExtension("container_push_target") {
return nil, errors.New("The source server is missing the required \"container_push_target\" API extension")
}
if args.Refresh {
if !r.HasExtension("container_incremental_copy") {
return nil, errors.New("The target server is missing the required \"container_incremental_copy\" API extension")
}
if !source.HasExtension("container_incremental_copy") {
return nil, errors.New("The source server is missing the required \"container_incremental_copy\" API extension")
}
}
if args.RefreshExcludeOlder && !source.HasExtension("custom_volume_refresh_exclude_older_snapshots") {
return nil, errors.New("The source server is missing the required \"custom_volume_refresh_exclude_older_snapshots\" API extension")
}
if args.AllowInconsistent {
if !r.HasExtension("instance_allow_inconsistent_copy") {
return nil, errors.New("The source server is missing the required \"instance_allow_inconsistent_copy\" API extension")
}
}
// Allow overriding the target name
if args.Name != "" {
req.Name = args.Name
}
req.Source.Live = args.Live
req.Source.InstanceOnly = args.InstanceOnly
req.Source.Refresh = args.Refresh
req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder
req.Source.AllowInconsistent = args.AllowInconsistent
}
if req.Source.Live {
req.Source.Live = instance.StatusCode == api.Running
}
sourceInfo, err := source.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get source connection info: %w", err)
}
destInfo, err := r.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
}
// Optimization for the local copy case
if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
// Project handling
if destInfo.Project != sourceInfo.Project {
if !r.HasExtension("container_copy_project") {
return nil, errors.New("The server is missing the required \"container_copy_project\" API extension")
}
req.Source.Project = sourceInfo.Project
}
// Local copy source fields
req.Source.Type = "copy"
req.Source.Source = instance.Name
// Copy the instance
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// Source request
sourceReq := api.InstancePost{
Migration: true,
Live: req.Source.Live,
InstanceOnly: req.Source.InstanceOnly,
AllowInconsistent: req.Source.AllowInconsistent,
}
// When dependent volumes are supported, Devices are sent to the
// migration source to allow overriding the per-device pools.
if source.HasExtension("dependent") {
sourceReq.Devices = req.Devices
}
// Push mode migration
if args != nil && args.Mode == "push" {
// Get target server connection information
info, err := r.GetConnectionInfo()
if err != nil {
return nil, err
}
// Create the instance
req.Source.Type = "migration"
req.Source.Mode = "push"
req.Source.Refresh = args.Refresh
req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
opAPI := op.Get()
targetSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Prepare the source request
target := api.InstancePostTarget{}
target.Operation = opAPI.ID
target.Websockets = targetSecrets
target.Certificate = info.Certificate
sourceReq.Target = &target
return r.tryMigrateInstance(source, instance.Name, sourceReq, info.Addresses, op)
}
// Get source server connection information
info, err := source.GetConnectionInfo()
if err != nil {
return nil, err
}
op, err := source.MigrateInstance(instance.Name, sourceReq)
if err != nil {
return nil, err
}
opAPI := op.Get()
sourceSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
sourceSecrets[k] = val
}
}
// Relay mode migration
if args != nil && args.Mode == "relay" {
// Push copy source fields
req.Source.Type = "migration"
req.Source.Mode = "push"
// Start the process
targetOp, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
targetOpAPI := targetOp.Get()
// Extract the websockets
targetSecrets := map[string]string{}
for k, v := range targetOpAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Launch the relay
err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
if err != nil {
return nil, err
}
// Prepare a tracking operation
rop := remoteOperation{
targetOp: targetOp,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// Pull mode migration
req.Source.Type = "migration"
req.Source.Mode = "pull"
req.Source.Operation = opAPI.ID
req.Source.Websockets = sourceSecrets
req.Source.Certificate = info.Certificate
return r.tryCreateInstance(req, info.Addresses, op)
}
// UpdateInstance updates the instance definition.
func (r *ProtocolIncus) UpdateInstance(name string, instance api.InstancePut, ETag string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if instance.DiskOnly {
err = r.CheckExtension("instance_snapshot_disk_only_restore")
if err != nil {
return nil, errors.New("The server is missing the required \"instance_snapshot_disk_only_restore\" API extension")
}
}
// Send the request
op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// RenameInstance requests that Incus renames the instance.
func (r *ProtocolIncus) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if instance.Migration {
return nil, errors.New("Can't ask for a migration through RenameInstance")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// tryMigrateInstance attempts to migrate a specific instance from a source server to one of the target URLs.
// The function runs the migration operation asynchronously and returns a RemoteOperation to track the progress and handle any errors.
func (r *ProtocolIncus) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The target server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Target.Operation
// Forward targetOp to remote op
chConnect := make(chan error, 1)
chWait := make(chan error, 1)
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
op, err := source.MigrateInstance(name, req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.targetOp = op
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if success {
chConnect <- nil
close(chConnect)
} else {
chConnect <- remoteOperationError("Failed instance migration", errors)
close(chConnect)
if op != nil {
_ = op.Cancel()
}
}
}()
if op != nil {
go func() {
chWait <- op.Wait()
close(chWait)
}()
}
go func() {
var err error
select {
case err = <-chConnect:
case err = <-chWait:
}
rop.err = err
close(rop.chDone)
}()
return &rop, nil
}
// MigrateInstance requests that Incus prepares for a instance migration.
func (r *ProtocolIncus) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if instance.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("The server is missing the required \"container_only_migration\" API extension")
}
}
if instance.Pool != "" && !r.HasExtension("instance_pool_move") {
return nil, errors.New("The server is missing the required \"instance_pool_move\" API extension")
}
if instance.Project != "" && !r.HasExtension("instance_project_move") {
return nil, errors.New("The server is missing the required \"instance_project_move\" API extension")
}
if instance.AllowInconsistent && !r.HasExtension("cluster_migration_inconsistent_copy") {
return nil, errors.New("The server is missing the required \"cluster_migration_inconsistent_copy\" API extension")
}
// Quick check.
if !instance.Migration {
return nil, errors.New("Can't ask for a rename through MigrateInstance")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteInstance requests that Incus deletes the instance.
func (r *ProtocolIncus) DeleteInstance(name string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// ExecInstance requests that Incus spawns a command inside the instance.
func (r *ProtocolIncus) ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (Operation, error) {
// Ensure args are equivalent to empty InstanceExecArgs.
if args == nil {
args = &InstanceExecArgs{}
}
if exec.RecordOutput {
if !r.HasExtension("container_exec_recording") {
return nil, errors.New("The server is missing the required \"container_exec_recording\" API extension")
}
}
if exec.User > 0 || exec.Group > 0 || exec.Cwd != "" {
if !r.HasExtension("container_exec_user_group_cwd") {
return nil, errors.New("The server is missing the required \"container_exec_user_group_cwd\" API extension")
}
}
var uri string
if r.IsAgent() {
uri = "/exec"
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
uri = fmt.Sprintf("%s/%s/exec", path, url.PathEscape(instanceName))
}
// Send the request
op, _, err := r.queryOperation("POST", uri, exec, "")
if err != nil {
return nil, err
}
opAPI := op.Get()
// Process additional arguments
// Parse the fds
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
if exec.RecordOutput && (args.Stdout != nil || args.Stderr != nil) {
err = op.Wait()
if err != nil {
return nil, err
}
opAPI = op.Get()
outputFiles := map[string]string{}
outputs, ok := opAPI.Metadata["output"].(map[string]any)
if ok {
for k, v := range outputs {
val, ok := v.(string)
if ok {
outputFiles[k] = val
}
}
}
if outputFiles["1"] != "" {
reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if args.Stdout != nil {
_, errCopy := util.SafeCopy(args.Stdout, reader)
// Regardless of errCopy value, we want to delete the file after a copy operation
errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if errDelete != nil {
return nil, errDelete
}
if errCopy != nil {
return nil, fmt.Errorf("Could not copy the content of the exec output log file to stdout: %w", err)
}
}
err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if err != nil {
return nil, err
}
}
if outputFiles["2"] != "" {
reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
if args.Stderr != nil {
_, errCopy := util.SafeCopy(args.Stderr, reader)
errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if errDelete != nil {
return nil, errDelete
}
if errCopy != nil {
return nil, fmt.Errorf("Could not copy the content of the exec output log file to stderr: %w", err)
}
}
err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
if err != nil {
return nil, err
}
}
}
if fds[api.SecretNameControl] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
go func() {
_, _, _ = conn.ReadMessage() // Consume pings from server.
}()
if args.Control != nil {
// Call the control handler with a connection to the control socket
go args.Control(conn)
}
}
if exec.Interactive {
// Handle interactive sections
if args.Stdin != nil && args.Stdout != nil {
// Connect to the websocket
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return nil, err
}
// And attach stdin and stdout to it
go func() {
ws.MirrorRead(conn, args.Stdin)
<-ws.MirrorWrite(conn, args.Stdout)
_ = conn.Close()
if args.DataDone != nil {
close(args.DataDone)
}
}()
} else {
if args.DataDone != nil {
close(args.DataDone)
}
}
} else {
// Handle non-interactive sessions
dones := make(map[int]chan error)
conns := []*websocket.Conn{}
// Handle stdin
if fds["0"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return nil, err
}
go func() {
_, _, _ = conn.ReadMessage() // Consume pings from server.
}()
conns = append(conns, conn)
dones[0] = ws.MirrorRead(conn, args.Stdin)
}
waitConns := 0 // Used for keeping track of when stdout and stderr have finished.
// Handle stdout
if fds["1"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
if err != nil {
return nil, err
}
// Discard Stdout from remote command if output writer not supplied.
if args.Stdout == nil {
args.Stdout = io.Discard
}
conns = append(conns, conn)
dones[1] = ws.MirrorWrite(conn, args.Stdout)
waitConns++
}
// Handle stderr
if fds["2"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
if err != nil {
return nil, err
}
// Discard Stderr from remote command if output writer not supplied.
if args.Stderr == nil {
args.Stderr = io.Discard
}
conns = append(conns, conn)
dones[2] = ws.MirrorWrite(conn, args.Stderr)
waitConns++
}
// Wait for everything to be done
go func() {
for {
select {
case <-dones[0]:
// Handle stdin finish, but don't wait for it if output channels
// have all finished.
dones[0] = nil
_ = conns[0].Close()
case <-dones[1]:
dones[1] = nil
_ = conns[1].Close()
waitConns--
case <-dones[2]:
dones[2] = nil
_ = conns[2].Close()
waitConns--
}
if waitConns <= 0 {
// Close stdin websocket if defined and not already closed.
if dones[0] != nil {
conns[0].Close()
}
break
}
}
if args.DataDone != nil {
close(args.DataDone)
}
}()
}
return op, nil
}
// GetInstanceFile retrieves the provided path from the instance.
func (r *ProtocolIncus) GetInstanceFile(instanceName string, filePath string) (io.ReadCloser, *InstanceFileResponse, error) {
var err error
var requestURL string
urlEncode := func(path string, query map[string]string) (string, error) {
u, err := url.Parse(path)
if err != nil {
return "", err
}
params := url.Values{}
for key, value := range query {
params.Add(key, value)
}
u.RawQuery = params.Encode()
return u.String(), nil
}
if r.IsAgent() {
requestURL, err = urlEncode(
fmt.Sprintf("%s/1.0/files", r.httpBaseURL.String()),
map[string]string{"path": filePath},
)
} else {
var path string
path, _, err = r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, nil, err
}
// Prepare the HTTP request
requestURL, err = urlEncode(
fmt.Sprintf("%s/1.0%s/%s/files", r.httpBaseURL.String(), path, url.PathEscape(instanceName)),
map[string]string{"path": filePath},
)
}
if err != nil {
return nil, nil, err
}
requestURL, err = r.setQueryAttributes(requestURL)
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, nil, err
}
}
// Parse the headers
uid, gid, mode, fileType, _ := api.ParseFileHeaders(resp.Header)
fileResp := InstanceFileResponse{
UID: uid,
GID: gid,
Mode: mode,
Type: fileType,
}
if fileResp.Type == "directory" {
// Decode the response
response := api.Response{}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&response)
if err != nil {
return nil, nil, err
}
// Get the file list
entries := []string{}
err = response.MetadataAsStruct(&entries)
if err != nil {
return nil, nil, err
}
fileResp.Entries = entries
return nil, &fileResp, err
}
return resp.Body, &fileResp, err
}
// CreateInstanceFile tells Incus to create a file in the instance.
func (r *ProtocolIncus) CreateInstanceFile(instanceName string, filePath string, args InstanceFileArgs) error {
if args.Type == "directory" {
if !r.HasExtension("directory_manipulation") {
return errors.New("The server is missing the required \"directory_manipulation\" API extension")
}
}
if args.Type == "symlink" {
if !r.HasExtension("file_symlinks") {
return errors.New("The server is missing the required \"file_symlinks\" API extension")
}
}
if args.WriteMode == "append" {
if !r.HasExtension("file_append") {
return errors.New("The server is missing the required \"file_append\" API extension")
}
}
var requestURL string
if r.IsAgent() {
requestURL = fmt.Sprintf("%s/1.0/files?path=%s", r.httpBaseURL.String(), url.QueryEscape(filePath))
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Prepare the HTTP request
requestURL = fmt.Sprintf("%s/1.0%s/%s/files?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(filePath))
}
requestURL, err := r.setQueryAttributes(requestURL)
if err != nil {
return err
}
req, err := http.NewRequest("POST", requestURL, args.Content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
_, err := args.Content.Seek(0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(args.Content), nil
}
// Set the various headers
if args.UID > -1 {
req.Header.Set("X-Incus-uid", fmt.Sprintf("%d", args.UID))
}
if args.GID > -1 {
req.Header.Set("X-Incus-gid", fmt.Sprintf("%d", args.GID))
}
if args.Mode > -1 {
req.Header.Set("X-Incus-mode", fmt.Sprintf("%04o", args.Mode))
}
if args.Type != "" {
req.Header.Set("X-Incus-type", args.Type)
}
if args.WriteMode != "" {
req.Header.Set("X-Incus-write", args.WriteMode)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return err
}
// Check the return value for a cleaner error
_, _, err = incusParseResponse(resp)
if err != nil {
return err
}
return nil
}
// DeleteInstanceFile deletes a file in the instance.
func (r *ProtocolIncus) DeleteInstanceFile(instanceName string, filePath string) error {
if !r.HasExtension("file_delete") {
return errors.New("The server is missing the required \"file_delete\" API extension")
}
var requestURL string
if r.IsAgent() {
requestURL = fmt.Sprintf("/files?path=%s", url.QueryEscape(filePath))
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Prepare the HTTP request
requestURL = fmt.Sprintf("%s/%s/files?path=%s", path, url.PathEscape(instanceName), url.QueryEscape(filePath))
}
requestURL, err := r.setQueryAttributes(requestURL)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", requestURL, nil, "")
if err != nil {
return err
}
return nil
}
// rawConn connects to the apiURL, upgrades to the requested protocol and returns it.
func (r *ProtocolIncus) rawConn(method string, apiURL *url.URL, protocol string, data any) (net.Conn, error) {
// Get the HTTP transport.
httpTransport, err := r.getUnderlyingHTTPTransport()
if err != nil {
return nil, err
}
req := &http.Request{
Method: method,
URL: apiURL,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: apiURL.Host,
}
req.Header["Upgrade"] = []string{protocol}
req.Header["Connection"] = []string{"Upgrade"}
// Add the request body.
if data != nil {
body, err := json.Marshal(data)
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body))
req.Header.Set("Content-Type", "application/json")
}
r.addClientHeaders(req)
// Add the default port if missing as the raw dialers don't apply it.
addr := apiURL.Host
if apiURL.Port() == "" {
addr = net.JoinHostPort(apiURL.Hostname(), "443")
}
// Establish the connection.
var conn net.Conn
if httpTransport.TLSClientConfig != nil {
conn, err = httpTransport.DialTLSContext(context.Background(), "tcp", addr)
} else {
conn, err = httpTransport.DialContext(context.Background(), "tcp", addr)
}
if err != nil {
return nil, err
}
remoteTCP, _ := tcp.ExtractConn(conn)
if remoteTCP != nil {
err = tcp.SetTimeouts(remoteTCP, 0)
if err != nil {
return nil, err
}
}
err = req.Write(conn)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
if resp.Header.Get("Upgrade") != protocol {
return nil, errors.New("Missing or unexpected Upgrade header in response")
}
return conn, nil
}
// GetInstanceNBDConn returns a connection to the instance's NBD endpoint exposing all of its disks.
func (r *ProtocolIncus) GetInstanceNBDConn(instanceName string, args InstanceNBDArgs) (net.Conn, error) {
if !r.HasExtension("instance_nbd") {
return nil, errors.New(`The server is missing the required "instance_nbd" API extension`)
}
apiURL := api.NewURL()
apiURL.URL = r.httpBaseURL // Preload the URL with the client base URL.
apiURL.Path("1.0", "instances", instanceName, "nbd")
values := apiURL.Query()
if args.Reuse {
values.Set("reuse", "1")
}
apiURL.RawQuery = values.Encode()
r.setURLQueryAttributes(&apiURL.URL)
return r.rawConn(http.MethodGet, &apiURL.URL, "nbd", nil)
}
// GetInstancePortForwardConn returns a connection to the given address and TCP port inside of the instance.
func (r *ProtocolIncus) GetInstancePortForwardConn(instanceName string, forward api.InstancePortForwardPost) (net.Conn, error) {
if !r.HasExtension("instance_port_forward") {
return nil, errors.New(`The server is missing the required "instance_port_forward" API extension`)
}
apiURL := api.NewURL()
apiURL.URL = r.httpBaseURL // Preload the URL with the client base URL.
apiURL.Path("1.0", "instances", instanceName, "port-forward")
r.setURLQueryAttributes(&apiURL.URL)
return r.rawConn(http.MethodPost, &apiURL.URL, "tcp", forward)
}
// GetInstanceFileSFTPConn returns a connection to the instance's SFTP endpoint.
func (r *ProtocolIncus) GetInstanceFileSFTPConn(instanceName string) (net.Conn, error) {
apiURL := api.NewURL()
apiURL.URL = r.httpBaseURL // Preload the URL with the client base URL.
apiURL.Path("1.0", "instances", instanceName, "sftp")
r.setURLQueryAttributes(&apiURL.URL)
return r.rawConn(http.MethodGet, &apiURL.URL, "sftp", nil)
}
// GetInstanceFileSFTP returns an SFTP connection to the instance.
func (r *ProtocolIncus) GetInstanceFileSFTP(instanceName string) (*sftp.Client, error) {
conn, err := r.GetInstanceFileSFTPConn(instanceName)
if err != nil {
return nil, err
}
// Get a SFTP client.
client, err := sftp.NewClientPipe(conn, conn, sftp.MaxPacketUnchecked(128*1024))
if err != nil {
_ = conn.Close()
return nil, err
}
go func() {
// Wait for the client to be done before closing the connection.
_ = client.Wait()
_ = conn.Close()
}()
return client, nil
}
// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
func (r *ProtocolIncus) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceSnapshots returns a list of snapshots for the instance.
func (r *ProtocolIncus) GetInstanceSnapshots(instanceName string) ([]api.InstanceSnapshot, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
snapshots := []api.InstanceSnapshot{}
// Fetch the raw value
_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots?recursion=1", path, url.PathEscape(instanceName)), nil, "", &snapshots)
if err != nil {
return nil, err
}
return snapshots, nil
}
// GetInstanceSnapshot returns a Snapshot struct for the provided instance and snapshot names.
func (r *ProtocolIncus) GetInstanceSnapshot(instanceName string, name string) (*api.InstanceSnapshot, string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
snapshot := api.InstanceSnapshot{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &snapshot)
if err != nil {
return nil, "", err
}
return &snapshot, etag, nil
}
// CreateInstanceSnapshot requests that Incus creates a new snapshot for the instance.
func (r *ProtocolIncus) CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Validate the request
if snapshot.ExpiresAt != nil && !r.HasExtension("snapshot_expiry_creation") {
return nil, errors.New("The server is missing the required \"snapshot_expiry_creation\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName)), snapshot, "")
if err != nil {
return nil, err
}
return op, nil
}
// CopyInstanceSnapshot copies a snapshot from a remote server into a new instance. Additional options can be passed using InstanceCopyArgs.
func (r *ProtocolIncus) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (RemoteOperation, error) {
// Backward compatibility (with broken Name field)
fields := strings.Split(snapshot.Name, "/")
cName := instanceName
sName := fields[len(fields)-1]
// Base request
req := api.InstancesPost{
Name: cName,
InstancePut: api.InstancePut{
Architecture: snapshot.Architecture,
Config: snapshot.Config,
Devices: snapshot.Devices,
Ephemeral: snapshot.Ephemeral,
Profiles: snapshot.Profiles,
},
}
if snapshot.Stateful && args.Live {
if !r.HasExtension("container_snapshot_stateful_migration") {
return nil, errors.New("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
}
req.Stateful = snapshot.Stateful
req.Source.Live = false // Snapshots are never running and so we don't need live migration.
}
req.Source.BaseImage = snapshot.Config["volatile.base_image"]
// Process the copy arguments
if args != nil {
// Quick checks.
if slices.Contains([]string{"push", "relay"}, args.Mode) {
if !r.HasExtension("container_push") {
return nil, errors.New("The target server is missing the required \"container_push\" API extension")
}
if !source.HasExtension("container_push") {
return nil, errors.New("The source server is missing the required \"container_push\" API extension")
}
}
if args.Mode == "push" && !source.HasExtension("container_push_target") {
return nil, errors.New("The source server is missing the required \"container_push_target\" API extension")
}
// Allow overriding the target name
if args.Name != "" {
req.Name = args.Name
}
}
sourceInfo, err := source.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get source connection info: %w", err)
}
destInfo, err := r.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
}
instance, _, err := source.GetInstance(cName)
if err != nil {
return nil, fmt.Errorf("Failed to get instance info: %w", err)
}
// Optimization for the local copy case
if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
// Project handling
if destInfo.Project != sourceInfo.Project {
if !r.HasExtension("container_copy_project") {
return nil, errors.New("The server is missing the required \"container_copy_project\" API extension")
}
req.Source.Project = sourceInfo.Project
}
// Local copy source fields
req.Source.Type = "copy"
req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)
// Copy the instance
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// If deadling with migration, we need to set the type.
if source.HasExtension("virtual-machines") {
inst, _, err := source.GetInstance(instanceName)
if err != nil {
return nil, err
}
req.Type = api.InstanceType(inst.Type)
}
// Source request
sourceReq := api.InstanceSnapshotPost{
Migration: true,
Name: args.Name,
}
if snapshot.Stateful && args.Live {
sourceReq.Live = args.Live
}
// Push mode migration
if args != nil && args.Mode == "push" {
// Get target server connection information
info, err := r.GetConnectionInfo()
if err != nil {
return nil, err
}
// Create the instance
req.Source.Type = "migration"
req.Source.Mode = "push"
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
opAPI := op.Get()
targetSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Prepare the source request
target := api.InstancePostTarget{}
target.Operation = opAPI.ID
target.Websockets = targetSecrets
target.Certificate = info.Certificate
sourceReq.Target = &target
return r.tryMigrateInstanceSnapshot(source, cName, sName, sourceReq, info.Addresses)
}
// Get source server connection information
info, err := source.GetConnectionInfo()
if err != nil {
return nil, err
}
op, err := source.MigrateInstanceSnapshot(cName, sName, sourceReq)
if err != nil {
return nil, err
}
opAPI := op.Get()
sourceSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
sourceSecrets[k] = val
}
}
// Relay mode migration
if args != nil && args.Mode == "relay" {
// Push copy source fields
req.Source.Type = "migration"
req.Source.Mode = "push"
// Start the process
targetOp, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
targetOpAPI := targetOp.Get()
// Extract the websockets
targetSecrets := map[string]string{}
for k, v := range targetOpAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Launch the relay
err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
if err != nil {
return nil, err
}
// Prepare a tracking operation
rop := remoteOperation{
targetOp: targetOp,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// Pull mode migration
req.Source.Type = "migration"
req.Source.Mode = "pull"
req.Source.Operation = opAPI.ID
req.Source.Websockets = sourceSecrets
req.Source.Certificate = info.Certificate
return r.tryCreateInstance(req, info.Addresses, op)
}
// RenameInstanceSnapshot requests that Incus renames the snapshot.
func (r *ProtocolIncus) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if instance.Migration {
return nil, errors.New("Can't ask for a migration through RenameInstanceSnapshot")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
func (r *ProtocolIncus) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The target server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Target.Operation
// Forward targetOp to remote op
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
op, err := source.MigrateInstanceSnapshot(instanceName, name, req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.targetOp = op
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed instance migration", errors)
}
close(rop.chDone)
}()
return &rop, nil
}
// MigrateInstanceSnapshot requests that Incus prepares for a snapshot migration.
func (r *ProtocolIncus) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if !instance.Migration {
return nil, errors.New("Can't ask for a rename through MigrateInstanceSnapshot")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteInstanceSnapshot requests that Incus deletes the instance snapshot.
func (r *ProtocolIncus) DeleteInstanceSnapshot(instanceName string, name string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// UpdateInstanceSnapshot requests that Incus updates the instance snapshot.
func (r *ProtocolIncus) UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("snapshot_expiry") {
return nil, errors.New("The server is missing the required \"snapshot_expiry\" API extension")
}
// Send the request
op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// GetInstanceState returns a InstanceState entry for the provided instance name.
func (r *ProtocolIncus) GetInstanceState(name string) (*api.InstanceState, string, error) {
var uri string
if r.IsAgent() {
uri = "/state"
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
uri = fmt.Sprintf("%s/%s/state", path, url.PathEscape(name))
}
state := api.InstanceState{}
// Fetch the raw value
etag, err := r.queryStruct("GET", uri, nil, "", &state)
if err != nil {
return nil, "", err
}
return &state, etag, nil
}
// UpdateInstanceState updates the instance to match the requested state.
func (r *ProtocolIncus) UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/state", path, url.PathEscape(name)), state, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// GetInstanceAccess returns an Access entry for the provided instance name.
func (r *ProtocolIncus) GetInstanceAccess(name string) (api.Access, error) {
access := api.Access{}
if !r.HasExtension("instance_access") {
return nil, errors.New("The server is missing the required \"instance_access\" API extension")
}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/access", url.PathEscape(name)), nil, "", &access)
if err != nil {
return nil, err
}
return access, nil
}
// GetInstanceLogfiles returns a list of logfiles for the instance.
func (r *ProtocolIncus) GetInstanceLogfiles(name string) ([]string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/logs", path, url.PathEscape(name))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceLogfile returns the content of the requested logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/logs/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// DeleteInstanceLogfile deletes the requested logfile.
func (r *ProtocolIncus) DeleteInstanceLogfile(name string, filename string) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/%s", path, url.PathEscape(name), url.PathEscape(filename)), nil, "")
if err != nil {
return err
}
return nil
}
// getInstanceExecOutputLogFile returns the content of the requested exec logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) getInstanceExecOutputLogFile(name string, filename string) (io.ReadCloser, error) {
err := r.CheckExtension("container_exec_recording")
if err != nil {
return nil, err
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/logs/exec-output/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, nil
}
// deleteInstanceExecOutputLogFiles deletes the requested exec logfile.
func (r *ProtocolIncus) deleteInstanceExecOutputLogFile(instanceName string, filename string) error {
err := r.CheckExtension("container_exec_recording")
if err != nil {
return err
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/exec-output/%s", path, url.PathEscape(instanceName), url.PathEscape(filename)), nil, "")
if err != nil {
return err
}
return nil
}
// GetInstanceMetadata returns instance metadata.
func (r *ProtocolIncus) GetInstanceMetadata(name string) (*api.ImageMetadata, string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
if !r.HasExtension("container_edit_metadata") {
return nil, "", errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
metadata := api.ImageMetadata{}
uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
etag, err := r.queryStruct("GET", uri, nil, "", &metadata)
if err != nil {
return nil, "", err
}
return &metadata, etag, err
}
// UpdateInstanceMetadata sets the content of the instance metadata file.
func (r *ProtocolIncus) UpdateInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("container_edit_metadata") {
return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
_, _, err = r.query("PUT", uri, metadata, ETag)
if err != nil {
return err
}
return nil
}
// GetInstanceTemplateFiles returns the list of names of template files for a instance.
func (r *ProtocolIncus) GetInstanceTemplateFiles(instanceName string) ([]string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_edit_metadata") {
return nil, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
templates := []string{}
uri := fmt.Sprintf("%s/%s/metadata/templates", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", uri, nil, "", &templates)
if err != nil {
return nil, err
}
return templates, nil
}
// GetInstanceTemplateFile returns the content of a template file for a instance.
func (r *ProtocolIncus) GetInstanceTemplateFile(instanceName string, templateName string) (io.ReadCloser, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_edit_metadata") {
return nil, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// CreateInstanceTemplateFile creates an a template for a instance.
func (r *ProtocolIncus) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("container_edit_metadata") {
return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return err
}
req, err := http.NewRequest("POST", uri, content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
_, err := content.Seek(0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(content), nil
}
req.Header.Set("Content-Type", "application/octet-stream")
// Send the request
resp, err := r.DoHTTP(req)
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return err
}
}
return err
}
// DeleteInstanceTemplateFile deletes a template file for a instance.
func (r *ProtocolIncus) DeleteInstanceTemplateFile(name string, templateName string) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("container_edit_metadata") {
return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/metadata/templates?path=%s", path, url.PathEscape(name), url.QueryEscape(templateName)), nil, "")
return err
}
// ConsoleInstance requests that Incus attaches to the console device of a instance.
func (r *ProtocolIncus) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("console") {
return nil, errors.New("The server is missing the required \"console\" API extension")
}
if console.Type == "" {
console.Type = "console"
}
if console.Type == "vga" && !r.HasExtension("console_vga_type") {
return nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
}
if console.Force && !r.HasExtension("console_force") {
return nil, errors.New(`The server is missing the required "console_force" API extension`)
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "")
if err != nil {
return nil, err
}
opAPI := op.Get()
if args == nil || args.Terminal == nil {
return nil, errors.New("A terminal must be set")
}
if args.Control == nil {
return nil, errors.New("A control channel must be set")
}
// Parse the fds
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
var controlConn *websocket.Conn
// Call the control handler with a connection to the control socket
if fds[api.SecretNameControl] == "" {
return nil, errors.New("Did not receive a file descriptor for the control channel")
}
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
go args.Control(controlConn)
// Connect to the websocket
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return nil, err
}
// Detach from console.
go func(consoleDisconnect <-chan bool) {
<-consoleDisconnect
msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
// We don't care if this fails. This is just for convenience.
_ = controlConn.WriteMessage(websocket.CloseMessage, msg)
_ = controlConn.Close()
}(args.ConsoleDisconnect)
// And attach stdin and stdout to it
go func() {
_, writeDone := ws.Mirror(conn, args.Terminal)
<-writeDone
_ = conn.Close()
}()
return op, nil
}
// ConsoleInstanceDynamic requests that Incus attaches to the console device of a
// instance with the possibility of opening multiple connections to it.
//
// Every time the returned 'console' function is called, a new connection will
// be established and proxied to the given io.ReadWriteCloser.
func (r *ProtocolIncus) ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, nil, err
}
if !r.HasExtension("console") {
return nil, nil, errors.New("The server is missing the required \"console\" API extension")
}
if console.Type == "" {
console.Type = "console"
}
if console.Type == "vga" && !r.HasExtension("console_vga_type") {
return nil, nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
}
if console.Force && !r.HasExtension("console_force") {
return nil, nil, errors.New(`The server is missing the required "console_force" API extension`)
}
// Send the request.
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "")
if err != nil {
return nil, nil, err
}
opAPI := op.Get()
if args == nil {
return nil, nil, errors.New("No arguments provided")
}
if args.Control == nil {
return nil, nil, errors.New("A control channel must be set")
}
// Parse the fds.
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
// Call the control handler with a connection to the control socket.
if fds[api.SecretNameControl] == "" {
return nil, nil, errors.New("Did not receive a file descriptor for the control channel")
}
controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, nil, err
}
go args.Control(controlConn)
// Handle main disconnect.
go func(consoleDisconnect <-chan bool) {
<-consoleDisconnect
msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
// We don't care if this fails. This is just for convenience.
_ = controlConn.WriteMessage(websocket.CloseMessage, msg)
_ = controlConn.Close()
}(args.ConsoleDisconnect)
f := func(rwc io.ReadWriteCloser) error {
// Connect to the websocket.
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return err
}
// Attach reader/writer.
_, writeDone := ws.Mirror(conn, rwc)
<-writeDone
_ = conn.Close()
return nil
}
return op, f, nil
}
// GetInstanceConsoleLog requests that Incus attaches to the console device of a instance.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) (io.ReadCloser, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("console") {
return nil, errors.New("The server is missing the required \"console\" API extension")
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/console", r.httpBaseURL.String(), path, url.PathEscape(instanceName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// DeleteInstanceConsoleLog deletes the requested instance's console log.
func (r *ProtocolIncus) DeleteInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("console") {
return errors.New("The server is missing the required \"console\" API extension")
}
// Send the request
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), nil, "")
if err != nil {
return err
}
return nil
}
// GetInstanceBackupNames returns a list of backup names for the instance.
func (r *ProtocolIncus) GetInstanceBackupNames(instanceName string) ([]string, error) {
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceBackups returns a list of backups for the instance.
func (r *ProtocolIncus) GetInstanceBackups(instanceName string) ([]api.InstanceBackup, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
// Fetch the raw value
backups := []api.InstanceBackup{}
_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/backups?recursion=1", path, url.PathEscape(instanceName)), nil, "", &backups)
if err != nil {
return nil, err
}
return backups, nil
}
// GetInstanceBackup returns a Backup struct for the provided instance and backup names.
func (r *ProtocolIncus) GetInstanceBackup(instanceName string, name string) (*api.InstanceBackup, string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
if !r.HasExtension("container_backup") {
return nil, "", errors.New("The server is missing the required \"container_backup\" API extension")
}
// Fetch the raw value
backup := api.InstanceBackup{}
etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &backup)
if err != nil {
return nil, "", err
}
return &backup, etag, nil
}
// CreateInstanceBackup requests that Incus creates a new backup for the instance.
func (r *ProtocolIncus) CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName)), backup, "")
if err != nil {
return nil, err
}
return op, nil
}
// RenameInstanceBackup requests that Incus renames the backup.
func (r *ProtocolIncus) RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), backup, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteInstanceBackup requests that Incus deletes the instance backup.
func (r *ProtocolIncus) DeleteInstanceBackup(instanceName string, name string) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// GetInstanceBackupFile requests the instance backup content.
func (r *ProtocolIncus) GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
// Build the URL
uri := fmt.Sprintf("%s/1.0%s/%s/backups/%s/export", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.PathEscape(name))
if r.project != "" {
uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
}
// Prepare the download request
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return nil, err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(response)
if err != nil {
return nil, err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Length: response.ContentLength,
Handler: func(percent int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
},
},
}
}
size, err := util.SafeCopy(req.BackupFile, body)
if err != nil {
return nil, err
}
resp := BackupFileResponse{}
resp.Size = size
return &resp, nil
}
// CreateInstanceBackupStream requests that Incus creates and returns new direct backup for the
// instance.
func (r *ProtocolIncus) CreateInstanceBackupStream(instanceName string, backup api.InstanceBackupsPost, req *BackupFileRequest) error {
if !r.HasExtension("direct_backup") {
return errors.New("The server is missing the required \"direct_backup\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Build the URL
uri := fmt.Sprintf("%s/1.0%s/%s/backups", r.httpBaseURL.String(), path, url.PathEscape(instanceName))
if r.project != "" {
uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
}
// Encode the backup data
buf := bytes.Buffer{}
err = json.NewEncoder(&buf).Encode(backup)
if err != nil {
return err
}
// Prepare the download request
request, err := http.NewRequest("POST", uri, bytes.NewReader(buf.Bytes()))
if err != nil {
return err
}
request.Header.Set("Accept", "application/octet-stream")
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err = incusParseResponse(response)
if err != nil {
return err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
}
_, err = util.SafeCopy(req.BackupFile, body)
return err
}
func (r *ProtocolIncus) proxyMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
// Quick checks.
for n := range targetSecrets {
_, ok := sourceSecrets[n]
if !ok {
return fmt.Errorf("Migration target expects the \"%s\" socket but source isn't providing it", n)
}
}
if targetSecrets[api.SecretNameControl] == "" {
return errors.New("Migration target didn't setup the required \"control\" socket")
}
// Struct used to hold everything together
type proxy struct {
done chan struct{}
sourceConn *websocket.Conn
targetConn *websocket.Conn
}
proxies := map[string]*proxy{}
// Connect the control socket
sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[api.SecretNameControl])
if err != nil {
return err
}
targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[api.SecretNameControl])
if err != nil {
return err
}
proxies[api.SecretNameControl] = &proxy{
done: ws.Proxy(sourceConn, targetConn),
sourceConn: sourceConn,
targetConn: targetConn,
}
// Connect the data sockets
for name := range sourceSecrets {
if name == api.SecretNameControl {
continue
}
// Handle resets (used for multiple objects)
sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[name])
if err != nil {
break
}
targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[name])
if err != nil {
break
}
proxies[name] = &proxy{
sourceConn: sourceConn,
targetConn: targetConn,
done: ws.Proxy(sourceConn, targetConn),
}
}
// Cleanup once everything is done
go func() {
// Wait for control socket
<-proxies[api.SecretNameControl].done
_ = proxies[api.SecretNameControl].sourceConn.Close()
_ = proxies[api.SecretNameControl].targetConn.Close()
// Then deal with the others
for name, proxy := range proxies {
if name == api.SecretNameControl {
continue
}
<-proxy.done
_ = proxy.sourceConn.Close()
_ = proxy.targetConn.Close()
}
}()
return nil
}
// GetInstanceDebugMemory retrieves memory debug information for a given instance and saves it to the specified file path.
func (r *ProtocolIncus) GetInstanceDebugMemory(name string, format string) (io.ReadCloser, error) {
path, v, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, err
}
v.Set("format", format)
// Prepare the HTTP request
requestURL := fmt.Sprintf("%s/1.0%s/%s/debug/memory?%s", r.httpBaseURL.String(), path, url.PathEscape(name), v.Encode())
requestURL, err = r.setQueryAttributes(requestURL)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, nil
}
// CreateInstanceBitmap requests that Incus creates a new bitmap for the instance.
func (r *ProtocolIncus) CreateInstanceBitmap(name string, bitmap api.StorageVolumeBitmapsPost) error {
if !r.HasExtension("storage_volume_nbd") {
return errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("POST", fmt.Sprintf("%s/%s/bitmaps", path, url.PathEscape(name)), bitmap, "")
if err != nil {
return err
}
return nil
}
// RepairInstance requests that Incus runs a low-level repair action on the instance.
func (r *ProtocolIncus) RepairInstance(name string, repair api.InstanceDebugRepairPost) error {
if !r.HasExtension("instances_debug_repair") {
return errors.New("The server is missing the required \"instances_debug_repair\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("POST", fmt.Sprintf("%s/%s/debug/repair", path, url.PathEscape(name)), repair, "")
if err != nil {
return err
}
return nil
}
func (r *ProtocolIncus) getInstanceNVRAM(name string, guid string, varName string, accept string) (*http.Response, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, err
}
// Prepare the HTTP request
requestURL := fmt.Sprintf("%s/1.0%s/%s/nvram/%s/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(guid), url.PathEscape(varName))
requestURL, err = r.setQueryAttributes(requestURL)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", accept)
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetInstanceNVRAM gets OVMF variables from an instance.
func (r *ProtocolIncus) GetInstanceNVRAM(name string) (map[string]map[string]*api.InstanceNVRAMVariable, error) {
if !r.HasExtension("instance_nvram") {
return nil, errors.New(`The server is missing the required "instance_nvram" API extension`)
}
vars := map[string]map[string]*api.InstanceNVRAMVariable{}
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, err
}
// Fetch the raw value.
_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/nvram?recursion=2", path, url.PathEscape(name)), nil, "", &vars)
if err != nil {
return nil, err
}
return vars, err
}
// GetInstanceNVRAMGUID gets namespaced OVMF variables from an instance.
func (r *ProtocolIncus) GetInstanceNVRAMGUID(name string, guid string) (map[string]*api.InstanceNVRAMVariable, error) {
if !r.HasExtension("instance_nvram") {
return nil, errors.New(`The server is missing the required "instance_nvram" API extension`)
}
vars := map[string]*api.InstanceNVRAMVariable{}
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, err
}
// Fetch the raw value.
_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/nvram/%s?recursion=1", path, url.PathEscape(name), url.PathEscape(guid)), nil, "", &vars)
if err != nil {
return nil, err
}
return vars, err
}
// GetRawInstanceNVRAMGUIDVar gets raw OVMF variables from an instance.
func (r *ProtocolIncus) GetRawInstanceNVRAMGUIDVar(name string, guid string, varName string) ([]byte, uint32, error) {
if !r.HasExtension("instance_nvram") {
return nil, 0, errors.New(`The server is missing the required "instance_nvram" API extension`)
}
resp, err := r.getInstanceNVRAM(name, guid, varName, "application/octet-stream")
if err != nil {
return nil, 0, err
}
attributes, err := strconv.ParseUint(resp.Header.Get("X-Incus-attributes"), 10, 32)
if err != nil {
return nil, 0, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, err
}
return data, uint32(attributes), err
}
// GetInstanceNVRAMGUIDVar gets interpreted OVMF variables from an instance.
func (r *ProtocolIncus) GetInstanceNVRAMGUIDVar(name string, guid string, varName string) (*api.InstanceNVRAMVariable, string, error) {
if !r.HasExtension("instance_nvram") {
return nil, "", errors.New(`The server is missing the required "instance_nvram" API extension`)
}
var v *api.InstanceNVRAMVariable
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, "", err
}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/nvram/%s/%s?recursion=1", path, url.PathEscape(name), url.PathEscape(guid), url.PathEscape(varName)), nil, "", &v)
if err != nil {
return nil, "", err
}
return v, etag, err
}
// DeleteInstanceNVRAMGUIDVar sets interpreted OVMF variables on an instance.
func (r *ProtocolIncus) DeleteInstanceNVRAMGUIDVar(name string, guid string, varName string) error {
if !r.HasExtension("instance_nvram") {
return errors.New(`The server is missing the required "instance_nvram" API extension`)
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/nvram/%s/%s", path, url.PathEscape(name), url.PathEscape(guid), url.PathEscape(varName)), nil, "")
if err != nil {
return err
}
return nil
}
// UpdateRawInstanceNVRAMGUIDVar sets raw OVMF variables on an instance.
func (r *ProtocolIncus) UpdateRawInstanceNVRAMGUIDVar(name string, guid string, varName string, data []byte, attributes uint32, timestamp int64) error {
if !r.HasExtension("instance_nvram") {
return errors.New(`The server is missing the required "instance_nvram" API extension`)
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return err
}
// Prepare the HTTP request
requestURL := fmt.Sprintf("%s/1.0%s/%s/nvram/%s/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(guid), url.PathEscape(varName))
requestURL, err = r.setQueryAttributes(requestURL)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", requestURL, bytes.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Incus-attributes", strconv.FormatUint(uint64(attributes), 10))
if timestamp != 0 {
req.Header.Set("X-Incus-timestamp", strconv.FormatInt(timestamp, 10))
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return err
}
// Handle errors
_, _, err = incusParseResponse(resp)
if err != nil {
return err
}
return nil
}
// UpdateInstanceNVRAMGUIDVar sets interpreted OVMF variables on an instance.
func (r *ProtocolIncus) UpdateInstanceNVRAMGUIDVar(name string, guid string, varName string, data api.InstanceNVRAMVariablePut, ETag string) error {
if !r.HasExtension("instance_nvram") {
return errors.New(`The server is missing the required "instance_nvram" API extension`)
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("PUT", fmt.Sprintf("%s/%s/nvram/%s/%s", path, url.PathEscape(name), url.PathEscape(guid), url.PathEscape(varName)), data, ETag)
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_metadata.go 0000664 0000000 0000000 00000001146 15232704312 0017431 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"github.com/lxc/incus/v7/shared/api"
)
// GetMetadataConfiguration returns a configuration metadata struct.
func (r *ProtocolIncus) GetMetadataConfiguration() (*api.MetadataConfiguration, error) {
metadataConfiguration := api.MetadataConfiguration{}
if !r.HasExtension("metadata_configuration") {
return nil, errors.New("The server is missing the required \"metadata_configuration\" API extension")
}
_, err := r.queryStruct("GET", "/metadata/configuration", nil, "", &metadataConfiguration)
if err != nil {
return nil, err
}
return &metadataConfiguration, nil
}
incus-7.3.0/client/incus_network_acls.go 0000664 0000000 0000000 00000011200 15232704312 0020334 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkACLNames returns a list of network ACL names.
func (r *ProtocolIncus) GetNetworkACLNames() ([]string, error) {
if !r.HasExtension("network_acl") {
return nil, errors.New(`The server is missing the required "network_acl" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/network-acls"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkACLs returns a list of Network ACL structs.
func (r *ProtocolIncus) GetNetworkACLs() ([]api.NetworkACL, error) {
if !r.HasExtension("network_acl") {
return nil, errors.New(`The server is missing the required "network_acl" API extension`)
}
acls := []api.NetworkACL{}
// Fetch the raw value.
_, err := r.queryStruct("GET", "/network-acls?recursion=1", nil, "", &acls)
if err != nil {
return nil, err
}
return acls, nil
}
// GetNetworkACLsAllProjects returns all list of Network ACL structs across all projects.
func (r *ProtocolIncus) GetNetworkACLsAllProjects() ([]api.NetworkACL, error) {
if !r.HasExtension("network_acls_all_projects") {
return nil, errors.New(`The server is missing the required "network_acls_all_projects" API extension`)
}
acls := []api.NetworkACL{}
_, err := r.queryStruct("GET", "/network-acls?recursion=1&all-projects=true", nil, "", &acls)
if err != nil {
return nil, err
}
return acls, nil
}
// GetNetworkACL returns a Network ACL entry for the provided name.
func (r *ProtocolIncus) GetNetworkACL(name string) (*api.NetworkACL, string, error) {
if !r.HasExtension("network_acl") {
return nil, "", errors.New(`The server is missing the required "network_acl" API extension`)
}
acl := api.NetworkACL{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/network-acls/%s", url.PathEscape(name)), nil, "", &acl)
if err != nil {
return nil, "", err
}
return &acl, etag, nil
}
// GetNetworkACLLogfile returns a reader for the ACL log file.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetNetworkACLLogfile(name string) (io.ReadCloser, error) {
if !r.HasExtension("network_acl_log") {
return nil, errors.New(`The server is missing the required "network_acl_log" API extension`)
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0/network-acls/%s/log", r.httpBaseURL.String(), url.PathEscape(name))
uri, err := r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// CreateNetworkACL defines a new network ACL using the provided struct.
func (r *ProtocolIncus) CreateNetworkACL(acl api.NetworkACLsPost) error {
if !r.HasExtension("network_acl") {
return errors.New(`The server is missing the required "network_acl" API extension`)
}
// Send the request.
_, _, err := r.query("POST", "/network-acls", acl, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkACL updates the network ACL to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkACL(name string, acl api.NetworkACLPut, ETag string) error {
if !r.HasExtension("network_acl") {
return errors.New(`The server is missing the required "network_acl" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/network-acls/%s", url.PathEscape(name)), acl, ETag)
if err != nil {
return err
}
return nil
}
// RenameNetworkACL renames an existing network ACL entry.
func (r *ProtocolIncus) RenameNetworkACL(name string, acl api.NetworkACLPost) error {
if !r.HasExtension("network_acl") {
return errors.New(`The server is missing the required "network_acl" API extension`)
}
// Send the request.
_, _, err := r.query("POST", fmt.Sprintf("/network-acls/%s", url.PathEscape(name)), acl, "")
if err != nil {
return err
}
return nil
}
// DeleteNetworkACL deletes an existing network ACL.
func (r *ProtocolIncus) DeleteNetworkACL(name string) error {
if !r.HasExtension("network_acl") {
return errors.New(`The server is missing the required "network_acl" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/network-acls/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_network_address_sets.go 0000664 0000000 0000000 00000010201 15232704312 0022075 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkAddressSetNames returns a list of network address set names.
func (r *ProtocolIncus) GetNetworkAddressSetNames() ([]string, error) {
if !r.HasExtension("network_address_set") {
return nil, errors.New(`The server is missing the required "network_address_set" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/network-address-sets"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkAddressSets returns a list of network address set structs.
func (r *ProtocolIncus) GetNetworkAddressSets() ([]api.NetworkAddressSet, error) {
if !r.HasExtension("network_address_set") {
return nil, errors.New(`The server is missing the required "network_address_set" API extension`)
}
addressSets := []api.NetworkAddressSet{}
// Fetch the raw value.
_, err := r.queryStruct("GET", "/network-address-sets?recursion=1", nil, "", &addressSets)
if err != nil {
return nil, err
}
return addressSets, nil
}
// GetNetworkAddressSetsAllProjects returns a list of network address set structs across all projects.
func (r *ProtocolIncus) GetNetworkAddressSetsAllProjects() ([]api.NetworkAddressSet, error) {
if !r.HasExtension("network_address_set") {
return nil, errors.New(`The server is missing the required "network_address_set" API extension`)
}
addressSets := []api.NetworkAddressSet{}
_, err := r.queryStruct("GET", "/network-address-sets?recursion=1&all-projects=true", nil, "", &addressSets)
if err != nil {
return nil, err
}
return addressSets, nil
}
// GetNetworkAddressSet returns a network address set entry for the provided name.
func (r *ProtocolIncus) GetNetworkAddressSet(name string) (*api.NetworkAddressSet, string, error) {
if !r.HasExtension("network_address_set") {
return nil, "", errors.New(`The server is missing the required "network_address_set" API extension`)
}
addrSet := api.NetworkAddressSet{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/network-address-sets/%s", url.PathEscape(name)), nil, "", &addrSet)
if err != nil {
return nil, "", err
}
return &addrSet, etag, nil
}
// CreateNetworkAddressSet defines a new network address set using the provided struct.
func (r *ProtocolIncus) CreateNetworkAddressSet(as api.NetworkAddressSetsPost) error {
if !r.HasExtension("network_address_set") {
return errors.New(`The server is missing the required "network_address_set" API extension`)
}
// Send the request.
_, _, err := r.query("POST", "/network-address-sets", as, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkAddressSet updates the network address set to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkAddressSet(name string, as api.NetworkAddressSetPut, ETag string) error {
if !r.HasExtension("network_address_set") {
return errors.New(`The server is missing the required "network_address_set" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/network-address-sets/%s", url.PathEscape(name)), as, ETag)
if err != nil {
return err
}
return nil
}
// RenameNetworkAddressSet renames an existing network address set entry.
func (r *ProtocolIncus) RenameNetworkAddressSet(name string, as api.NetworkAddressSetPost) error {
if !r.HasExtension("network_address_set") {
return errors.New(`The server is missing the required "network_address_set" API extension`)
}
// Send the request.
_, _, err := r.query("POST", fmt.Sprintf("/network-address-sets/%s", url.PathEscape(name)), as, "")
if err != nil {
return err
}
return nil
}
// DeleteNetworkAddressSet deletes an existing network address set.
func (r *ProtocolIncus) DeleteNetworkAddressSet(name string) error {
if !r.HasExtension("network_address_set") {
return errors.New(`The server is missing the required "network_address_set" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/network-address-sets/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_network_allocations.go 0000664 0000000 0000000 00000002045 15232704312 0021731 0 ustar 00root root 0000000 0000000 package incus
import (
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkAllocations returns a list of Network allocations for a specific project.
func (r *ProtocolIncus) GetNetworkAllocations() ([]api.NetworkAllocations, error) {
err := r.CheckExtension("network_allocations")
if err != nil {
return nil, err
}
// Fetch the raw value.
netAllocations := []api.NetworkAllocations{}
_, err = r.queryStruct("GET", "/network-allocations", nil, "", &netAllocations)
if err != nil {
return nil, err
}
return netAllocations, nil
}
// GetNetworkAllocationsAllProjects returns a list of Network allocations across all projects.
func (r *ProtocolIncus) GetNetworkAllocationsAllProjects() ([]api.NetworkAllocations, error) {
err := r.CheckExtension("network_allocations")
if err != nil {
return nil, err
}
// Fetch the raw value.
netAllocations := []api.NetworkAllocations{}
_, err = r.queryStruct("GET", "/network-allocations?all-projects=true", nil, "", &netAllocations)
if err != nil {
return nil, err
}
return netAllocations, nil
}
incus-7.3.0/client/incus_network_forwards.go 0000664 0000000 0000000 00000006632 15232704312 0021256 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkForwardAddresses returns a list of network forward listen addresses.
func (r *ProtocolIncus) GetNetworkForwardAddresses(networkName string) ([]string, error) {
if !r.HasExtension("network_forward") {
return nil, errors.New(`The server is missing the required "network_forward" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/networks/%s/forwards", url.PathEscape(networkName))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkForwards returns a list of Network forward structs.
func (r *ProtocolIncus) GetNetworkForwards(networkName string) ([]api.NetworkForward, error) {
if !r.HasExtension("network_forward") {
return nil, errors.New(`The server is missing the required "network_forward" API extension`)
}
forwards := []api.NetworkForward{}
// Fetch the raw value.
_, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/forwards?recursion=1", url.PathEscape(networkName)), nil, "", &forwards)
if err != nil {
return nil, err
}
return forwards, nil
}
// GetNetworkForward returns a Network forward entry for the provided network and listen address.
func (r *ProtocolIncus) GetNetworkForward(networkName string, listenAddress string) (*api.NetworkForward, string, error) {
if !r.HasExtension("network_forward") {
return nil, "", errors.New(`The server is missing the required "network_forward" API extension`)
}
forward := api.NetworkForward{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/forwards/%s", url.PathEscape(networkName), url.PathEscape(listenAddress)), nil, "", &forward)
if err != nil {
return nil, "", err
}
return &forward, etag, nil
}
// CreateNetworkForward defines a new network forward using the provided struct.
func (r *ProtocolIncus) CreateNetworkForward(networkName string, forward api.NetworkForwardsPost) error {
if !r.HasExtension("network_forward") {
return errors.New(`The server is missing the required "network_forward" API extension`)
}
// Send the request.
_, _, err := r.query("POST", fmt.Sprintf("/networks/%s/forwards", url.PathEscape(networkName)), forward, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkForward updates the network forward to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkForward(networkName string, listenAddress string, forward api.NetworkForwardPut, ETag string) error {
if !r.HasExtension("network_forward") {
return errors.New(`The server is missing the required "network_forward" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/networks/%s/forwards/%s", url.PathEscape(networkName), url.PathEscape(listenAddress)), forward, ETag)
if err != nil {
return err
}
return nil
}
// DeleteNetworkForward deletes an existing network forward.
func (r *ProtocolIncus) DeleteNetworkForward(networkName string, listenAddress string) error {
if !r.HasExtension("network_forward") {
return errors.New(`The server is missing the required "network_forward" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/networks/%s/forwards/%s", url.PathEscape(networkName), url.PathEscape(listenAddress)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_network_integrations.go 0000664 0000000 0000000 00000007432 15232704312 0022134 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkIntegrationNames returns a list of network integration names.
func (r *ProtocolIncus) GetNetworkIntegrationNames() ([]string, error) {
if !r.HasExtension("network_integrations") {
return nil, errors.New(`The server is missing the required "network_integrations" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/network-integrations"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkIntegrations returns a list of network integration structs.
func (r *ProtocolIncus) GetNetworkIntegrations() ([]api.NetworkIntegration, error) {
if !r.HasExtension("network_integrations") {
return nil, errors.New(`The server is missing the required "network_integrations" API extension`)
}
integrations := []api.NetworkIntegration{}
// Fetch the raw value.
_, err := r.queryStruct("GET", "/network-integrations?recursion=1", nil, "", &integrations)
if err != nil {
return nil, err
}
return integrations, nil
}
// GetNetworkIntegration returns a network integration entry.
func (r *ProtocolIncus) GetNetworkIntegration(name string) (*api.NetworkIntegration, string, error) {
if !r.HasExtension("network_integrations") {
return nil, "", errors.New(`The server is missing the required "network_integrations" API extension`)
}
integration := api.NetworkIntegration{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/network-integrations/%s", url.PathEscape(name)), nil, "", &integration)
if err != nil {
return nil, "", err
}
return &integration, etag, nil
}
// CreateNetworkIntegration defines a new network integration using the provided struct.
// Returns true if the integration connection has been mutually created. Returns false if integrationing has been only initiated.
func (r *ProtocolIncus) CreateNetworkIntegration(integration api.NetworkIntegrationsPost) error {
if !r.HasExtension("network_integrations") {
return errors.New(`The server is missing the required "network_integrations" API extension`)
}
// Send the request.
_, _, err := r.query("POST", "/network-integrations", integration, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkIntegration updates the network integration to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkIntegration(name string, integration api.NetworkIntegrationPut, ETag string) error {
if !r.HasExtension("network_integrations") {
return errors.New(`The server is missing the required "network_integrations" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/network-integrations/%s", url.PathEscape(name)), integration, ETag)
if err != nil {
return err
}
return nil
}
// RenameNetworkIntegration renames an existing network integration entry.
func (r *ProtocolIncus) RenameNetworkIntegration(name string, network api.NetworkIntegrationPost) error {
if !r.HasExtension("network_integrations") {
return errors.New("The server is missing the required \"network_integrations\" API extension")
}
// Send the request
_, _, err := r.query("POST", fmt.Sprintf("/network-integrations/%s", url.PathEscape(name)), network, "")
if err != nil {
return err
}
return nil
}
// DeleteNetworkIntegration deletes an existing network integration.
func (r *ProtocolIncus) DeleteNetworkIntegration(name string) error {
if !r.HasExtension("network_integrations") {
return errors.New(`The server is missing the required "network_integrations" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/network-integrations/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_network_load_balancers.go 0000664 0000000 0000000 00000007572 15232704312 0022364 0 ustar 00root root 0000000 0000000 package incus
import (
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkLoadBalancerAddresses returns a list of network load balancer listen addresses.
func (r *ProtocolIncus) GetNetworkLoadBalancerAddresses(networkName string) ([]string, error) {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
u := api.NewURL().Path("networks", networkName, "load-balancers")
_, err = r.queryStruct("GET", u.String(), nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(u.String(), urls...)
}
// GetNetworkLoadBalancers returns a list of Network load balancer structs.
func (r *ProtocolIncus) GetNetworkLoadBalancers(networkName string) ([]api.NetworkLoadBalancer, error) {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return nil, err
}
loadBalancers := []api.NetworkLoadBalancer{}
// Fetch the raw value.
u := api.NewURL().Path("networks", networkName, "load-balancers").WithQuery("recursion", "1")
_, err = r.queryStruct("GET", u.String(), nil, "", &loadBalancers)
if err != nil {
return nil, err
}
return loadBalancers, nil
}
// GetNetworkLoadBalancer returns a Network load balancer entry for the provided network and listen address.
func (r *ProtocolIncus) GetNetworkLoadBalancer(networkName string, listenAddress string) (*api.NetworkLoadBalancer, string, error) {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return nil, "", err
}
loadBalancer := api.NetworkLoadBalancer{}
// Fetch the raw value.
u := api.NewURL().Path("networks", networkName, "load-balancers", listenAddress)
etag, err := r.queryStruct("GET", u.String(), nil, "", &loadBalancer)
if err != nil {
return nil, "", err
}
return &loadBalancer, etag, nil
}
// CreateNetworkLoadBalancer defines a new network load balancer using the provided struct.
func (r *ProtocolIncus) CreateNetworkLoadBalancer(networkName string, loadBalancer api.NetworkLoadBalancersPost) error {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("networks", networkName, "load-balancers")
_, _, err = r.query("POST", u.String(), loadBalancer, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkLoadBalancer updates the network load balancer to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkLoadBalancer(networkName string, listenAddress string, loadBalancer api.NetworkLoadBalancerPut, ETag string) error {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("networks", networkName, "load-balancers", listenAddress)
_, _, err = r.query("PUT", u.String(), loadBalancer, ETag)
if err != nil {
return err
}
return nil
}
// DeleteNetworkLoadBalancer deletes an existing network load balancer.
func (r *ProtocolIncus) DeleteNetworkLoadBalancer(networkName string, listenAddress string) error {
err := r.CheckExtension("network_load_balancer")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("networks", networkName, "load-balancers", listenAddress)
_, _, err = r.query("DELETE", u.String(), nil, "")
if err != nil {
return err
}
return nil
}
// GetNetworkLoadBalancerState returns a Network load balancer state for the provided network and listen address.
func (r *ProtocolIncus) GetNetworkLoadBalancerState(networkName string, listenAddress string) (*api.NetworkLoadBalancerState, error) {
err := r.CheckExtension("network_load_balancer_state")
if err != nil {
return nil, err
}
lbState := api.NetworkLoadBalancerState{}
// Fetch the raw value.
u := api.NewURL().Path("networks", networkName, "load-balancers", listenAddress, "state")
_, err = r.queryStruct("GET", u.String(), nil, "", &lbState)
if err != nil {
return nil, err
}
return &lbState, nil
}
incus-7.3.0/client/incus_network_peers.go 0000664 0000000 0000000 00000006770 15232704312 0020550 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkPeerNames returns a list of network peer names.
func (r *ProtocolIncus) GetNetworkPeerNames(networkName string) ([]string, error) {
if !r.HasExtension("network_peer") {
return nil, errors.New(`The server is missing the required "network_peer" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/networks/%s/peers", url.PathEscape(networkName))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkPeers returns a list of network peer structs.
func (r *ProtocolIncus) GetNetworkPeers(networkName string) ([]api.NetworkPeer, error) {
if !r.HasExtension("network_peer") {
return nil, errors.New(`The server is missing the required "network_peer" API extension`)
}
peers := []api.NetworkPeer{}
// Fetch the raw value.
_, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/peers?recursion=1", url.PathEscape(networkName)), nil, "", &peers)
if err != nil {
return nil, err
}
return peers, nil
}
// GetNetworkPeer returns a network peer entry for the provided network and peer name.
func (r *ProtocolIncus) GetNetworkPeer(networkName string, peerName string) (*api.NetworkPeer, string, error) {
if !r.HasExtension("network_peer") {
return nil, "", errors.New(`The server is missing the required "network_peer" API extension`)
}
peer := api.NetworkPeer{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/peers/%s", url.PathEscape(networkName), url.PathEscape(peerName)), nil, "", &peer)
if err != nil {
return nil, "", err
}
return &peer, etag, nil
}
// CreateNetworkPeer defines a new network peer using the provided struct.
// Returns true if the peer connection has been mutually created. Returns false if peering has been only initiated.
func (r *ProtocolIncus) CreateNetworkPeer(networkName string, peer api.NetworkPeersPost) error {
if !r.HasExtension("network_peer") {
return errors.New(`The server is missing the required "network_peer" API extension`)
}
if peer.Type != "" && peer.Type != "local" && !r.HasExtension("network_integrations") {
return errors.New(`The server is missing the required "network_integrations" API extension`)
}
// Send the request.
_, _, err := r.query("POST", fmt.Sprintf("/networks/%s/peers", url.PathEscape(networkName)), peer, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkPeer updates the network peer to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkPeer(networkName string, peerName string, peer api.NetworkPeerPut, ETag string) error {
if !r.HasExtension("network_peer") {
return errors.New(`The server is missing the required "network_peer" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/networks/%s/peers/%s", url.PathEscape(networkName), url.PathEscape(peerName)), peer, ETag)
if err != nil {
return err
}
return nil
}
// DeleteNetworkPeer deletes an existing network peer.
func (r *ProtocolIncus) DeleteNetworkPeer(networkName string, peerName string) error {
if !r.HasExtension("network_peer") {
return errors.New(`The server is missing the required "network_peer" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/networks/%s/peers/%s", url.PathEscape(networkName), url.PathEscape(peerName)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_network_zones.go 0000664 0000000 0000000 00000015176 15232704312 0020570 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkZoneNames returns a list of network zone names.
func (r *ProtocolIncus) GetNetworkZoneNames() ([]string, error) {
if !r.HasExtension("network_dns") {
return nil, errors.New(`The server is missing the required "network_dns" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/network-zones"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkZones returns a list of Network zone structs.
func (r *ProtocolIncus) GetNetworkZones() ([]api.NetworkZone, error) {
if !r.HasExtension("network_dns") {
return nil, errors.New(`The server is missing the required "network_dns" API extension`)
}
zones := []api.NetworkZone{}
// Fetch the raw value.
_, err := r.queryStruct("GET", "/network-zones?recursion=1", nil, "", &zones)
if err != nil {
return nil, err
}
return zones, nil
}
// GetNetworkZonesAllProjects returns a list of network zones across all projects as NetworkZone structs.
func (r *ProtocolIncus) GetNetworkZonesAllProjects() ([]api.NetworkZone, error) {
err := r.CheckExtension("network_zones_all_projects")
if err != nil {
return nil, errors.New(`The server is missing the required "network_zones_all_projects" API extension`)
}
zones := []api.NetworkZone{}
_, err = r.queryStruct("GET", "/network-zones?recursion=1&all-projects=true", nil, "", &zones)
if err != nil {
return nil, err
}
return zones, nil
}
// GetNetworkZone returns a Network zone entry for the provided name.
func (r *ProtocolIncus) GetNetworkZone(name string) (*api.NetworkZone, string, error) {
if !r.HasExtension("network_dns") {
return nil, "", errors.New(`The server is missing the required "network_dns" API extension`)
}
zone := api.NetworkZone{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/network-zones/%s", url.PathEscape(name)), nil, "", &zone)
if err != nil {
return nil, "", err
}
return &zone, etag, nil
}
// CreateNetworkZone defines a new Network zone using the provided struct.
func (r *ProtocolIncus) CreateNetworkZone(zone api.NetworkZonesPost) error {
if !r.HasExtension("network_dns") {
return errors.New(`The server is missing the required "network_dns" API extension`)
}
// Send the request.
_, _, err := r.query("POST", "/network-zones", zone, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkZone updates the network zone to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkZone(name string, zone api.NetworkZonePut, ETag string) error {
if !r.HasExtension("network_dns") {
return errors.New(`The server is missing the required "network_dns" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/network-zones/%s", url.PathEscape(name)), zone, ETag)
if err != nil {
return err
}
return nil
}
// DeleteNetworkZone deletes an existing network zone.
func (r *ProtocolIncus) DeleteNetworkZone(name string) error {
if !r.HasExtension("network_dns") {
return errors.New(`The server is missing the required "network_dns" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/network-zones/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
// GetNetworkZoneRecordNames returns a list of network zone record names.
func (r *ProtocolIncus) GetNetworkZoneRecordNames(zone string) ([]string, error) {
if !r.HasExtension("network_dns_records") {
return nil, errors.New(`The server is missing the required "network_dns_records" API extension`)
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/network-zones/%s/records", url.PathEscape(zone))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworkZoneRecords returns a list of Network zone record structs.
func (r *ProtocolIncus) GetNetworkZoneRecords(zone string) ([]api.NetworkZoneRecord, error) {
if !r.HasExtension("network_dns_records") {
return nil, errors.New(`The server is missing the required "network_dns_records" API extension`)
}
records := []api.NetworkZoneRecord{}
// Fetch the raw value.
_, err := r.queryStruct("GET", fmt.Sprintf("/network-zones/%s/records?recursion=1", url.PathEscape(zone)), nil, "", &records)
if err != nil {
return nil, err
}
return records, nil
}
// GetNetworkZoneRecord returns a Network zone record entry for the provided zone and name.
func (r *ProtocolIncus) GetNetworkZoneRecord(zone string, name string) (*api.NetworkZoneRecord, string, error) {
if !r.HasExtension("network_dns_records") {
return nil, "", errors.New(`The server is missing the required "network_dns_records" API extension`)
}
record := api.NetworkZoneRecord{}
// Fetch the raw value.
etag, err := r.queryStruct("GET", fmt.Sprintf("/network-zones/%s/records/%s", url.PathEscape(zone), url.PathEscape(name)), nil, "", &record)
if err != nil {
return nil, "", err
}
return &record, etag, nil
}
// CreateNetworkZoneRecord defines a new Network zone record using the provided struct.
func (r *ProtocolIncus) CreateNetworkZoneRecord(zone string, record api.NetworkZoneRecordsPost) error {
if !r.HasExtension("network_dns_records") {
return errors.New(`The server is missing the required "network_dns_records" API extension`)
}
// Send the request.
_, _, err := r.query("POST", fmt.Sprintf("/network-zones/%s/records", url.PathEscape(zone)), record, "")
if err != nil {
return err
}
return nil
}
// UpdateNetworkZoneRecord updates the network zone record to match the provided struct.
func (r *ProtocolIncus) UpdateNetworkZoneRecord(zone string, name string, record api.NetworkZoneRecordPut, ETag string) error {
if !r.HasExtension("network_dns_records") {
return errors.New(`The server is missing the required "network_dns_records" API extension`)
}
// Send the request.
_, _, err := r.query("PUT", fmt.Sprintf("/network-zones/%s/records/%s", url.PathEscape(zone), url.PathEscape(name)), record, ETag)
if err != nil {
return err
}
return nil
}
// DeleteNetworkZoneRecord deletes an existing network zone record.
func (r *ProtocolIncus) DeleteNetworkZoneRecord(zone string, name string) error {
if !r.HasExtension("network_dns_records") {
return errors.New(`The server is missing the required "network_dns_records" API extension`)
}
// Send the request.
_, _, err := r.query("DELETE", fmt.Sprintf("/network-zones/%s/records/%s", url.PathEscape(zone), url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_networks.go 0000664 0000000 0000000 00000013374 15232704312 0017533 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// GetNetworkNames returns a list of network names.
func (r *ProtocolIncus) GetNetworkNames() ([]string, error) {
if !r.HasExtension("network") {
return nil, errors.New("The server is missing the required \"network\" API extension")
}
// Fetch the raw values.
urls := []string{}
baseURL := "/networks"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetNetworks returns a list of Network struct.
func (r *ProtocolIncus) GetNetworks() ([]api.Network, error) {
if !r.HasExtension("network") {
return nil, errors.New("The server is missing the required \"network\" API extension")
}
networks := []api.Network{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/networks?recursion=1", nil, "", &networks)
if err != nil {
return nil, err
}
return networks, nil
}
// GetNetworksWithFilter returns a list of filtered Network struct.
func (r *ProtocolIncus) GetNetworksWithFilter(filters []string) ([]api.Network, error) {
if !r.HasExtension("network") {
return nil, errors.New("The server is missing the required \"network\" API extension")
}
networks := []api.Network{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/networks?%s", v.Encode()), nil, "", &networks)
if err != nil {
return nil, err
}
return networks, nil
}
// GetNetworksAllProjects gets all networks across all projects.
func (r *ProtocolIncus) GetNetworksAllProjects() ([]api.Network, error) {
if !r.HasExtension("networks_all_projects") {
return nil, errors.New(`The server is missing the required "networks_all_projects" API extension`)
}
networks := []api.Network{}
_, err := r.queryStruct("GET", "/networks?recursion=1&all-projects=true", nil, "", &networks)
if err != nil {
return nil, err
}
return networks, nil
}
// GetNetworksAllProjectsWithFilter gets a filtered list of all networks across all projects.
func (r *ProtocolIncus) GetNetworksAllProjectsWithFilter(filters []string) ([]api.Network, error) {
if !r.HasExtension("networks_all_projects") {
return nil, errors.New(`The server is missing the required "networks_all_projects" API extension`)
}
networks := []api.Network{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/networks?%s", v.Encode()), nil, "", &networks)
if err != nil {
return nil, err
}
return networks, nil
}
// GetNetwork returns a Network entry for the provided name.
func (r *ProtocolIncus) GetNetwork(name string) (*api.Network, string, error) {
if !r.HasExtension("network") {
return nil, "", errors.New("The server is missing the required \"network\" API extension")
}
network := api.Network{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s", url.PathEscape(name)), nil, "", &network)
if err != nil {
return nil, "", err
}
return &network, etag, nil
}
// GetNetworkLeases returns a list of Network struct.
func (r *ProtocolIncus) GetNetworkLeases(name string) ([]api.NetworkLease, error) {
if !r.HasExtension("network_leases") {
return nil, errors.New("The server is missing the required \"network_leases\" API extension")
}
leases := []api.NetworkLease{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/leases", url.PathEscape(name)), nil, "", &leases)
if err != nil {
return nil, err
}
return leases, nil
}
// GetNetworkState returns metrics and information on the running network.
func (r *ProtocolIncus) GetNetworkState(name string) (*api.NetworkState, error) {
if !r.HasExtension("network_state") {
return nil, errors.New("The server is missing the required \"network_state\" API extension")
}
state := api.NetworkState{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s/state", url.PathEscape(name)), nil, "", &state)
if err != nil {
return nil, err
}
return &state, nil
}
// CreateNetwork defines a new network using the provided Network struct.
func (r *ProtocolIncus) CreateNetwork(network api.NetworksPost) error {
if !r.HasExtension("network") {
return errors.New("The server is missing the required \"network\" API extension")
}
// Send the request
_, _, err := r.query("POST", "/networks", network, "")
if err != nil {
return err
}
return nil
}
// UpdateNetwork updates the network to match the provided Network struct.
func (r *ProtocolIncus) UpdateNetwork(name string, network api.NetworkPut, ETag string) error {
if !r.HasExtension("network") {
return errors.New("The server is missing the required \"network\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/networks/%s", url.PathEscape(name)), network, ETag)
if err != nil {
return err
}
return nil
}
// RenameNetwork renames an existing network entry.
func (r *ProtocolIncus) RenameNetwork(name string, network api.NetworkPost) error {
if !r.HasExtension("network") {
return errors.New("The server is missing the required \"network\" API extension")
}
// Send the request
_, _, err := r.query("POST", fmt.Sprintf("/networks/%s", url.PathEscape(name)), network, "")
if err != nil {
return err
}
return nil
}
// DeleteNetwork deletes an existing network.
func (r *ProtocolIncus) DeleteNetwork(name string) error {
if !r.HasExtension("network") {
return errors.New("The server is missing the required \"network\" API extension")
}
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/networks/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_oidc.go 0000664 0000000 0000000 00000023750 15232704312 0016574 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/gorilla/websocket"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2"
"github.com/lxc/incus/v7/shared/util"
)
// ErrOIDCExpired is returned when the token is expired and we can't retry the request ourselves.
var ErrOIDCExpired = errors.New("OIDC token expired, please re-try the request")
// setupOIDCClient initializes the OIDC (OpenID Connect) client with given tokens if it hasn't been set up already.
// It also assigns the protocol's http client to the oidcClient's httpClient.
func (r *ProtocolIncus) setupOIDCClient(token *oidc.Tokens[*oidc.IDTokenClaims], skipAuthenticate bool) {
if r.oidcClient != nil {
return
}
r.oidcClient = newOIDCClient(token)
r.oidcClient.skipAuthenticate = skipAuthenticate
r.oidcClient.httpClient = r.http
}
// GetOIDCTokens returns the current OIDC tokens (if any) from the OIDC client.
//
// This should only be used by internal Incus tools when it's not possible to get the tokens from a Config struct.
func (r *ProtocolIncus) GetOIDCTokens() *oidc.Tokens[*oidc.IDTokenClaims] {
if r.oidcClient == nil {
return nil
}
return r.oidcClient.tokens
}
// oidcTransport is a custom HTTP transport that injects the audience field into requests directed at the device authorization endpoint.
type oidcTransport struct {
deviceAuthorizationEndpoint string
audience string
}
// RoundTrip is a method of oidcTransport that modifies the request, adds the audience parameter if appropriate, and sends it along.
func (o *oidcTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// Don't modify the request if it's not to the device authorization endpoint, or there are no
// URL parameters which need to be set.
if r.URL.String() != o.deviceAuthorizationEndpoint || len(o.audience) == 0 {
return http.DefaultTransport.RoundTrip(r)
}
err := r.ParseForm()
if err != nil {
return nil, err
}
if o.audience != "" {
r.Form.Add("audience", o.audience)
}
// Update the body with the new URL parameters.
body := r.Form.Encode()
r.Body = io.NopCloser(strings.NewReader(body))
r.ContentLength = int64(len(body))
return http.DefaultTransport.RoundTrip(r)
}
var errRefreshAccessToken = errors.New("Failed refreshing access token")
type oidcClient struct {
httpClient *http.Client
oidcTransport *oidcTransport
tokens *oidc.Tokens[*oidc.IDTokenClaims]
skipAuthenticate bool
}
// oidcClient is a structure encapsulating an HTTP client, OIDC transport, and a token for OpenID Connect (OIDC) operations.
// newOIDCClient constructs a new oidcClient, ensuring the token field is non-nil to prevent panics during authentication.
func newOIDCClient(tokens *oidc.Tokens[*oidc.IDTokenClaims]) *oidcClient {
client := oidcClient{
tokens: tokens,
httpClient: &http.Client{},
oidcTransport: &oidcTransport{},
}
// Ensure client.tokens is never nil otherwise authenticate() will panic.
if client.tokens == nil {
client.tokens = &oidc.Tokens[*oidc.IDTokenClaims]{}
}
return &client
}
// getAccessToken returns the Access Token from the oidcClient's tokens, or an empty string if no tokens are present.
func (o *oidcClient) getAccessToken() string {
if o.tokens == nil || o.tokens.Token == nil {
return ""
}
return o.tokens.AccessToken
}
// do function executes an HTTP request using the oidcClient's http client, and manages authorization by refreshing or authenticating as needed.
// If the request fails with an HTTP Unauthorized status, it attempts to refresh the access token, or perform an OIDC authentication if refresh fails.
func (o *oidcClient) do(req *http.Request) (*http.Response, error) {
resp, err := o.httpClient.Do(req)
if err != nil {
return nil, err
}
// Return immediately if the error is not HTTP status unauthorized.
if resp.StatusCode != http.StatusUnauthorized {
return resp, nil
}
issuer := resp.Header.Get("X-Incus-OIDC-issuer")
clientID := resp.Header.Get("X-Incus-OIDC-clientid")
audience := resp.Header.Get("X-Incus-OIDC-audience")
scopes := resp.Header.Get("X-Incus-OIDC-scopes")
if scopes == "" {
scopes = "openid,offline_access"
}
if issuer == "" || clientID == "" {
return resp, nil
}
// Refresh the token.
err = o.refresh(issuer, clientID, scopes)
if err != nil {
if o.skipAuthenticate {
return nil, fmt.Errorf("Authentication not found or expired: %w", err)
}
err = o.authenticate(issuer, clientID, audience, scopes)
if err != nil {
return nil, err
}
}
// If not dealing with something we can retry, return a clear error.
if req.Method != "GET" && req.GetBody == nil {
return resp, ErrOIDCExpired
}
// Set the new access token in the header.
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", o.tokens.AccessToken))
// Reset the request body.
if req.GetBody != nil {
body, err := req.GetBody()
if err != nil {
return nil, err
}
req.Body = body
}
resp, err = o.httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
// dial function executes a websocket request and handles OIDC authentication and refresh.
func (o *oidcClient) dial(dialer websocket.Dialer, uri string, req *http.Request) (*websocket.Conn, *http.Response, error) {
conn, resp, err := dialer.Dial(uri, req.Header)
if err != nil && resp == nil {
return nil, nil, err
}
// Return immediately if the error is not HTTP status unauthorized.
if conn != nil && resp.StatusCode != http.StatusUnauthorized {
return conn, resp, nil
}
issuer := resp.Header.Get("X-Incus-OIDC-issuer")
clientID := resp.Header.Get("X-Incus-OIDC-clientid")
audience := resp.Header.Get("X-Incus-OIDC-audience")
scopes := resp.Header.Get("X-Incus-OIDC-scopes")
if scopes == "" {
scopes = "openid,offline_access"
}
if issuer == "" || clientID == "" {
return nil, resp, err
}
err = o.refresh(issuer, clientID, scopes)
if err != nil {
if o.skipAuthenticate {
return nil, resp, fmt.Errorf("Authentication not found or expired: %w", err)
}
err = o.authenticate(issuer, clientID, audience, scopes)
if err != nil {
return nil, resp, err
}
}
// Set the new access token in the header.
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", o.tokens.AccessToken))
return dialer.Dial(uri, req.Header)
}
// getProvider initializes a new OpenID Connect Relying Party for a given issuer and clientID.
// The function also creates a secure CookieHandler with random encryption and hash keys, and applies a series of configurations on the Relying Party.
func (o *oidcClient) getProvider(issuer string, clientID string, scopes string) (rp.RelyingParty, error) {
hashKey := make([]byte, 16)
encryptKey := make([]byte, 16)
_, err := rand.Read(hashKey)
if err != nil {
return nil, err
}
_, err = rand.Read(encryptKey)
if err != nil {
return nil, err
}
cookieHandler := httphelper.NewCookieHandler(hashKey, encryptKey, httphelper.WithUnsecure())
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithPKCE(cookieHandler),
rp.WithHTTPClient(o.httpClient),
}
provider, err := rp.NewRelyingPartyOIDC(context.TODO(), issuer, clientID, "", "", strings.Split(scopes, ","), options...)
if err != nil {
return nil, err
}
return provider, nil
}
// refresh attempts to refresh the OpenID Connect access token for the client using the refresh token.
// If no token is present or the refresh token is empty, it returns an error. If successful, it updates the access token and other relevant token fields.
func (o *oidcClient) refresh(issuer string, clientID string, scopes string) error {
if o.tokens.Token == nil || o.tokens.RefreshToken == "" {
return errRefreshAccessToken
}
provider, err := o.getProvider(issuer, clientID, scopes)
if err != nil {
return errRefreshAccessToken
}
oauthTokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](context.TODO(), provider, o.tokens.RefreshToken, "", "")
if err != nil {
return errRefreshAccessToken
}
o.tokens.AccessToken = oauthTokens.AccessToken
o.tokens.TokenType = oauthTokens.TokenType
o.tokens.Expiry = oauthTokens.Expiry
if oauthTokens.RefreshToken != "" {
o.tokens.RefreshToken = oauthTokens.RefreshToken
}
return nil
}
// authenticate initiates the OpenID Connect device flow authentication process for the client.
// It presents a user code for the end user to input in the device that has web access and waits for them to complete the authentication,
// subsequently updating the client's tokens upon successful authentication.
func (o *oidcClient) authenticate(issuer string, clientID string, audience string, scopes string) error {
// Store the old transport and restore it in the end.
oldTransport := o.httpClient.Transport
o.oidcTransport.audience = audience
o.httpClient.Transport = o.oidcTransport
defer func() {
o.httpClient.Transport = oldTransport
}()
provider, err := o.getProvider(issuer, clientID, scopes)
if err != nil {
return err
}
o.oidcTransport.deviceAuthorizationEndpoint = provider.GetDeviceAuthorizationEndpoint()
resp, err := rp.DeviceAuthorization(context.TODO(), strings.Split(scopes, ","), provider, nil)
if err != nil {
return err
}
u, _ := url.Parse(resp.VerificationURIComplete)
fmt.Printf("URL: %s\n", u.String())
fmt.Printf("Code: %s\n\n", resp.UserCode)
_ = util.OpenBrowser(u.String())
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
defer stop()
token, err := rp.DeviceAccessToken(ctx, resp.DeviceCode, time.Duration(resp.Interval)*time.Second, provider)
if err != nil {
return err
}
if o.tokens.Token == nil {
o.tokens.Token = &oauth2.Token{}
}
o.tokens.Expiry = time.Now().Add(time.Duration(token.ExpiresIn))
o.tokens.IDToken = token.IDToken
o.tokens.AccessToken = token.AccessToken
o.tokens.TokenType = token.TokenType
if token.RefreshToken != "" {
o.tokens.RefreshToken = token.RefreshToken
}
return nil
}
incus-7.3.0/client/incus_operations.go 0000664 0000000 0000000 00000007522 15232704312 0020040 0 ustar 00root root 0000000 0000000 package incus
import (
"fmt"
"net/url"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/shared/api"
)
// GetOperationUUIDs returns a list of operation uuids.
func (r *ProtocolIncus) GetOperationUUIDs() ([]string, error) {
// Fetch the raw URL values.
urls := []string{}
baseURL := "/operations"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetOperations returns a list of Operation struct.
func (r *ProtocolIncus) GetOperations() ([]api.Operation, error) {
apiOperations := map[string][]api.Operation{}
// Fetch the raw value.
_, err := r.queryStruct("GET", "/operations?recursion=1", nil, "", &apiOperations)
if err != nil {
return nil, err
}
// Turn it into a list of operations.
operations := []api.Operation{}
for _, v := range apiOperations {
operations = append(operations, v...)
}
return operations, nil
}
// GetOperationsAllProjects returns a list of operations from all projects.
func (r *ProtocolIncus) GetOperationsAllProjects() ([]api.Operation, error) {
err := r.CheckExtension("operations_get_query_all_projects")
if err != nil {
return nil, err
}
apiOperations := map[string][]api.Operation{}
path := "/operations"
v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")
// Fetch the raw value.
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &apiOperations)
if err != nil {
return nil, err
}
// Turn it into a list of operations.
operations := []api.Operation{}
for _, v := range apiOperations {
operations = append(operations, v...)
}
return operations, nil
}
// GetOperation returns an Operation entry for the provided uuid.
func (r *ProtocolIncus) GetOperation(uuid string) (*api.Operation, string, error) {
op := api.Operation{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/operations/%s", url.PathEscape(uuid)), nil, "", &op)
if err != nil {
return nil, "", err
}
return &op, etag, nil
}
// GetOperationWait returns an Operation entry for the provided uuid once it's complete or hits the timeout.
func (r *ProtocolIncus) GetOperationWait(uuid string, timeout int) (*api.Operation, string, error) {
op := api.Operation{}
// Unset the response header timeout so that the request does not time out.
transport, err := r.getUnderlyingHTTPTransport()
if err != nil {
return nil, "", err
}
transport.ResponseHeaderTimeout = 0
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/operations/%s/wait?timeout=%d", url.PathEscape(uuid), timeout), nil, "", &op)
if err != nil {
return nil, "", err
}
return &op, etag, nil
}
// GetOperationWaitSecret returns an Operation entry for the provided uuid and secret once it's complete or hits the timeout.
func (r *ProtocolIncus) GetOperationWaitSecret(uuid string, secret string, timeout int) (*api.Operation, string, error) {
op := api.Operation{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/operations/%s/wait?secret=%s&timeout=%d", url.PathEscape(uuid), url.PathEscape(secret), timeout), nil, "", &op)
if err != nil {
return nil, "", err
}
return &op, etag, nil
}
// GetOperationWebsocket returns a websocket connection for the provided operation.
func (r *ProtocolIncus) GetOperationWebsocket(uuid string, secret string) (*websocket.Conn, error) {
path := fmt.Sprintf("/operations/%s/websocket", url.PathEscape(uuid))
if secret != "" {
path = fmt.Sprintf("%s?secret=%s", path, url.QueryEscape(secret))
}
return r.websocket(path)
}
// DeleteOperation deletes (cancels) a running operation.
func (r *ProtocolIncus) DeleteOperation(uuid string) error {
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/operations/%s", url.PathEscape(uuid)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_profiles.go 0000664 0000000 0000000 00000007572 15232704312 0017505 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// Profile handling functions
// GetProfileNames returns a list of available profile names.
func (r *ProtocolIncus) GetProfileNames() ([]string, error) {
// Fetch the raw URL values.
urls := []string{}
baseURL := "/profiles"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetProfiles returns a list of available Profile structs.
func (r *ProtocolIncus) GetProfiles() ([]api.Profile, error) {
profiles := []api.Profile{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/profiles?recursion=1", nil, "", &profiles)
if err != nil {
return nil, err
}
return profiles, nil
}
// GetProfilesWithFilter returns a filtered list of available Profile structs.
func (r *ProtocolIncus) GetProfilesWithFilter(filters []string) ([]api.Profile, error) {
profiles := []api.Profile{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/profiles?%s", v.Encode()), nil, "", &profiles)
if err != nil {
return nil, err
}
return profiles, nil
}
// GetProfilesAllProjects returns a list of profiles across all projects as Profile structs.
func (r *ProtocolIncus) GetProfilesAllProjects() ([]api.Profile, error) {
err := r.CheckExtension("profiles_all_projects")
if err != nil {
return nil, errors.New(`The server is missing the required "profiles_all_projects" API extension`)
}
profiles := []api.Profile{}
_, err = r.queryStruct("GET", "/profiles?recursion=1&all-projects=true", nil, "", &profiles)
if err != nil {
return nil, err
}
return profiles, nil
}
// GetProfilesAllProjectsWithFilter returns a filtered list of profiles across all projects as Profile structs.
func (r *ProtocolIncus) GetProfilesAllProjectsWithFilter(filters []string) ([]api.Profile, error) {
err := r.CheckExtension("profiles_all_projects")
if err != nil {
return nil, errors.New(`The server is missing the required "profiles_all_projects" API extension`)
}
profiles := []api.Profile{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
_, err = r.queryStruct("GET", fmt.Sprintf("/profiles?%s", v.Encode()), nil, "", &profiles)
if err != nil {
return nil, err
}
return profiles, nil
}
// GetProfile returns a Profile entry for the provided name.
func (r *ProtocolIncus) GetProfile(name string) (*api.Profile, string, error) {
profile := api.Profile{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/profiles/%s", url.PathEscape(name)), nil, "", &profile)
if err != nil {
return nil, "", err
}
return &profile, etag, nil
}
// CreateProfile defines a new instance profile.
func (r *ProtocolIncus) CreateProfile(profile api.ProfilesPost) error {
// Send the request
_, _, err := r.query("POST", "/profiles", profile, "")
if err != nil {
return err
}
return nil
}
// UpdateProfile updates the profile to match the provided Profile struct.
func (r *ProtocolIncus) UpdateProfile(name string, profile api.ProfilePut, ETag string) error {
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/profiles/%s", url.PathEscape(name)), profile, ETag)
if err != nil {
return err
}
return nil
}
// RenameProfile renames an existing profile entry.
func (r *ProtocolIncus) RenameProfile(name string, profile api.ProfilePost) error {
// Send the request
_, _, err := r.query("POST", fmt.Sprintf("/profiles/%s", url.PathEscape(name)), profile, "")
if err != nil {
return err
}
return nil
}
// DeleteProfile deletes a profile.
func (r *ProtocolIncus) DeleteProfile(name string) error {
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/profiles/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_projects.go 0000664 0000000 0000000 00000012233 15232704312 0017501 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// Project handling functions
// GetProjectNames returns a list of available project names.
func (r *ProtocolIncus) GetProjectNames() ([]string, error) {
if !r.HasExtension("projects") {
return nil, errors.New("The server is missing the required \"projects\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/projects"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetProjects returns a list of available Project structs.
func (r *ProtocolIncus) GetProjects() ([]api.Project, error) {
if !r.HasExtension("projects") {
return nil, errors.New("The server is missing the required \"projects\" API extension")
}
projects := []api.Project{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/projects?recursion=1", nil, "", &projects)
if err != nil {
return nil, err
}
return projects, nil
}
// GetProjectsWithFilter returns a filtered list of projects as Project structs.
func (r *ProtocolIncus) GetProjectsWithFilter(filters []string) ([]api.Project, error) {
if !r.HasExtension("projects") {
return nil, errors.New("The server is missing the required \"projects\" API extension")
}
projects := []api.Project{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/projects?%s", v.Encode()), nil, "", &projects)
if err != nil {
return nil, err
}
return projects, nil
}
// GetProject returns a Project entry for the provided name.
func (r *ProtocolIncus) GetProject(name string) (*api.Project, string, error) {
if !r.HasExtension("projects") {
return nil, "", errors.New("The server is missing the required \"projects\" API extension")
}
project := api.Project{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/projects/%s", url.PathEscape(name)), nil, "", &project)
if err != nil {
return nil, "", err
}
return &project, etag, nil
}
// GetProjectState returns a Project state for the provided name.
func (r *ProtocolIncus) GetProjectState(name string) (*api.ProjectState, error) {
if !r.HasExtension("project_usage") {
return nil, errors.New("The server is missing the required \"project_usage\" API extension")
}
projectState := api.ProjectState{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/projects/%s/state", url.PathEscape(name)), nil, "", &projectState)
if err != nil {
return nil, err
}
return &projectState, nil
}
// GetProjectAccess returns an Access entry for the specified project.
func (r *ProtocolIncus) GetProjectAccess(name string) (api.Access, error) {
access := api.Access{}
if !r.HasExtension("project_access") {
return nil, errors.New("The server is missing the required \"project_access\" API extension")
}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/projects/%s/access", url.PathEscape(name)), nil, "", &access)
if err != nil {
return nil, err
}
return access, nil
}
// CreateProject defines a new project.
func (r *ProtocolIncus) CreateProject(project api.ProjectsPost) error {
if !r.HasExtension("projects") {
return errors.New("The server is missing the required \"projects\" API extension")
}
// Send the request
_, _, err := r.query("POST", "/projects", project, "")
if err != nil {
return err
}
return nil
}
// UpdateProject updates the project to match the provided Project struct.
func (r *ProtocolIncus) UpdateProject(name string, project api.ProjectPut, ETag string) error {
if !r.HasExtension("projects") {
return errors.New("The server is missing the required \"projects\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/projects/%s", url.PathEscape(name)), project, ETag)
if err != nil {
return err
}
return nil
}
// RenameProject renames an existing project entry.
func (r *ProtocolIncus) RenameProject(name string, project api.ProjectPost) (Operation, error) {
if !r.HasExtension("projects") {
return nil, errors.New("The server is missing the required \"projects\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/projects/%s", url.PathEscape(name)), project, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteProject deletes a project.
func (r *ProtocolIncus) DeleteProject(name string) error {
if !r.HasExtension("projects") {
return errors.New("The server is missing the required \"projects\" API extension")
}
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/projects/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
// DeleteProjectForce deletes a project and everything inside of it.
func (r *ProtocolIncus) DeleteProjectForce(name string) error {
if !r.HasExtension("projects_force_delete") {
return errors.New("The server is missing the required \"projects_force_delete\" API extension")
}
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/projects/%s?force=1", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_server.go 0000664 0000000 0000000 00000042652 15232704312 0017166 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"io"
"net/http"
"slices"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/util"
)
// Server handling functions
// GetServer returns the server status as a Server struct.
func (r *ProtocolIncus) GetServer() (*api.Server, string, error) {
server := api.Server{}
// Fetch the raw value
etag, err := r.queryStruct("GET", "", nil, "", &server)
if err != nil {
return nil, "", err
}
// Fill in certificate fingerprint if not provided
if server.Environment.CertificateFingerprint == "" && server.Environment.Certificate != "" {
var err error
server.Environment.CertificateFingerprint, err = localtls.CertFingerprintStr(server.Environment.Certificate)
if err != nil {
return nil, "", err
}
}
if !server.Public && len(server.AuthMethods) == 0 {
// TLS is always available for Incus servers
server.AuthMethods = []string{api.AuthenticationMethodTLS}
}
// Add the value to the cache
r.server = &server
return &server, etag, nil
}
// UpdateServer updates the server status to match the provided Server struct.
func (r *ProtocolIncus) UpdateServer(server api.ServerPut, ETag string) error {
// Send the request
_, _, err := r.query("PUT", "", server, ETag)
if err != nil {
return err
}
return nil
}
// HasExtension returns true if the server supports a given API extension.
// Deprecated: Use CheckExtension instead.
func (r *ProtocolIncus) HasExtension(extension string) bool {
// If no cached API information, just assume we're good
// This is needed for those rare cases where we must avoid a GetServer call
if r.server == nil {
return true
}
return slices.Contains(r.server.APIExtensions, extension)
}
// CheckExtension checks if the server has the specified extension.
func (r *ProtocolIncus) CheckExtension(extensionName string) error {
if !r.HasExtension(extensionName) {
return fmt.Errorf("The server is missing the required %q API extension", extensionName)
}
return nil
}
// IsClustered returns true if the server is part of an Incus cluster.
func (r *ProtocolIncus) IsClustered() bool {
return r.server.Environment.ServerClustered
}
// GetServerResources returns the resources available to a given Incus server.
func (r *ProtocolIncus) GetServerResources() (*api.Resources, error) {
if !r.HasExtension("resources") {
return nil, errors.New("The server is missing the required \"resources\" API extension")
}
resources := api.Resources{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/resources", nil, "", &resources)
if err != nil {
return nil, err
}
return &resources, nil
}
// UseProject returns a client that will use a specific project.
func (r *ProtocolIncus) UseProject(name string) InstanceServer {
return &ProtocolIncus{
ctx: r.ctx,
ctxConnected: r.ctxConnected,
ctxConnectedCancel: r.ctxConnectedCancel,
server: r.server,
http: r.http,
httpCertificate: r.httpCertificate,
httpBaseURL: r.httpBaseURL,
httpProtocol: r.httpProtocol,
httpUserAgent: r.httpUserAgent,
httpUnixPath: r.httpUnixPath,
requireAuthenticated: r.requireAuthenticated,
clusterTarget: r.clusterTarget,
project: name,
eventConns: make(map[string]*websocket.Conn), // New project specific listener conns.
eventListeners: make(map[string][]*EventListener), // New project specific listeners.
skipEvents: r.skipEvents,
oidcClient: r.oidcClient,
}
}
// UseTarget returns a client that will target a specific cluster member.
// Use this member-specific operations such as specific container
// placement, preparing a new storage pool or network, ...
func (r *ProtocolIncus) UseTarget(name string) InstanceServer {
return &ProtocolIncus{
ctx: r.ctx,
ctxConnected: r.ctxConnected,
ctxConnectedCancel: r.ctxConnectedCancel,
server: r.server,
http: r.http,
httpCertificate: r.httpCertificate,
httpBaseURL: r.httpBaseURL,
httpProtocol: r.httpProtocol,
httpUserAgent: r.httpUserAgent,
httpUnixPath: r.httpUnixPath,
requireAuthenticated: r.requireAuthenticated,
project: r.project,
eventConns: make(map[string]*websocket.Conn), // New target specific listener conns.
eventListeners: make(map[string][]*EventListener), // New target specific listeners.
skipEvents: r.skipEvents,
oidcClient: r.oidcClient,
clusterTarget: name,
}
}
// IsAgent returns true if the server is an Incus agent.
func (r *ProtocolIncus) IsAgent() bool {
return r.server != nil && r.server.Environment.Server == "incus-agent"
}
// GetMetrics returns the text OpenMetrics data.
func (r *ProtocolIncus) GetMetrics() (string, error) {
// Check that the server supports it.
if !r.HasExtension("metrics") {
return "", errors.New("The server is missing the required \"metrics\" API extension")
}
// Prepare the request.
requestURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0/metrics", r.httpBaseURL.String()))
if err != nil {
return "", err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return "", err
}
// Send the request.
resp, err := r.DoHTTP(req)
if err != nil {
return "", err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Bad HTTP status: %d", resp.StatusCode)
}
// Get the content.
content, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(content), nil
}
// ApplyServerPreseed configures a target Incus server with the provided server and cluster configuration.
func (r *ProtocolIncus) ApplyServerPreseed(config api.InitPreseed) error {
// Apply server configuration.
if len(config.Config) > 0 {
// Get current config.
server, etag, err := r.GetServer()
if err != nil {
return fmt.Errorf("Failed to retrieve current server configuration: %w", err)
}
for k, v := range config.Config {
server.Config[k] = fmt.Sprintf("%v", v)
}
// Apply it.
err = r.UpdateServer(server.Writable(), etag)
if err != nil {
return fmt.Errorf("Failed to update server configuration: %w", err)
}
}
// Apply storage configuration.
if len(config.StoragePools) > 0 {
// Get the list of storagePools.
storagePoolNames, err := r.GetStoragePoolNames()
if err != nil {
return fmt.Errorf("Failed to retrieve list of storage pools: %w", err)
}
// StoragePool creator
createStoragePool := func(storagePool api.StoragePoolsPost) error {
// Create the storagePool if doesn't exist.
err := r.CreateStoragePool(storagePool)
if err != nil {
return fmt.Errorf("Failed to create storage pool %q: %w", storagePool.Name, err)
}
return nil
}
// StoragePool updater.
updateStoragePool := func(target api.StoragePoolsPost) error {
// Get the current storagePool.
storagePool, etag, err := r.GetStoragePool(target.Name)
if err != nil {
return fmt.Errorf("Failed to retrieve current storage pool %q: %w", target.Name, err)
}
// Quick check.
if storagePool.Driver != target.Driver {
return fmt.Errorf("Storage pool %q is of type %q instead of %q", storagePool.Name, storagePool.Driver, target.Driver)
}
// Description override.
if target.Description != "" {
storagePool.Description = target.Description
}
// Config overrides.
for k, v := range target.Config {
storagePool.Config[k] = fmt.Sprintf("%v", v)
}
// Apply it.
err = r.UpdateStoragePool(target.Name, storagePool.Writable(), etag)
if err != nil {
return fmt.Errorf("Failed to update storage pool %q: %w", target.Name, err)
}
return nil
}
for _, storagePool := range config.StoragePools {
// New storagePool.
if !slices.Contains(storagePoolNames, storagePool.Name) {
err := createStoragePool(storagePool)
if err != nil {
return err
}
continue
}
// Existing storagePool.
err := updateStoragePool(storagePool)
if err != nil {
return err
}
}
}
// Apply network configuration function.
applyNetwork := func(target api.InitNetworksProjectPost) error {
network, etag, err := r.UseProject(target.Project).GetNetwork(target.Name)
if err != nil {
// Create the network if doesn't exist.
err := r.UseProject(target.Project).CreateNetwork(target.NetworksPost)
if err != nil {
return fmt.Errorf("Failed to create local member network %q in project %q: %w", target.Name, target.Project, err)
}
} else {
// Description override.
if target.Description != "" {
network.Description = target.Description
}
// Config overrides.
for k, v := range target.Config {
network.Config[k] = fmt.Sprintf("%v", v)
}
// Apply it.
err = r.UseProject(target.Project).UpdateNetwork(target.Name, network.Writable(), etag)
if err != nil {
return fmt.Errorf("Failed to update local member network %q in project %q: %w", target.Name, target.Project, err)
}
}
return nil
}
// Apply networks in the default project before other projects config applied (so that if the projects
// depend on a network in the default project they can have their config applied successfully).
for i := range config.Networks {
// Populate default project if not specified for backwards compatibility with earlier
// preseed dump files.
if config.Networks[i].Project == "" {
config.Networks[i].Project = api.ProjectDefaultName
}
if config.Networks[i].Project != api.ProjectDefaultName {
continue
}
err := applyNetwork(config.Networks[i])
if err != nil {
return err
}
}
// Apply project configuration.
if len(config.Projects) > 0 {
// Get the list of projects.
projectNames, err := r.GetProjectNames()
if err != nil {
return fmt.Errorf("Failed to retrieve list of projects: %w", err)
}
// Project creator.
createProject := func(project api.ProjectsPost) error {
// Create the project if doesn't exist.
err := r.CreateProject(project)
if err != nil {
return fmt.Errorf("Failed to create local member project %q: %w", project.Name, err)
}
return nil
}
// Project updater.
updateProject := func(target api.ProjectsPost) error {
// Get the current project.
project, etag, err := r.GetProject(target.Name)
if err != nil {
return fmt.Errorf("Failed to retrieve current project %q: %w", target.Name, err)
}
// Description override.
if target.Description != "" {
project.Description = target.Description
}
// Config overrides.
for k, v := range target.Config {
project.Config[k] = fmt.Sprintf("%v", v)
}
// Apply it.
err = r.UpdateProject(target.Name, project.Writable(), etag)
if err != nil {
return fmt.Errorf("Failed to update local member project %q: %w", target.Name, err)
}
return nil
}
for _, project := range config.Projects {
// New project.
if !slices.Contains(projectNames, project.Name) {
err := createProject(project)
if err != nil {
return err
}
continue
}
// Existing project.
err := updateProject(project)
if err != nil {
return err
}
}
}
// Apply networks in non-default projects after project config applied (so that their projects exist).
for i := range config.Networks {
if config.Networks[i].Project == api.ProjectDefaultName {
continue
}
err := applyNetwork(config.Networks[i])
if err != nil {
return err
}
}
// Apply storage volumes configuration.
applyStorageVolume := func(storageVolume api.InitStorageVolumesProjectPost) error {
// Get the current storageVolume.
currentStorageVolume, etag, err := r.UseProject(storageVolume.Project).GetStoragePoolVolume(storageVolume.Pool, storageVolume.Type, storageVolume.Name)
if err != nil {
// Create the storage volume if it doesn't exist.
err := r.UseProject(storageVolume.Project).CreateStoragePoolVolume(storageVolume.Pool, storageVolume.StorageVolumesPost)
if err != nil {
return fmt.Errorf("Failed to create storage volume %q in project %q on pool %q: %w", storageVolume.Name, storageVolume.Project, storageVolume.Pool, err)
}
} else {
// Quick check.
if currentStorageVolume.Type != storageVolume.Type {
return fmt.Errorf("Storage volume %q in project %q is of type %q instead of %q", currentStorageVolume.Name, storageVolume.Project, currentStorageVolume.Type, storageVolume.Type)
}
// Prepare the update.
newStorageVolume := api.StorageVolumePut{}
err = util.DeepCopy(currentStorageVolume.Writable(), &newStorageVolume)
if err != nil {
return fmt.Errorf("Failed to copy configuration of storage volume %q in project %q: %w", storageVolume.Name, storageVolume.Project, err)
}
// Description override.
if storageVolume.Description != "" {
newStorageVolume.Description = storageVolume.Description
}
// Config overrides.
for k, v := range storageVolume.Config {
newStorageVolume.Config[k] = fmt.Sprintf("%v", v)
}
// Apply it.
err = r.UseProject(storageVolume.Project).UpdateStoragePoolVolume(storageVolume.Pool, storageVolume.Type, currentStorageVolume.Name, newStorageVolume, etag)
if err != nil {
return fmt.Errorf("Failed to update storage volume %q in project %q: %w", storageVolume.Name, storageVolume.Project, err)
}
}
return nil
}
// Apply storage volumes in the default project before other projects config.
for i := range config.StorageVolumes {
// Populate default project if not specified.
if config.StorageVolumes[i].Project == "" {
config.StorageVolumes[i].Project = api.ProjectDefaultName
}
// Populate default type if not specified.
if config.StorageVolumes[i].Type == "" {
config.StorageVolumes[i].Type = "custom"
}
err := applyStorageVolume(config.StorageVolumes[i])
if err != nil {
return err
}
}
// Apply profile configuration.
if len(config.Profiles) > 0 {
// Apply profile configuration.
applyProfile := func(profile api.InitProfileProjectPost) error {
// Get the current profile.
currentProfile, etag, err := r.UseProject(profile.Project).GetProfile(profile.Name)
if err != nil {
// // Create the profile if it doesn't exist.
err := r.UseProject(profile.Project).CreateProfile(profile.ProfilesPost)
if err != nil {
return fmt.Errorf("Failed to create profile %q in project %q: %w", profile.Name, profile.Project, err)
}
} else {
// Prepare the update.
updatedProfile := api.ProfilePut{}
err = util.DeepCopy(currentProfile.Writable(), &updatedProfile)
if err != nil {
return fmt.Errorf("Failed to copy configuration of profile %q in project %q: %w", profile.Name, profile.Project, err)
}
// Description override.
if profile.Description != "" {
updatedProfile.Description = profile.Description
}
// Config overrides.
for k, v := range profile.Config {
updatedProfile.Config[k] = fmt.Sprintf("%v", v)
}
// Device overrides.
for k, v := range profile.Devices {
// New device.
_, ok := updatedProfile.Devices[k]
if !ok {
updatedProfile.Devices[k] = v
continue
}
// Existing device.
for configKey, configValue := range v {
updatedProfile.Devices[k][configKey] = fmt.Sprintf("%v", configValue)
}
}
// Apply it.
err = r.UseProject(profile.Project).UpdateProfile(profile.Name, updatedProfile, etag)
if err != nil {
return fmt.Errorf("Failed to update profile %q in project %q: %w", profile.Name, profile.Project, err)
}
}
return nil
}
for _, profile := range config.Profiles {
if profile.Project == "" {
profile.Project = api.ProjectDefaultName
}
err := applyProfile(profile)
if err != nil {
return err
}
}
}
// Apply certificate configuration.
if len(config.Certificates) > 0 {
for _, certificate := range config.Certificates {
err := r.CreateCertificate(certificate)
if err != nil {
return fmt.Errorf("Failed to create certificate %q: %w", certificate.Name, err)
}
}
}
// Cluster configuration.
if config.Cluster != nil && config.Cluster.Enabled {
// Get the current cluster configuration
currentCluster, etag, err := r.GetCluster()
if err != nil {
return fmt.Errorf("Failed to retrieve current cluster config: %w", err)
}
// Check if already enabled
if !currentCluster.Enabled {
// Configure the cluster
op, err := r.UpdateCluster(config.Cluster.ClusterPut, etag)
if err != nil {
return fmt.Errorf("Failed to configure cluster: %w", err)
}
err = op.Wait()
if err != nil {
return fmt.Errorf("Failed to configure cluster: %w", err)
}
}
}
// Apply cluster group configurations.
if len(config.ClusterGroups) > 0 {
for _, clusterGroup := range config.ClusterGroups {
// Check if it already exists.
existing, etag, err := r.GetClusterGroup(clusterGroup.Name)
if err == nil && existing != nil {
// Keep existing members if none specified (set of empty slice to empty).
if clusterGroup.Members == nil {
clusterGroup.Members = existing.Members
}
// Update the existing group.
err = r.UpdateClusterGroup(clusterGroup.Name, clusterGroup.ClusterGroupPut, etag)
if err != nil {
return fmt.Errorf("Failed to update cluster group")
}
continue
}
// Create the new group.
err = r.CreateClusterGroup(clusterGroup)
if err != nil {
return fmt.Errorf("Failed to create cluster group")
}
}
}
return nil
}
incus-7.3.0/client/incus_storage_buckets.go 0000664 0000000 0000000 00000044306 15232704312 0021042 0 ustar 00root root 0000000 0000000 package incus
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/cancel"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
// GetStoragePoolBucketNames returns a list of storage bucket names.
func (r *ProtocolIncus) GetStoragePoolBucketNames(poolName string) ([]string, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
u := api.NewURL().Path("storage-pools", poolName, "buckets")
_, err = r.queryStruct("GET", u.String(), nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(u.String(), urls...)
}
// GetStoragePoolBuckets returns a list of storage buckets for the provided pool.
func (r *ProtocolIncus) GetStoragePoolBuckets(poolName string) ([]api.StorageBucket, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
buckets := []api.StorageBucket{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets").WithQuery("recursion", "1")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsWithFilter returns a filtered list of storage buckets for the provided pool.
func (r *ProtocolIncus) GetStoragePoolBucketsWithFilter(poolName string, filters []string) ([]api.StorageBucket, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
buckets := []api.StorageBucket{}
// Fetch the raw value
u := api.NewURL().Path("storage-pools", poolName, "buckets").
WithQuery("recursion", "1").
WithQuery("filter", parseFilters(filters))
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsAllProjects gets all storage pool buckets across all projects.
func (r *ProtocolIncus) GetStoragePoolBucketsAllProjects(poolName string) ([]api.StorageBucket, error) {
err := r.CheckExtension("storage_buckets_all_projects")
if err != nil {
return nil, errors.New(`The server is missing the required "storage_buckets_all_projects" API extension`)
}
buckets := []api.StorageBucket{}
u := api.NewURL().Path("storage-pools", poolName, "buckets").WithQuery("recursion", "1").WithQuery("all-projects", "true")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsWithFilterAllProjects gets a filtered list of storage pool buckets across all projects.
func (r *ProtocolIncus) GetStoragePoolBucketsWithFilterAllProjects(poolName string, filters []string) ([]api.StorageBucket, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
err = r.CheckExtension("storage_buckets_all_projects")
if err != nil {
return nil, errors.New(`The server is missing the required "storage_buckets_all_projects" API extension`)
}
buckets := []api.StorageBucket{}
u := api.NewURL().Path("storage-pools", poolName, "buckets").
WithQuery("recursion", "1").
WithQuery("filter", parseFilters(filters)).
WithQuery("all-projects", "true")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsFull returns a list of storage buckets for the provided pool (full struct).
func (r *ProtocolIncus) GetStoragePoolBucketsFull(poolName string) ([]api.StorageBucketFull, error) {
err := r.CheckExtension("storage_bucket_full")
if err != nil {
return nil, err
}
buckets := []api.StorageBucketFull{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets").WithQuery("recursion", "2")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsFullWithFilter returns a filtered list of storage buckets for the provided pool (full struct).
func (r *ProtocolIncus) GetStoragePoolBucketsFullWithFilter(poolName string, filters []string) ([]api.StorageBucketFull, error) {
err := r.CheckExtension("storage_bucket_full")
if err != nil {
return nil, err
}
buckets := []api.StorageBucketFull{}
// Fetch the raw value
u := api.NewURL().Path("storage-pools", poolName, "buckets").
WithQuery("recursion", "2").
WithQuery("filter", parseFilters(filters))
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsFullAllProjects gets all storage pool buckets across all projects (full struct).
func (r *ProtocolIncus) GetStoragePoolBucketsFullAllProjects(poolName string) ([]api.StorageBucketFull, error) {
err := r.CheckExtension("storage_bucket_full")
if err != nil {
return nil, errors.New(`The server is missing the required "storage_bucket_full" API extension`)
}
buckets := []api.StorageBucketFull{}
u := api.NewURL().Path("storage-pools", poolName, "buckets").WithQuery("recursion", "2").WithQuery("all-projects", "true")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucketsFullWithFilterAllProjects gets a filtered list of storage pool buckets across all projects (full struct).
func (r *ProtocolIncus) GetStoragePoolBucketsFullWithFilterAllProjects(poolName string, filters []string) ([]api.StorageBucketFull, error) {
err := r.CheckExtension("storage_bucket_full")
if err != nil {
return nil, err
}
buckets := []api.StorageBucketFull{}
u := api.NewURL().Path("storage-pools", poolName, "buckets").
WithQuery("recursion", "2").
WithQuery("filter", parseFilters(filters)).
WithQuery("all-projects", "true")
_, err = r.queryStruct("GET", u.String(), nil, "", &buckets)
if err != nil {
return nil, err
}
return buckets, nil
}
// GetStoragePoolBucket returns a storage bucket entry for the provided pool and bucket name.
func (r *ProtocolIncus) GetStoragePoolBucket(poolName string, bucketName string) (*api.StorageBucket, string, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, "", err
}
bucket := api.StorageBucket{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName)
etag, err := r.queryStruct("GET", u.String(), nil, "", &bucket)
if err != nil {
return nil, "", err
}
return &bucket, etag, nil
}
// GetStoragePoolBucketFull returns a full storage bucket entry for the provided pool and bucket name.
func (r *ProtocolIncus) GetStoragePoolBucketFull(poolName string, bucketName string) (*api.StorageBucketFull, string, error) {
err := r.CheckExtension("storage_bucket_full")
if err != nil {
return nil, "", err
}
bucket := api.StorageBucketFull{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName).WithQuery("recursion", "1")
etag, err := r.queryStruct("GET", u.String(), nil, "", &bucket)
if err != nil {
return nil, "", err
}
return &bucket, etag, nil
}
// CreateStoragePoolBucket defines a new storage bucket using the provided struct.
// If the server supports storage_buckets_create_credentials API extension, then this function will return the
// initial admin credentials. Otherwise it will be nil.
func (r *ProtocolIncus) CreateStoragePoolBucket(poolName string, bucket api.StorageBucketsPost) (*api.StorageBucketKey, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
u := api.NewURL().Path("storage-pools", poolName, "buckets")
// Send the request and get the resulting key info (including generated keys).
if r.HasExtension("storage_buckets_create_credentials") {
var newKey api.StorageBucketKey
_, err = r.queryStruct("POST", u.String(), bucket, "", &newKey)
if err != nil {
return nil, err
}
return &newKey, nil
}
_, _, err = r.query("POST", u.String(), bucket, "")
if err != nil {
return nil, err
}
return nil, nil
}
// UpdateStoragePoolBucket updates the storage bucket to match the provided struct.
func (r *ProtocolIncus) UpdateStoragePoolBucket(poolName string, bucketName string, bucket api.StorageBucketPut, ETag string) error {
err := r.CheckExtension("storage_buckets")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName)
_, _, err = r.query("PUT", u.String(), bucket, ETag)
if err != nil {
return err
}
return nil
}
// DeleteStoragePoolBucket deletes an existing storage bucket.
func (r *ProtocolIncus) DeleteStoragePoolBucket(poolName string, bucketName string) error {
err := r.CheckExtension("storage_buckets")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName)
_, _, err = r.query("DELETE", u.String(), nil, "")
if err != nil {
return err
}
return nil
}
// GetStoragePoolBucketKeyNames returns a list of storage bucket key names.
func (r *ProtocolIncus) GetStoragePoolBucketKeyNames(poolName string, bucketName string) ([]string, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys")
_, err = r.queryStruct("GET", u.String(), nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(u.String(), urls...)
}
// GetStoragePoolBucketKeys returns a list of storage bucket keys for the provided pool and bucket.
func (r *ProtocolIncus) GetStoragePoolBucketKeys(poolName string, bucketName string) ([]api.StorageBucketKey, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
bucketKeys := []api.StorageBucketKey{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys").WithQuery("recursion", "1")
_, err = r.queryStruct("GET", u.String(), nil, "", &bucketKeys)
if err != nil {
return nil, err
}
return bucketKeys, nil
}
// GetStoragePoolBucketKey returns a storage bucket key entry for the provided pool, bucket and key name.
func (r *ProtocolIncus) GetStoragePoolBucketKey(poolName string, bucketName string, keyName string) (*api.StorageBucketKey, string, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, "", err
}
bucketKey := api.StorageBucketKey{}
// Fetch the raw value.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys", keyName)
etag, err := r.queryStruct("GET", u.String(), nil, "", &bucketKey)
if err != nil {
return nil, "", err
}
return &bucketKey, etag, nil
}
// CreateStoragePoolBucketKey adds a key to a storage bucket.
func (r *ProtocolIncus) CreateStoragePoolBucketKey(poolName string, bucketName string, key api.StorageBucketKeysPost) (*api.StorageBucketKey, error) {
err := r.CheckExtension("storage_buckets")
if err != nil {
return nil, err
}
// Send the request and get the resulting key info (including generated keys).
var newKey api.StorageBucketKey
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys")
_, err = r.queryStruct("POST", u.String(), key, "", &newKey)
if err != nil {
return nil, err
}
return &newKey, err
}
// UpdateStoragePoolBucketKey updates an existing storage bucket key.
func (r *ProtocolIncus) UpdateStoragePoolBucketKey(poolName string, bucketName string, keyName string, key api.StorageBucketKeyPut, ETag string) error {
err := r.CheckExtension("storage_buckets")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys", keyName)
_, _, err = r.query("PUT", u.String(), key, ETag)
if err != nil {
return err
}
return nil
}
// DeleteStoragePoolBucketKey removes a key from a storage bucket.
func (r *ProtocolIncus) DeleteStoragePoolBucketKey(poolName string, bucketName string, keyName string) error {
err := r.CheckExtension("storage_buckets")
if err != nil {
return err
}
// Send the request.
u := api.NewURL().Path("storage-pools", poolName, "buckets", bucketName, "keys", keyName)
_, _, err = r.query("DELETE", u.String(), nil, "")
if err != nil {
return err
}
return nil
}
// CreateStoragePoolBucketBackup creates a new storage bucket backup.
func (r *ProtocolIncus) CreateStoragePoolBucketBackup(poolName string, bucketName string, backup api.StorageBucketBackupsPost) (Operation, error) {
err := r.CheckExtension("storage_bucket_backup")
if err != nil {
return nil, err
}
op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/buckets/%s/backups", url.PathEscape(poolName), url.PathEscape(bucketName)), backup, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteStoragePoolBucketBackup deletes an existing storage bucket backup.
func (r *ProtocolIncus) DeleteStoragePoolBucketBackup(pool string, bucketName string, name string) (Operation, error) {
err := r.CheckExtension("storage_bucket_backup")
if err != nil {
return nil, err
}
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/storage-pools/%s/buckets/%s/backups/%s", url.PathEscape(pool), url.PathEscape(bucketName), url.PathEscape(name)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// GetStoragePoolBucketBackupFile returns the storage bucket file.
func (r *ProtocolIncus) GetStoragePoolBucketBackupFile(pool string, bucketName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
err := r.CheckExtension("storage_bucket_backup")
if err != nil {
return nil, err
}
// Build the URL
uri := fmt.Sprintf("%s/1.0/storage-pools/%s/buckets/%s/backups/%s/export", r.httpBaseURL.String(), url.PathEscape(pool), url.PathEscape(bucketName), url.PathEscape(name))
if r.project != "" {
uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
}
// Prepare the download request
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return nil, err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(response)
if err != nil {
return nil, err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Length: response.ContentLength,
Handler: func(percent int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
},
},
}
}
size, err := util.SafeCopy(req.BackupFile, body)
if err != nil {
return nil, err
}
resp := BackupFileResponse{}
resp.Size = size
return &resp, nil
}
// CreateStoragePoolBucketBackupStream requests that Incus creates and returns new direct backup
// for the storage bucket.
func (r *ProtocolIncus) CreateStoragePoolBucketBackupStream(poolName string, bucketName string, backup api.StorageBucketBackupsPost, req *BackupFileRequest) error {
if !r.HasExtension("direct_backup") {
return errors.New("The server is missing the required \"direct_backup\" API extension")
}
// Build the URL
uri := fmt.Sprintf("%s/1.0/storage-pools/%s/buckets/%s/backups", r.httpBaseURL.String(), url.PathEscape(poolName), url.PathEscape(bucketName))
if r.project != "" {
uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
}
// Encode the backup data
buf := bytes.Buffer{}
err := json.NewEncoder(&buf).Encode(backup)
if err != nil {
return err
}
// Prepare the download request
request, err := http.NewRequest("POST", uri, bytes.NewReader(buf.Bytes()))
if err != nil {
return err
}
request.Header.Set("Accept", "application/octet-stream")
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err = incusParseResponse(response)
if err != nil {
return err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
}
_, err = util.SafeCopy(req.BackupFile, body)
return err
}
// CreateStoragePoolBucketFromBackup creates a new storage bucket from a backup.
func (r *ProtocolIncus) CreateStoragePoolBucketFromBackup(pool string, args StoragePoolBucketBackupArgs) (Operation, error) {
if !r.HasExtension("storage_bucket_backup") {
return nil, errors.New(`The server is missing the required "custom_volume_backup" API extension`)
}
path := fmt.Sprintf("/storage-pools/%s/buckets", url.PathEscape(pool))
// Prepare the HTTP request.
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", reqURL, args.BackupFile)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
if args.Name != "" {
req.Header.Set("X-Incus-name", args.Name)
}
// Send the request.
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
// Handle errors.
response, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
respOperation, err := response.MetadataAsOperation()
if err != nil {
return nil, err
}
op := operation{
Operation: *respOperation,
r: r,
chActive: make(chan bool),
}
return &op, nil
}
incus-7.3.0/client/incus_storage_pools.go 0000664 0000000 0000000 00000007643 15232704312 0020541 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// Storage pool handling functions
// GetStoragePoolNames returns the names of all storage pools.
func (r *ProtocolIncus) GetStoragePoolNames() ([]string, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := "/storage-pools"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetStoragePools returns a list of StoragePool entries.
func (r *ProtocolIncus) GetStoragePools() ([]api.StoragePool, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
pools := []api.StoragePool{}
// Fetch the raw value
_, err := r.queryStruct("GET", "/storage-pools?recursion=1", nil, "", &pools)
if err != nil {
return nil, err
}
return pools, nil
}
// GetStoragePoolsWithFilter returns a filtered list of storage pools as StoragePool structs.
func (r *ProtocolIncus) GetStoragePoolsWithFilter(filters []string) ([]api.StoragePool, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
pools := []api.StoragePool{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools?%s", v.Encode()), nil, "", &pools)
if err != nil {
return nil, err
}
return pools, nil
}
// GetStoragePool returns a StoragePool entry for the provided pool name.
func (r *ProtocolIncus) GetStoragePool(name string) (*api.StoragePool, string, error) {
if !r.HasExtension("storage") {
return nil, "", errors.New("The server is missing the required \"storage\" API extension")
}
pool := api.StoragePool{}
// Fetch the raw value
etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s", url.PathEscape(name)), nil, "", &pool)
if err != nil {
return nil, "", err
}
return &pool, etag, nil
}
// CreateStoragePool defines a new storage pool using the provided StoragePool struct.
func (r *ProtocolIncus) CreateStoragePool(pool api.StoragePoolsPost) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
// Send the request
_, _, err := r.query("POST", "/storage-pools", pool, "")
if err != nil {
return err
}
return nil
}
// UpdateStoragePool updates the pool to match the provided StoragePool struct.
func (r *ProtocolIncus) UpdateStoragePool(name string, pool api.StoragePoolPut, ETag string) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/storage-pools/%s", url.PathEscape(name)), pool, ETag)
if err != nil {
return err
}
return nil
}
// DeleteStoragePool deletes a storage pool.
func (r *ProtocolIncus) DeleteStoragePool(name string) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/storage-pools/%s", url.PathEscape(name)), nil, "")
if err != nil {
return err
}
return nil
}
// GetStoragePoolResources gets the resources available to a given storage pool.
func (r *ProtocolIncus) GetStoragePoolResources(name string) (*api.ResourcesStoragePool, error) {
if !r.HasExtension("resources") {
return nil, errors.New("The server is missing the required \"resources\" API extension")
}
res := api.ResourcesStoragePool{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/resources", url.PathEscape(name)), nil, "", &res)
if err != nil {
return nil, err
}
return &res, nil
}
incus-7.3.0/client/incus_storage_volumes.go 0000664 0000000 0000000 00000140642 15232704312 0021074 0 ustar 00root root 0000000 0000000 package incus
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/sftp"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/cancel"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
// Storage volumes handling function
// GetStoragePoolVolumeNames returns the names of all volumes in a pool.
func (r *ProtocolIncus) GetStoragePoolVolumeNames(pool string) ([]string, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/storage-pools/%s/volumes", url.PathEscape(pool))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetStoragePoolVolumeNamesAllProjects returns the names of all volumes in a pool for all projects.
func (r *ProtocolIncus) GetStoragePoolVolumeNamesAllProjects(pool string) (map[string][]string, error) {
err := r.CheckExtension("storage")
if err != nil {
return nil, err
}
err = r.CheckExtension("storage_volumes_all_projects")
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
u := api.NewURL().Path("storage-pools", pool, "volumes").WithQuery("all-projects", "true")
_, err = r.queryStruct("GET", u.String(), nil, "", &urls)
if err != nil {
return nil, err
}
names := make(map[string][]string)
for _, urlString := range urls {
resourceURL, err := url.Parse(urlString)
if err != nil {
return nil, fmt.Errorf("Could not parse unexpected URL %q: %w", urlString, err)
}
project := resourceURL.Query().Get("project")
if project == "" {
project = api.ProjectDefaultName
}
_, after, found := strings.Cut(resourceURL.Path, fmt.Sprintf("%s/", u.URL.Path))
if !found {
return nil, fmt.Errorf("Unexpected URL path %q", resourceURL)
}
names[project] = append(names[project], after)
}
return names, nil
}
// GetStoragePoolVolumes returns a list of StorageVolume entries for the provided pool.
func (r *ProtocolIncus) GetStoragePoolVolumes(pool string) ([]api.StorageVolume, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
volumes := []api.StorageVolume{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?recursion=1", url.PathEscape(pool)), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesAllProjects returns a list of StorageVolume entries for the provided pool for all projects.
func (r *ProtocolIncus) GetStoragePoolVolumesAllProjects(pool string) ([]api.StorageVolume, error) {
err := r.CheckExtension("storage")
if err != nil {
return nil, err
}
err = r.CheckExtension("storage_volumes_all_projects")
if err != nil {
return nil, err
}
volumes := []api.StorageVolume{}
uri := api.NewURL().Path("storage-pools", pool, "volumes").
WithQuery("recursion", "1").
WithQuery("all-projects", "true")
// Fetch the raw value.
_, err = r.queryStruct("GET", uri.String(), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesWithFilter returns a filtered list of StorageVolume entries for the provided pool.
func (r *ProtocolIncus) GetStoragePoolVolumesWithFilter(pool string, filters []string) ([]api.StorageVolume, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}
volumes := []api.StorageVolume{}
v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?%s", url.PathEscape(pool), v.Encode()), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesWithFilterAllProjects returns a filtered list of StorageVolume entries for the provided pool for all projects.
func (r *ProtocolIncus) GetStoragePoolVolumesWithFilterAllProjects(pool string, filters []string) ([]api.StorageVolume, error) {
err := r.CheckExtension("storage")
if err != nil {
return nil, err
}
err = r.CheckExtension("storage_volumes_all_projects")
if err != nil {
return nil, err
}
volumes := []api.StorageVolume{}
uri := api.NewURL().Path("storage-pools", pool, "volumes").
WithQuery("recursion", "1").
WithQuery("filter", parseFilters(filters)).
WithQuery("all-projects", "true")
// Fetch the raw value.
_, err = r.queryStruct("GET", uri.String(), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesFull returns a list of StorageVolume entries for the provided pool (full struct).
func (r *ProtocolIncus) GetStoragePoolVolumesFull(pool string) ([]api.StorageVolumeFull, error) {
if !r.HasExtension("storage_volume_full") {
return nil, errors.New("The server is missing the required \"storage_volume_full\" API extension")
}
volumes := []api.StorageVolumeFull{}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?recursion=2", url.PathEscape(pool)), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesFullAllProjects returns a list of StorageVolume entries for the provided pool for all projects (full struct).
func (r *ProtocolIncus) GetStoragePoolVolumesFullAllProjects(pool string) ([]api.StorageVolumeFull, error) {
err := r.CheckExtension("storage_volume_full")
if err != nil {
return nil, err
}
volumes := []api.StorageVolumeFull{}
uri := api.NewURL().Path("storage-pools", pool, "volumes").
WithQuery("recursion", "2").
WithQuery("all-projects", "true")
// Fetch the raw value.
_, err = r.queryStruct("GET", uri.String(), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesFullWithFilter returns a filtered list of StorageVolume entries for the provided pool (full struct).
func (r *ProtocolIncus) GetStoragePoolVolumesFullWithFilter(pool string, filters []string) ([]api.StorageVolumeFull, error) {
if !r.HasExtension("storage_volume_full") {
return nil, errors.New("The server is missing the required \"storage_volume_full\" API extension")
}
volumes := []api.StorageVolumeFull{}
v := url.Values{}
v.Set("recursion", "2")
v.Set("filter", parseFilters(filters))
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?%s", url.PathEscape(pool), v.Encode()), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolumesFullWithFilterAllProjects returns a filtered list of StorageVolume entries for the provided pool for all projects (full struct).
func (r *ProtocolIncus) GetStoragePoolVolumesFullWithFilterAllProjects(pool string, filters []string) ([]api.StorageVolumeFull, error) {
err := r.CheckExtension("storage_volume_full")
if err != nil {
return nil, err
}
volumes := []api.StorageVolumeFull{}
uri := api.NewURL().Path("storage-pools", pool, "volumes").
WithQuery("recursion", "2").
WithQuery("filter", parseFilters(filters)).
WithQuery("all-projects", "true")
// Fetch the raw value.
_, err = r.queryStruct("GET", uri.String(), nil, "", &volumes)
if err != nil {
return nil, err
}
return volumes, nil
}
// GetStoragePoolVolume returns a StorageVolume entry for the provided pool and volume name.
func (r *ProtocolIncus) GetStoragePoolVolume(pool string, volType string, name string) (*api.StorageVolume, string, error) {
if !r.HasExtension("storage") {
return nil, "", errors.New("The server is missing the required \"storage\" API extension")
}
volume := api.StorageVolume{}
// Fetch the raw value
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
etag, err := r.queryStruct("GET", path, nil, "", &volume)
if err != nil {
return nil, "", err
}
return &volume, etag, nil
}
// GetStoragePoolVolumeFull returns a StorageVolumeFull entry for the provided pool and volume name.
func (r *ProtocolIncus) GetStoragePoolVolumeFull(pool string, volType string, name string) (*api.StorageVolumeFull, string, error) {
if !r.HasExtension("storage_volume_full") {
return nil, "", errors.New("The server is missing the required \"storage_volume_full\" API extension")
}
volume := api.StorageVolumeFull{}
// Fetch the raw value
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s?recursion=1", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
etag, err := r.queryStruct("GET", path, nil, "", &volume)
if err != nil {
return nil, "", err
}
return &volume, etag, nil
}
// GetStoragePoolVolumeState returns a StorageVolumeState entry for the provided pool and volume name.
func (r *ProtocolIncus) GetStoragePoolVolumeState(pool string, volType string, name string) (*api.StorageVolumeState, error) {
if !r.HasExtension("storage_volume_state") {
return nil, errors.New("The server is missing the required \"storage_volume_state\" API extension")
}
// Fetch the raw value
state := api.StorageVolumeState{}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/state", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
_, err := r.queryStruct("GET", path, nil, "", &state)
if err != nil {
return nil, err
}
return &state, nil
}
// CreateStoragePoolVolume defines a new storage volume.
func (r *ProtocolIncus) CreateStoragePoolVolume(pool string, volume api.StorageVolumesPost) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(volume.Type))
_, _, err := r.query("POST", path, volume, "")
if err != nil {
return err
}
return nil
}
// CreateStoragePoolVolumeSnapshot defines a new storage volume.
func (r *ProtocolIncus) CreateStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshot api.StorageVolumeSnapshotsPost) (Operation, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots",
url.PathEscape(pool),
url.PathEscape(volumeType),
url.PathEscape(volumeName))
op, _, err := r.queryOperation("POST", path, snapshot, "")
if err != nil {
return nil, err
}
return op, nil
}
// GetStoragePoolVolumeSnapshotNames returns a list of snapshot names for the
// storage volume.
func (r *ProtocolIncus) GetStoragePoolVolumeSnapshotNames(pool string, volumeType string, volumeName string) ([]string, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots", url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetStoragePoolVolumeSnapshots returns a list of snapshots for the storage
// volume.
func (r *ProtocolIncus) GetStoragePoolVolumeSnapshots(pool string, volumeType string, volumeName string) ([]api.StorageVolumeSnapshot, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
snapshots := []api.StorageVolumeSnapshot{}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots?recursion=1",
url.PathEscape(pool),
url.PathEscape(volumeType),
url.PathEscape(volumeName))
_, err := r.queryStruct("GET", path, nil, "", &snapshots)
if err != nil {
return nil, err
}
return snapshots, nil
}
// GetStoragePoolVolumeSnapshot returns a snapshots for the storage volume.
func (r *ProtocolIncus) GetStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string) (*api.StorageVolumeSnapshot, string, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, "", errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
snapshot := api.StorageVolumeSnapshot{}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots/%s",
url.PathEscape(pool),
url.PathEscape(volumeType),
url.PathEscape(volumeName),
url.PathEscape(snapshotName))
etag, err := r.queryStruct("GET", path, nil, "", &snapshot)
if err != nil {
return nil, "", err
}
return &snapshot, etag, nil
}
// RenameStoragePoolVolumeSnapshot renames a storage volume snapshot.
func (r *ProtocolIncus) RenameStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string, snapshot api.StorageVolumeSnapshotPost) (Operation, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots/%s", url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.PathEscape(snapshotName))
// Send the request
op, _, err := r.queryOperation("POST", path, snapshot, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteStoragePoolVolumeSnapshot deletes a storage volume snapshot.
func (r *ProtocolIncus) DeleteStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string) (Operation, error) {
if !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
// Send the request
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/snapshots/%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.PathEscape(snapshotName),
)
op, _, err := r.queryOperation("DELETE", path, nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// UpdateStoragePoolVolumeSnapshot updates the volume to match the provided StoragePoolVolume struct.
func (r *ProtocolIncus) UpdateStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string, volume api.StorageVolumeSnapshotPut, ETag string) error {
if !r.HasExtension("storage_api_volume_snapshots") {
return errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/snapshots/%s", url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.PathEscape(snapshotName))
_, _, err := r.queryOperation("PUT", path, volume, ETag)
if err != nil {
return err
}
return nil
}
// MigrateStoragePoolVolume requests that Incus prepares for a storage volume migration.
func (r *ProtocolIncus) MigrateStoragePoolVolume(pool string, volume api.StorageVolumePost) (Operation, error) {
if !r.HasExtension("storage_api_remote_volume_handling") {
return nil, errors.New("The server is missing the required \"storage_api_remote_volume_handling\" API extension")
}
// Quick check.
if !volume.Migration {
return nil, errors.New("Can't ask for a rename through MigrateStoragePoolVolume")
}
var req any
var path string
srcVolParentName, srcVolSnapName, srcIsSnapshot := api.GetParentAndSnapshotName(volume.Name)
if srcIsSnapshot {
err := r.CheckExtension("storage_api_remote_volume_snapshot_copy")
if err != nil {
return nil, err
}
// Set the actual name of the snapshot without delimiter.
req = api.StorageVolumeSnapshotPost{
Name: srcVolSnapName,
Migration: volume.Migration,
Target: volume.Target,
}
path = api.NewURL().Path("storage-pools", pool, "volumes", "custom", srcVolParentName, "snapshots", srcVolSnapName).String()
} else {
req = volume
path = api.NewURL().Path("storage-pools", pool, "volumes", "custom", volume.Name).String()
}
// Send the request
op, _, err := r.queryOperation("POST", path, req, "")
if err != nil {
return nil, err
}
return op, nil
}
func (r *ProtocolIncus) tryMigrateStoragePoolVolume(source InstanceServer, pool string, req api.StorageVolumePost, urls []string) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Target.Operation
// Forward targetOp to remote op
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
// Send the request
top, err := source.MigrateStoragePoolVolume(pool, req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop := remoteOperation{
targetOp: top,
chDone: make(chan bool),
}
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed storage volume creation", errors)
}
close(rop.chDone)
}()
return &rop, nil
}
// tryCreateStoragePoolVolume attempts to create a storage volume in the specified storage pool.
// It will try to do this on every server in the provided list of urls, and waits for the creation to be complete.
func (r *ProtocolIncus) tryCreateStoragePoolVolume(pool string, req api.StorageVolumesPost, urls []string) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Source.Operation
// Forward targetOp to remote op
go func() {
success := false
var errors []remoteOperationResult
for _, serverURL := range urls {
req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(req.Type))
top, _, err := r.queryOperation("POST", path, req, "")
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop := remoteOperation{
targetOp: top,
chDone: make(chan bool),
}
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed storage volume creation", errors)
}
close(rop.chDone)
}()
return &rop, nil
}
// CopyStoragePoolVolume copies an existing storage volume.
func (r *ProtocolIncus) CopyStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (RemoteOperation, error) {
if !r.HasExtension("storage_api_local_volume_handling") {
return nil, errors.New("The server is missing the required \"storage_api_local_volume_handling\" API extension")
}
if args != nil && args.VolumeOnly && !r.HasExtension("storage_api_volume_snapshots") {
return nil, errors.New("The target server is missing the required \"storage_api_volume_snapshots\" API extension")
}
if args != nil && args.Refresh && !r.HasExtension("custom_volume_refresh") {
return nil, errors.New("The target server is missing the required \"custom_volume_refresh\" API extension")
}
if args != nil && args.RefreshExcludeOlder && !r.HasExtension("custom_volume_refresh_exclude_older_snapshots") {
return nil, errors.New("The target server is missing the required \"custom_volume_refresh_exclude_older_snapshots\" API extension")
}
req := api.StorageVolumesPost{
Name: args.Name,
Type: volume.Type,
Source: api.StorageVolumeSource{
Name: volume.Name,
Type: "copy",
Pool: sourcePool,
VolumeOnly: args.VolumeOnly,
Refresh: args.Refresh,
RefreshExcludeOlder: args.RefreshExcludeOlder,
},
}
req.Config = volume.Config
req.Description = volume.Description
req.ContentType = volume.ContentType
sourceInfo, err := source.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get source connection info: %w", err)
}
destInfo, err := r.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
}
clusterInternalVolumeCopy := r.CheckExtension("cluster_internal_custom_volume_copy") == nil
// Copy the storage pool volume locally.
if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (volume.Location == r.clusterTarget || (volume.Location == "none" && r.clusterTarget == "") || clusterInternalVolumeCopy) {
// Project handling
if destInfo.Project != sourceInfo.Project {
if !r.HasExtension("storage_api_project") {
return nil, errors.New("The server is missing the required \"storage_api_project\" API extension")
}
req.Source.Project = sourceInfo.Project
}
if clusterInternalVolumeCopy {
req.Source.Location = sourceInfo.Target
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(volume.Type)), req, "")
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
if !r.HasExtension("storage_api_remote_volume_handling") {
return nil, errors.New("The server is missing the required \"storage_api_remote_volume_handling\" API extension")
}
sourceReq := api.StorageVolumePost{
Migration: true,
Name: volume.Name,
Pool: sourcePool,
}
if args != nil {
sourceReq.VolumeOnly = args.VolumeOnly
}
// Push mode migration
if args != nil && args.Mode == "push" {
// Get target server connection information
info, err := r.GetConnectionInfo()
if err != nil {
return nil, err
}
// Set the source type and direction
req.Source.Type = "migration"
req.Source.Mode = "push"
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(volume.Type))
// Send the request
op, _, err := r.queryOperation("POST", path, req, "")
if err != nil {
return nil, err
}
opAPI := op.Get()
targetSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Prepare the source request
target := api.StorageVolumePostTarget{}
target.Operation = opAPI.ID
target.Websockets = targetSecrets
target.Certificate = info.Certificate
sourceReq.Target = &target
return r.tryMigrateStoragePoolVolume(source, sourcePool, sourceReq, info.Addresses)
}
// Get source server connection information
info, err := source.GetConnectionInfo()
if err != nil {
return nil, err
}
// Get secrets from source server
op, err := source.MigrateStoragePoolVolume(sourcePool, sourceReq)
if err != nil {
return nil, err
}
opAPI := op.Get()
// Prepare source server secrets for remote
sourceSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
sourceSecrets[k] = val
}
}
// Relay mode migration
if args != nil && args.Mode == "relay" {
// Push copy source fields
req.Source.Type = "migration"
req.Source.Mode = "push"
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(volume.Type))
// Send the request
targetOp, _, err := r.queryOperation("POST", path, req, "")
if err != nil {
return nil, err
}
targetOpAPI := targetOp.Get()
// Extract the websockets
targetSecrets := map[string]string{}
for k, v := range targetOpAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// Launch the relay
err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
if err != nil {
return nil, err
}
// Prepare a tracking operation
rop := remoteOperation{
targetOp: targetOp,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// Pull mode migration
req.Source.Type = "migration"
req.Source.Mode = "pull"
req.Source.Operation = opAPI.ID
req.Source.Websockets = sourceSecrets
req.Source.Certificate = info.Certificate
return r.tryCreateStoragePoolVolume(pool, req, info.Addresses)
}
// MoveStoragePoolVolume renames or moves an existing storage volume.
func (r *ProtocolIncus) MoveStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (RemoteOperation, error) {
if !r.HasExtension("storage_api_local_volume_handling") {
return nil, errors.New("The server is missing the required \"storage_api_local_volume_handling\" API extension")
}
if r != source {
return nil, errors.New("Moving storage volumes between remotes is not implemented")
}
req := api.StorageVolumePost{
Name: args.Name,
Pool: pool,
}
if args.Project != "" {
if !r.HasExtension("storage_volume_project_move") {
return nil, errors.New("The server is missing the required \"storage_volume_project_move\" API extension")
}
req.Project = args.Project
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", url.PathEscape(sourcePool), url.PathEscape(volume.Type), volume.Name), req, "")
if err != nil {
return nil, err
}
rop := remoteOperation{
targetOp: op,
chDone: make(chan bool),
}
// Forward targetOp to remote op
go func() {
rop.err = rop.targetOp.Wait()
close(rop.chDone)
}()
return &rop, nil
}
// UpdateStoragePoolVolume updates the volume to match the provided StoragePoolVolume struct.
func (r *ProtocolIncus) UpdateStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePut, ETag string) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
if volume.Restore != "" && !r.HasExtension("storage_api_volume_snapshots") {
return errors.New("The server is missing the required \"storage_api_volume_snapshots\" API extension")
}
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
_, _, err := r.query("PUT", path, volume, ETag)
if err != nil {
return err
}
return nil
}
// DeleteStoragePoolVolume deletes a storage pool.
func (r *ProtocolIncus) DeleteStoragePoolVolume(pool string, volType string, name string) error {
if !r.HasExtension("storage") {
return errors.New("The server is missing the required \"storage\" API extension")
}
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
_, _, err := r.query("DELETE", path, nil, "")
if err != nil {
return err
}
return nil
}
// RebuildStoragePoolVolume rebuilds an existing custom storage volume as empty.
func (r *ProtocolIncus) RebuildStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumeRebuildPost) (Operation, error) {
err := r.CheckExtension("storage_volumes_rebuild")
if err != nil {
return nil, err
}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/rebuild", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
// Send the request.
op, _, err := r.queryOperation("POST", path, volume, "")
if err != nil {
return nil, err
}
return op, nil
}
// RenameStoragePoolVolume renames a storage volume.
func (r *ProtocolIncus) RenameStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePost) error {
if !r.HasExtension("storage_api_volume_rename") {
return errors.New("The server is missing the required \"storage_api_volume_rename\" API extension")
}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s", url.PathEscape(pool), url.PathEscape(volType), url.PathEscape(name))
// Send the request
_, _, err := r.query("POST", path, volume, "")
if err != nil {
return err
}
return nil
}
// GetStorageVolumeBackupNames returns a list of volume backup names.
func (r *ProtocolIncus) GetStorageVolumeBackupNames(pool string, volName string) ([]string, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups", url.PathEscape(pool), url.PathEscape(volName))
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetStorageVolumeBackups returns a list of custom volume backups.
func (r *ProtocolIncus) GetStorageVolumeBackups(pool string, volName string) ([]api.StorageVolumeBackup, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Fetch the raw value
backups := []api.StorageVolumeBackup{}
_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups?recursion=1", url.PathEscape(pool), url.PathEscape(volName)), nil, "", &backups)
if err != nil {
return nil, err
}
return backups, nil
}
// GetStorageVolumeBackup returns a custom volume backup.
func (r *ProtocolIncus) GetStorageVolumeBackup(pool string, volName string, name string) (*api.StorageVolumeBackup, string, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, "", errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Fetch the raw value
backup := api.StorageVolumeBackup{}
etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups/%s", url.PathEscape(pool), url.PathEscape(volName), url.PathEscape(name)), nil, "", &backup)
if err != nil {
return nil, "", err
}
return &backup, etag, nil
}
// CreateStorageVolumeBackup creates new custom volume backup.
func (r *ProtocolIncus) CreateStorageVolumeBackup(pool string, volName string, backup api.StorageVolumeBackupsPost) (Operation, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups", url.PathEscape(pool), url.PathEscape(volName)), backup, "")
if err != nil {
return nil, err
}
return op, nil
}
// RenameStorageVolumeBackup renames a custom volume backup.
func (r *ProtocolIncus) RenameStorageVolumeBackup(pool string, volName string, name string, backup api.StorageVolumeBackupPost) (Operation, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups/%s", url.PathEscape(pool), url.PathEscape(volName), url.PathEscape(name)), backup, "")
if err != nil {
return nil, err
}
return op, nil
}
// DeleteStorageVolumeBackup deletes a custom volume backup.
func (r *ProtocolIncus) DeleteStorageVolumeBackup(pool string, volName string, name string) (Operation, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Send the request
op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/storage-pools/%s/volumes/custom/%s/backups/%s", url.PathEscape(pool), url.PathEscape(volName), url.PathEscape(name)), nil, "")
if err != nil {
return nil, err
}
return op, nil
}
// GetStorageVolumeBackupFile requests the custom volume backup content.
func (r *ProtocolIncus) GetStorageVolumeBackupFile(pool string, volName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New("The server is missing the required \"custom_volume_backup\" API extension")
}
// Build the URL
uri := fmt.Sprintf("%s/1.0/storage-pools/%s/volumes/custom/%s/backups/%s/export", r.httpBaseURL.String(), url.PathEscape(pool), url.PathEscape(volName), url.PathEscape(name))
// Add project/target
uri, err := r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
// Prepare the download request
request, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return nil, err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(response)
if err != nil {
return nil, err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Length: response.ContentLength,
Handler: func(percent int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
},
},
}
}
size, err := util.SafeCopy(req.BackupFile, body)
if err != nil {
return nil, err
}
resp := BackupFileResponse{}
resp.Size = size
return &resp, nil
}
// CreateStorageVolumeBackupStream requests that Incus creates and returns new direct backup for
// the storage volume.
func (r *ProtocolIncus) CreateStorageVolumeBackupStream(pool string, volName string, backup api.StorageVolumeBackupsPost, req *BackupFileRequest) error {
if !r.HasExtension("direct_backup") {
return errors.New("The server is missing the required \"direct_backup\" API extension")
}
// Build the URL
uri := fmt.Sprintf("%s/1.0/storage-pools/%s/volumes/custom/%s/backups", r.httpBaseURL.String(), url.PathEscape(pool), url.PathEscape(volName))
if r.project != "" {
uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
}
// Encode the backup data
buf := bytes.Buffer{}
err := json.NewEncoder(&buf).Encode(backup)
if err != nil {
return err
}
// Prepare the download request
request, err := http.NewRequest("POST", uri, bytes.NewReader(buf.Bytes()))
if err != nil {
return err
}
request.Header.Set("Accept", "application/octet-stream")
if r.httpUserAgent != "" {
request.Header.Set("User-Agent", r.httpUserAgent)
}
// Start the request
response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
if err != nil {
return err
}
defer logger.WarnOnError(response.Body.Close, "Failed to close response body")
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err = incusParseResponse(response)
if err != nil {
return err
}
}
// Handle the data
body := response.Body
if req.ProgressHandler != nil {
body = &ioprogress.ProgressReader{
ReadCloser: response.Body,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
}
_, err = util.SafeCopy(req.BackupFile, body)
return err
}
// CreateStoragePoolVolumeFromMigration defines a new storage volume.
// In contrast to CreateStoragePoolVolume, it also returns an operation object.
func (r *ProtocolIncus) CreateStoragePoolVolumeFromMigration(pool string, volume api.StorageVolumesPost) (Operation, error) {
// Send the request
path := fmt.Sprintf("/storage-pools/%s/volumes/%s", url.PathEscape(pool), url.PathEscape(volume.Type))
op, _, err := r.queryOperation("POST", path, volume, "")
if err != nil {
return nil, err
}
return op, nil
}
// CreateStoragePoolVolumeFromISO creates a custom volume from an ISO file.
func (r *ProtocolIncus) CreateStoragePoolVolumeFromISO(pool string, args StorageVolumeBackupArgs) (Operation, error) {
err := r.CheckExtension("custom_volume_iso")
if err != nil {
return nil, err
}
if args.Name == "" {
return nil, errors.New("Missing volume name")
}
path := fmt.Sprintf("/storage-pools/%s/volumes/custom", url.PathEscape(pool))
// Prepare the HTTP request.
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", reqURL, args.BackupFile)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Incus-name", args.Name)
req.Header.Set("X-Incus-type", "iso")
// Send the request.
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
// Handle errors.
response, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
// Get to the operation.
respOperation, err := response.MetadataAsOperation()
if err != nil {
return nil, err
}
// Setup an Operation wrapper.
op := operation{
Operation: *respOperation,
r: r,
chActive: make(chan bool),
}
return &op, nil
}
// CreateStoragePoolVolumeFromBackup creates a custom volume from a backup file.
func (r *ProtocolIncus) CreateStoragePoolVolumeFromBackup(pool string, args StorageVolumeBackupArgs) (Operation, error) {
if !r.HasExtension("custom_volume_backup") {
return nil, errors.New(`The server is missing the required "custom_volume_backup" API extension`)
}
if args.Name != "" && !r.HasExtension("backup_override_name") {
return nil, errors.New(`The server is missing the required "backup_override_name" API extension`)
}
path := fmt.Sprintf("/storage-pools/%s/volumes/custom", url.PathEscape(pool))
// Prepare the HTTP request.
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", reqURL, args.BackupFile)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
if args.Name != "" {
req.Header.Set("X-Incus-name", args.Name)
}
// Send the request.
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer logger.WarnOnError(resp.Body.Close, "Failed to close response body")
// Handle errors.
response, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
// Get to the operation.
respOperation, err := response.MetadataAsOperation()
if err != nil {
return nil, err
}
// Setup an Operation wrapper.
op := operation{
Operation: *respOperation,
r: r,
chActive: make(chan bool),
}
return &op, nil
}
// GetStoragePoolVolumeBlockNBDConn returns a connection to the volume's NBD endpoint.
func (r *ProtocolIncus) GetStoragePoolVolumeBlockNBDConn(pool string, volType string, volName string, args StorageVolumeNBDPost) (net.Conn, error) {
if !r.HasExtension("storage_volume_nbd") {
return nil, errors.New(`The server is missing the required "storage_volume_nbd" API extension`)
}
u := api.NewURL()
u.URL = r.httpBaseURL // Preload the URL with the client base URL.
u.Path("1.0", "storage-pools", pool, "volumes", volType, volName, "nbd")
values := u.Query()
if args.Writable {
values.Set("writable", "1")
}
u.RawQuery = values.Encode()
r.setURLQueryAttributes(&u.URL)
return r.rawConn(http.MethodGet, &u.URL, "nbd", nil)
}
// GetStoragePoolVolumeFileSFTPConn returns a connection to the volume's SFTP endpoint.
func (r *ProtocolIncus) GetStoragePoolVolumeFileSFTPConn(pool string, volType string, volName string) (net.Conn, error) {
if !r.HasExtension("custom_volume_sftp") {
return nil, errors.New(`The server is missing the required "custom_volume_sftp" API extension`)
}
u := api.NewURL()
u.URL = r.httpBaseURL // Preload the URL with the client base URL.
u.Path("1.0", "storage-pools", pool, "volumes", volType, volName, "sftp")
r.setURLQueryAttributes(&u.URL)
return r.rawConn(http.MethodGet, &u.URL, "sftp", nil)
}
// GetStoragePoolVolumeFileSFTP returns an SFTP connection to the volume.
func (r *ProtocolIncus) GetStoragePoolVolumeFileSFTP(pool string, volType string, volName string) (*sftp.Client, error) {
if !r.HasExtension("custom_volume_sftp") {
return nil, errors.New(`The server is missing the required "custom_volume_sftp" API extension`)
}
conn, err := r.GetStoragePoolVolumeFileSFTPConn(pool, volType, volName)
if err != nil {
return nil, err
}
// Get a SFTP client.
client, err := sftp.NewClientPipe(conn, conn, sftp.MaxPacketUnchecked(128*1024))
if err != nil {
_ = conn.Close()
return nil, err
}
go func() {
// Wait for the client to be done before closing the connection.
_ = client.Wait()
_ = conn.Close()
}()
return client, nil
}
// GetStorageVolumeFile retrieves the provided path from the storage volume.
func (r *ProtocolIncus) GetStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string) (io.ReadCloser, *InstanceFileResponse, error) {
// Send the request
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/files?path=%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.QueryEscape(filePath),
)
requestURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, nil, err
}
}
// Parse the headers
uid, gid, mode, fileType, _ := api.ParseFileHeaders(resp.Header)
fileResp := InstanceFileResponse{
UID: uid,
GID: gid,
Mode: mode,
Type: fileType,
}
if fileResp.Type == "directory" {
// Decode the response
response := api.Response{}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&response)
if err != nil {
return nil, nil, err
}
// Get the file list
entries := []string{}
err = response.MetadataAsStruct(&entries)
if err != nil {
return nil, nil, err
}
fileResp.Entries = entries
return nil, &fileResp, err
}
return resp.Body, &fileResp, err
}
// CreateStorageVolumeFile tells Incus to create a file in the storage volume.
func (r *ProtocolIncus) CreateStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string, args InstanceFileArgs) error {
// Send the request
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/files?path=%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.QueryEscape(filePath),
)
requestURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
if err != nil {
return err
}
req, err := http.NewRequest("POST", requestURL, args.Content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
_, err := args.Content.Seek(0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(args.Content), nil
}
// Set the various headers
if args.UID > -1 {
req.Header.Set("X-Incus-uid", fmt.Sprintf("%d", args.UID))
}
if args.GID > -1 {
req.Header.Set("X-Incus-gid", fmt.Sprintf("%d", args.GID))
}
if args.Mode > -1 {
req.Header.Set("X-Incus-mode", fmt.Sprintf("%04o", args.Mode))
}
if args.Type != "" {
req.Header.Set("X-Incus-type", args.Type)
}
if args.WriteMode != "" {
req.Header.Set("X-Incus-write", args.WriteMode)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return err
}
// Check the return value for a cleaner error
_, _, err = incusParseResponse(resp)
if err != nil {
return err
}
return nil
}
// DeleteStorageVolumeFile deletes a file in the storage volume.
func (r *ProtocolIncus) DeleteStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string) error {
// Send the request
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/files?path=%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.QueryEscape(filePath),
)
requestURL, err := r.setQueryAttributes(path)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", requestURL, nil, "")
if err != nil {
return err
}
return nil
}
// GetStorageVolumeBitmapNames returns a list of volume bitmap names.
func (r *ProtocolIncus) GetStorageVolumeBitmapNames(pool string, volumeType string, volumeName string) ([]string, error) {
if !r.HasExtension("storage_volume_nbd") {
return nil, errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/bitmaps",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName),
)
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetStorageVolumeBitmaps returns a list of volume bitmaps.
func (r *ProtocolIncus) GetStorageVolumeBitmaps(pool string, volumeType string, volumeName string) ([]api.StorageVolumeBitmap, error) {
if !r.HasExtension("storage_volume_nbd") {
return nil, errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
bitmaps := []api.StorageVolumeBitmap{}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/bitmaps?recursion=1",
url.PathEscape(pool),
url.PathEscape(volumeType),
url.PathEscape(volumeName))
_, err := r.queryStruct("GET", path, nil, "", &bitmaps)
if err != nil {
return nil, err
}
return bitmaps, nil
}
// GetStorageVolumeBitmap returns information about a volume bitmap.
func (r *ProtocolIncus) GetStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmapName string) (*api.StorageVolumeBitmap, error) {
if !r.HasExtension("storage_volume_nbd") {
return nil, errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
bitmap := api.StorageVolumeBitmap{}
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/bitmaps/%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.PathEscape(bitmapName),
)
_, err := r.queryStruct("GET", path, nil, "", &bitmap)
if err != nil {
return nil, err
}
return &bitmap, nil
}
// CreateStorageVolumeBitmap creates a new volume bitmap.
func (r *ProtocolIncus) CreateStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmap api.StorageVolumeBitmapsPost) error {
if !r.HasExtension("storage_volume_nbd") {
return errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
path := fmt.Sprintf("/storage-pools/%s/volumes/%s/%s/bitmaps",
url.PathEscape(pool),
url.PathEscape(volumeType),
url.PathEscape(volumeName))
// Send the request
_, _, err := r.query("POST", path, bitmap, "")
if err != nil {
return err
}
return nil
}
// DeleteStorageVolumeBitmap deletes a volume bitmap.
func (r *ProtocolIncus) DeleteStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmapName string) error {
if !r.HasExtension("storage_volume_nbd") {
return errors.New("The server is missing the required \"storage_volume_nbd\" API extension")
}
path := fmt.Sprintf(
"/storage-pools/%s/volumes/%s/%s/bitmaps/%s",
url.PathEscape(pool), url.PathEscape(volumeType), url.PathEscape(volumeName), url.PathEscape(bitmapName),
)
_, _, err := r.query("DELETE", path, nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/incus_warnings.go 0000664 0000000 0000000 00000004404 15232704312 0017501 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"fmt"
"net/url"
"github.com/lxc/incus/v7/shared/api"
)
// Warning handling functions
// GetWarningUUIDs returns a list of operation uuids.
func (r *ProtocolIncus) GetWarningUUIDs() ([]string, error) {
if !r.HasExtension("warnings") {
return nil, errors.New("The server is missing the required \"warnings\" API extension")
}
// Fetch the raw values.
urls := []string{}
baseURL := "/warnings"
_, err := r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetWarnings returns a list of warnings.
func (r *ProtocolIncus) GetWarnings() ([]api.Warning, error) {
if !r.HasExtension("warnings") {
return nil, errors.New("The server is missing the required \"warnings\" API extension")
}
warnings := []api.Warning{}
_, err := r.queryStruct("GET", "/warnings?recursion=1", nil, "", &warnings)
if err != nil {
return nil, err
}
return warnings, nil
}
// GetWarning returns the warning with the given UUID.
func (r *ProtocolIncus) GetWarning(UUID string) (*api.Warning, string, error) {
if !r.HasExtension("warnings") {
return nil, "", errors.New("The server is missing the required \"warnings\" API extension")
}
warning := api.Warning{}
etag, err := r.queryStruct("GET", fmt.Sprintf("/warnings/%s", url.PathEscape(UUID)), nil, "", &warning)
if err != nil {
return nil, "", err
}
return &warning, etag, nil
}
// UpdateWarning updates the warning with the given UUID.
func (r *ProtocolIncus) UpdateWarning(UUID string, warning api.WarningPut, ETag string) error {
if !r.HasExtension("warnings") {
return errors.New("The server is missing the required \"warnings\" API extension")
}
// Send the request
_, _, err := r.query("PUT", fmt.Sprintf("/warnings/%s", url.PathEscape(UUID)), warning, ETag)
if err != nil {
return err
}
return nil
}
// DeleteWarning deletes the provided warning.
func (r *ProtocolIncus) DeleteWarning(UUID string) error {
if !r.HasExtension("warnings") {
return errors.New("The server is missing the required \"warnings\" API extension")
}
// Send the request
_, _, err := r.query("DELETE", fmt.Sprintf("/warnings/%s", url.PathEscape(UUID)), nil, "")
if err != nil {
return err
}
return nil
}
incus-7.3.0/client/interfaces.go 0000664 0000000 0000000 00000115510 15232704312 0016574 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"io"
"net"
"net/http"
"github.com/gorilla/websocket"
"github.com/pkg/sftp"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/cancel"
"github.com/lxc/incus/v7/shared/ioprogress"
)
// The Operation type represents a currently running operation.
type Operation interface {
AddHandler(function func(api.Operation)) (target *EventTarget, err error)
Cancel() (err error)
Get() (op api.Operation)
GetWebsocket(secret string) (conn *websocket.Conn, err error)
RemoveHandler(target *EventTarget) (err error)
Refresh() (err error)
Wait() (err error)
WaitContext(ctx context.Context) error
}
// The RemoteOperation type represents an Operation that may be using multiple servers.
type RemoteOperation interface {
AddHandler(function func(api.Operation)) (target *EventTarget, err error)
CancelTarget() (err error)
GetTarget() (op *api.Operation, err error)
Wait() (err error)
}
// The Server type represents a generic read-only server.
type Server interface {
GetConnectionInfo() (info *ConnectionInfo, err error)
GetHTTPClient() (client *http.Client, err error)
DoHTTP(req *http.Request) (resp *http.Response, err error)
Disconnect()
}
// The ImageServer type represents a read-only image server.
type ImageServer interface {
Server
// Image handling functions
GetImages() (images []api.Image, err error)
GetImagesAllProjects() (images []api.Image, err error)
GetImagesAllProjectsWithFilter(filters []string) (images []api.Image, err error)
GetImageFingerprints() (fingerprints []string, err error)
GetImagesWithFilter(filters []string) (images []api.Image, err error)
GetImage(fingerprint string) (image *api.Image, ETag string, err error)
GetImageFile(fingerprint string, req ImageFileRequest) (resp *ImageFileResponse, err error)
GetImageSecret(fingerprint string) (secret string, err error)
GetPrivateImage(fingerprint string, secret string) (image *api.Image, ETag string, err error)
GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (resp *ImageFileResponse, err error)
GetImageAliases() (aliases []api.ImageAliasesEntry, err error)
GetImageAliasNames() (names []string, err error)
GetImageAlias(name string) (alias *api.ImageAliasesEntry, ETag string, err error)
GetImageAliasType(imageType string, name string) (alias *api.ImageAliasesEntry, ETag string, err error)
GetImageAliasArchitectures(imageType string, name string) (entries map[string]*api.ImageAliasesEntry, err error)
ExportImage(fingerprint string, image api.ImageExportPost) (Operation, error)
}
// The InstanceServer type represents a full featured Incus server.
type InstanceServer interface {
ImageServer
// Server functions
GetMetrics() (metrics string, err error)
GetServer() (server *api.Server, ETag string, err error)
GetServerResources() (resources *api.Resources, err error)
UpdateServer(server api.ServerPut, ETag string) (err error)
ApplyServerPreseed(config api.InitPreseed) error
HasExtension(extension string) (exists bool)
RequireAuthenticated(authenticated bool)
IsClustered() (clustered bool)
UseTarget(name string) (client InstanceServer)
UseProject(name string) (client InstanceServer)
// Certificate functions
GetCertificateFingerprints() (fingerprints []string, err error)
GetCertificates() (certificates []api.Certificate, err error)
GetCertificatesWithFilter(filters []string) ([]api.Certificate, error)
GetCertificate(fingerprint string) (certificate *api.Certificate, ETag string, err error)
CreateCertificate(certificate api.CertificatesPost) (err error)
UpdateCertificate(fingerprint string, certificate api.CertificatePut, ETag string) (err error)
DeleteCertificate(fingerprint string) (err error)
CreateCertificateToken(certificate api.CertificatesPost) (op Operation, err error)
// Instance functions.
GetInstanceNames(instanceType api.InstanceType) (names []string, err error)
GetInstanceNamesAllProjects(instanceType api.InstanceType) (names map[string][]string, err error)
GetInstances(instanceType api.InstanceType) (instances []api.Instance, err error)
GetInstancesFull(instanceType api.InstanceType) (instances []api.InstanceFull, err error)
GetInstancesAllProjects(instanceType api.InstanceType) (instances []api.Instance, err error)
GetInstancesFullAllProjects(instanceType api.InstanceType) (instances []api.InstanceFull, err error)
GetInstancesWithFilter(instanceType api.InstanceType, filters []string) (instances []api.Instance, err error)
GetInstancesFullWithFilter(instanceType api.InstanceType, filters []string) (instances []api.InstanceFull, err error)
GetInstancesAllProjectsWithFilter(instanceType api.InstanceType, filters []string) (instances []api.Instance, err error)
GetInstancesFullAllProjectsWithFilter(instanceType api.InstanceType, filters []string) (instances []api.InstanceFull, err error)
GetInstance(name string) (instance *api.Instance, ETag string, err error)
GetInstanceFull(name string) (instance *api.InstanceFull, ETag string, err error)
CreateInstance(instance api.InstancesPost) (op Operation, err error)
CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (op RemoteOperation, err error)
CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (op RemoteOperation, err error)
UpdateInstance(name string, instance api.InstancePut, ETag string) (op Operation, err error)
RenameInstance(name string, instance api.InstancePost) (op Operation, err error)
MigrateInstance(name string, instance api.InstancePost) (op Operation, err error)
DeleteInstance(name string) (op Operation, err error)
UpdateInstances(state api.InstancesPut, ETag string) (op Operation, err error)
RebuildInstance(instanceName string, req api.InstanceRebuildPost) (op Operation, err error)
RebuildInstanceFromImage(source ImageServer, image api.Image, instanceName string, req api.InstanceRebuildPost) (op RemoteOperation, err error)
ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (op Operation, err error)
ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (op Operation, err error)
ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error)
CreateInstanceBitmap(name string, bitmap api.StorageVolumeBitmapsPost) error
GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (content io.ReadCloser, err error)
DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (err error)
GetInstanceFile(instanceName string, path string) (content io.ReadCloser, resp *InstanceFileResponse, err error)
CreateInstanceFile(instanceName string, path string, args InstanceFileArgs) (err error)
DeleteInstanceFile(instanceName string, path string) (err error)
GetInstanceFileSFTPConn(instanceName string) (net.Conn, error)
GetInstanceFileSFTP(instanceName string) (*sftp.Client, error)
GetInstanceNBDConn(instanceName string, args InstanceNBDArgs) (net.Conn, error)
GetInstancePortForwardConn(instanceName string, forward api.InstancePortForwardPost) (net.Conn, error)
GetInstanceSnapshotNames(instanceName string) (names []string, err error)
GetInstanceSnapshots(instanceName string) (snapshots []api.InstanceSnapshot, err error)
GetInstanceSnapshot(instanceName string, name string) (snapshot *api.InstanceSnapshot, ETag string, err error)
CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (op Operation, err error)
CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (op RemoteOperation, err error)
RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
DeleteInstanceSnapshot(instanceName string, name string) (op Operation, err error)
UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (op Operation, err error)
GetInstanceBackupNames(instanceName string) (names []string, err error)
GetInstanceBackups(instanceName string) (backups []api.InstanceBackup, err error)
GetInstanceBackup(instanceName string, name string) (backup *api.InstanceBackup, ETag string, err error)
CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (op Operation, err error)
RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (op Operation, err error)
DeleteInstanceBackup(instanceName string, name string) (op Operation, err error)
GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
CreateInstanceBackupStream(instanceName string, backup api.InstanceBackupsPost, req *BackupFileRequest) (err error)
CreateInstanceFromBackup(args InstanceBackupArgs) (op Operation, err error)
GetInstanceState(name string) (state *api.InstanceState, ETag string, err error)
UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (op Operation, err error)
GetInstanceAccess(name string) (access api.Access, err error)
GetInstanceLogfiles(name string) (logfiles []string, err error)
GetInstanceLogfile(name string, filename string) (content io.ReadCloser, err error)
DeleteInstanceLogfile(name string, filename string) (err error)
GetInstanceMetadata(name string) (metadata *api.ImageMetadata, ETag string, err error)
UpdateInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) (err error)
GetInstanceTemplateFiles(instanceName string) (templates []string, err error)
GetInstanceTemplateFile(instanceName string, templateName string) (content io.ReadCloser, err error)
CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
DeleteInstanceTemplateFile(name string, templateName string) (err error)
GetInstanceDebugMemory(name string, format string) (rc io.ReadCloser, err error)
RepairInstance(name string, repair api.InstanceDebugRepairPost) (err error)
GetInstanceNVRAM(name string) (vars map[string]map[string]*api.InstanceNVRAMVariable, err error)
GetInstanceNVRAMGUID(name string, guid string) (vars map[string]*api.InstanceNVRAMVariable, err error)
GetRawInstanceNVRAMGUIDVar(name string, guid string, varName string) (resp []byte, attributes uint32, err error)
GetInstanceNVRAMGUIDVar(name string, guid string, varName string) (resp *api.InstanceNVRAMVariable, ETag string, err error)
DeleteInstanceNVRAMGUIDVar(name string, guid string, varName string) error
UpdateRawInstanceNVRAMGUIDVar(name string, guid string, varName string, data []byte, attributes uint32, timestamp int64) error
UpdateInstanceNVRAMGUIDVar(name string, guid string, varName string, data api.InstanceNVRAMVariablePut, ETag string) error
// Event handling functions
GetEvents() (listener *EventListener, err error)
GetEventsByType(eventTypes []string) (listener *EventListener, err error)
GetEventsAllProjects() (listener *EventListener, err error)
GetEventsAllProjectsByType(eventTypes []string) (listener *EventListener, err error)
SendEvent(event api.Event) error
// Image functions
CreateImage(image api.ImagesPost, args *ImageCreateArgs) (op Operation, err error)
CopyImage(source ImageServer, image api.Image, args *ImageCopyArgs) (op RemoteOperation, err error)
UpdateImage(fingerprint string, image api.ImagePut, ETag string) (err error)
DeleteImage(fingerprint string) (op Operation, err error)
RefreshImage(fingerprint string) (op Operation, err error)
CreateImageSecret(fingerprint string) (op Operation, err error)
CreateImageAlias(alias api.ImageAliasesPost) (err error)
UpdateImageAlias(name string, alias api.ImageAliasesEntryPut, ETag string) (err error)
RenameImageAlias(name string, alias api.ImageAliasesEntryPost) (err error)
DeleteImageAlias(name string) (err error)
// Configuration metadata functions
GetMetadataConfiguration() (meta *api.MetadataConfiguration, err error)
// Network functions ("network" API extension)
GetNetworkNames() (names []string, err error)
GetNetworks() (networks []api.Network, err error)
GetNetworksWithFilter(filters []string) (networks []api.Network, err error)
GetNetworksAllProjects() (networks []api.Network, err error)
GetNetworksAllProjectsWithFilter(filters []string) (networks []api.Network, err error)
GetNetwork(name string) (network *api.Network, ETag string, err error)
GetNetworkLeases(name string) (leases []api.NetworkLease, err error)
GetNetworkState(name string) (state *api.NetworkState, err error)
CreateNetwork(network api.NetworksPost) (err error)
UpdateNetwork(name string, network api.NetworkPut, ETag string) (err error)
RenameNetwork(name string, network api.NetworkPost) (err error)
DeleteNetwork(name string) (err error)
// Network forward functions ("network_forward" API extension)
GetNetworkForwardAddresses(networkName string) ([]string, error)
GetNetworkForwards(networkName string) ([]api.NetworkForward, error)
GetNetworkForward(networkName string, listenAddress string) (forward *api.NetworkForward, ETag string, err error)
CreateNetworkForward(networkName string, forward api.NetworkForwardsPost) error
UpdateNetworkForward(networkName string, listenAddress string, forward api.NetworkForwardPut, ETag string) (err error)
DeleteNetworkForward(networkName string, listenAddress string) (err error)
// Network load balancer functions ("network_load_balancer" API extension)
GetNetworkLoadBalancerAddresses(networkName string) ([]string, error)
GetNetworkLoadBalancers(networkName string) ([]api.NetworkLoadBalancer, error)
GetNetworkLoadBalancer(networkName string, listenAddress string) (forward *api.NetworkLoadBalancer, ETag string, err error)
CreateNetworkLoadBalancer(networkName string, forward api.NetworkLoadBalancersPost) error
UpdateNetworkLoadBalancer(networkName string, listenAddress string, forward api.NetworkLoadBalancerPut, ETag string) (err error)
DeleteNetworkLoadBalancer(networkName string, listenAddress string) (err error)
GetNetworkLoadBalancerState(networkName string, listenAddress string) (lbState *api.NetworkLoadBalancerState, err error)
// Network peer functions ("network_peer" API extension)
GetNetworkPeerNames(networkName string) ([]string, error)
GetNetworkPeers(networkName string) ([]api.NetworkPeer, error)
GetNetworkPeer(networkName string, peerName string) (peer *api.NetworkPeer, ETag string, err error)
CreateNetworkPeer(networkName string, peer api.NetworkPeersPost) error
UpdateNetworkPeer(networkName string, peerName string, peer api.NetworkPeerPut, ETag string) (err error)
DeleteNetworkPeer(networkName string, peerName string) (err error)
// Network ACL functions ("network_acl" API extension)
GetNetworkACLNames() (names []string, err error)
GetNetworkACLs() (acls []api.NetworkACL, err error)
GetNetworkACLsAllProjects() (acls []api.NetworkACL, err error)
GetNetworkACL(name string) (acl *api.NetworkACL, ETag string, err error)
GetNetworkACLLogfile(name string) (log io.ReadCloser, err error)
CreateNetworkACL(acl api.NetworkACLsPost) (err error)
UpdateNetworkACL(name string, acl api.NetworkACLPut, ETag string) (err error)
RenameNetworkACL(name string, acl api.NetworkACLPost) (err error)
DeleteNetworkACL(name string) (err error)
// Network address set functions ("network_address_set" API extension)
GetNetworkAddressSetNames() (names []string, err error)
GetNetworkAddressSets() (AddressSets []api.NetworkAddressSet, err error)
GetNetworkAddressSetsAllProjects() (AddressSets []api.NetworkAddressSet, err error)
GetNetworkAddressSet(name string) (AddressSet *api.NetworkAddressSet, ETag string, err error)
CreateNetworkAddressSet(AddressSet api.NetworkAddressSetsPost) (err error)
UpdateNetworkAddressSet(name string, AddressSet api.NetworkAddressSetPut, ETag string) (err error)
RenameNetworkAddressSet(name string, AddressSet api.NetworkAddressSetPost) (err error)
DeleteNetworkAddressSet(name string) (err error)
// Network allocations functions ("network_allocations" API extension)
GetNetworkAllocations() (allocations []api.NetworkAllocations, err error)
GetNetworkAllocationsAllProjects() (allocations []api.NetworkAllocations, err error)
// Network zone functions ("network_dns" API extension)
GetNetworkZonesAllProjects() (zones []api.NetworkZone, err error)
GetNetworkZoneNames() (names []string, err error)
GetNetworkZones() (zones []api.NetworkZone, err error)
GetNetworkZone(name string) (zone *api.NetworkZone, ETag string, err error)
CreateNetworkZone(zone api.NetworkZonesPost) (err error)
UpdateNetworkZone(name string, zone api.NetworkZonePut, ETag string) (err error)
DeleteNetworkZone(name string) (err error)
GetNetworkZoneRecordNames(zone string) (names []string, err error)
GetNetworkZoneRecords(zone string) (records []api.NetworkZoneRecord, err error)
GetNetworkZoneRecord(zone string, name string) (record *api.NetworkZoneRecord, ETag string, err error)
CreateNetworkZoneRecord(zone string, record api.NetworkZoneRecordsPost) (err error)
UpdateNetworkZoneRecord(zone string, name string, record api.NetworkZoneRecordPut, ETag string) (err error)
DeleteNetworkZoneRecord(zone string, name string) (err error)
// Network integrations functions ("network_integrations" API extension)
GetNetworkIntegrationNames() (names []string, err error)
GetNetworkIntegrations() (integrations []api.NetworkIntegration, err error)
GetNetworkIntegration(name string) (integration *api.NetworkIntegration, ETag string, err error)
CreateNetworkIntegration(integration api.NetworkIntegrationsPost) (err error)
UpdateNetworkIntegration(name string, integration api.NetworkIntegrationPut, ETag string) (err error)
RenameNetworkIntegration(name string, integration api.NetworkIntegrationPost) (err error)
DeleteNetworkIntegration(name string) (err error)
// Operation functions
GetOperationUUIDs() (uuids []string, err error)
GetOperations() (operations []api.Operation, err error)
GetOperationsAllProjects() (operations []api.Operation, err error)
GetOperation(uuid string) (op *api.Operation, ETag string, err error)
GetOperationWait(uuid string, timeout int) (op *api.Operation, ETag string, err error)
GetOperationWaitSecret(uuid string, secret string, timeout int) (op *api.Operation, ETag string, err error)
GetOperationWebsocket(uuid string, secret string) (conn *websocket.Conn, err error)
DeleteOperation(uuid string) (err error)
// Profile functions
GetProfilesAllProjects() (profiles []api.Profile, err error)
GetProfilesAllProjectsWithFilter(filters []string) ([]api.Profile, error)
GetProfileNames() (names []string, err error)
GetProfiles() (profiles []api.Profile, err error)
GetProfilesWithFilter(filters []string) ([]api.Profile, error)
GetProfile(name string) (profile *api.Profile, ETag string, err error)
CreateProfile(profile api.ProfilesPost) (err error)
UpdateProfile(name string, profile api.ProfilePut, ETag string) (err error)
RenameProfile(name string, profile api.ProfilePost) (err error)
DeleteProfile(name string) (err error)
// Project functions
GetProjectNames() (names []string, err error)
GetProjects() (projects []api.Project, err error)
GetProjectsWithFilter(filters []string) (projects []api.Project, err error)
GetProject(name string) (project *api.Project, ETag string, err error)
GetProjectState(name string) (project *api.ProjectState, err error)
GetProjectAccess(name string) (access api.Access, err error)
CreateProject(project api.ProjectsPost) (err error)
UpdateProject(name string, project api.ProjectPut, ETag string) (err error)
RenameProject(name string, project api.ProjectPost) (op Operation, err error)
DeleteProject(name string) (err error)
DeleteProjectForce(name string) (err error)
// Storage pool functions ("storage" API extension)
GetStoragePoolNames() (names []string, err error)
GetStoragePools() (pools []api.StoragePool, err error)
GetStoragePoolsWithFilter(filters []string) ([]api.StoragePool, error)
GetStoragePool(name string) (pool *api.StoragePool, ETag string, err error)
GetStoragePoolResources(name string) (resources *api.ResourcesStoragePool, err error)
CreateStoragePool(pool api.StoragePoolsPost) (err error)
UpdateStoragePool(name string, pool api.StoragePoolPut, ETag string) (err error)
DeleteStoragePool(name string) (err error)
// Storage bucket functions ("storage_buckets" API extension)
GetStoragePoolBucketNames(poolName string) ([]string, error)
GetStoragePoolBucketsAllProjects(poolName string) ([]api.StorageBucket, error)
GetStoragePoolBucketsWithFilterAllProjects(poolName string, filters []string) (bucket []api.StorageBucket, err error)
GetStoragePoolBuckets(poolName string) ([]api.StorageBucket, error)
GetStoragePoolBucketsWithFilter(poolName string, filters []string) (bucket []api.StorageBucket, err error)
GetStoragePoolBucketsFullAllProjects(poolName string) ([]api.StorageBucketFull, error)
GetStoragePoolBucketsFullWithFilterAllProjects(poolName string, filters []string) (bucket []api.StorageBucketFull, err error)
GetStoragePoolBucketsFull(poolName string) ([]api.StorageBucketFull, error)
GetStoragePoolBucketsFullWithFilter(poolName string, filters []string) (bucket []api.StorageBucketFull, err error)
GetStoragePoolBucket(poolName string, bucketName string) (bucket *api.StorageBucket, ETag string, err error)
GetStoragePoolBucketFull(poolName string, bucketName string) (bucket *api.StorageBucketFull, ETag string, err error)
CreateStoragePoolBucket(poolName string, bucket api.StorageBucketsPost) (*api.StorageBucketKey, error)
UpdateStoragePoolBucket(poolName string, bucketName string, bucket api.StorageBucketPut, ETag string) (err error)
DeleteStoragePoolBucket(poolName string, bucketName string) (err error)
GetStoragePoolBucketKeyNames(poolName string, bucketName string) ([]string, error)
GetStoragePoolBucketKeys(poolName string, bucketName string) ([]api.StorageBucketKey, error)
GetStoragePoolBucketKey(poolName string, bucketName string, keyName string) (key *api.StorageBucketKey, ETag string, err error)
CreateStoragePoolBucketKey(poolName string, bucketName string, key api.StorageBucketKeysPost) (newKey *api.StorageBucketKey, err error)
UpdateStoragePoolBucketKey(poolName string, bucketName string, keyName string, key api.StorageBucketKeyPut, ETag string) (err error)
DeleteStoragePoolBucketKey(poolName string, bucketName string, keyName string) (err error)
// Storage bucket backup functions ("storage_bucket_backup" API extension)
CreateStoragePoolBucketBackup(poolName string, bucketName string, backup api.StorageBucketBackupsPost) (op Operation, err error)
DeleteStoragePoolBucketBackup(pool string, bucketName string, name string) (op Operation, err error)
GetStoragePoolBucketBackupFile(pool string, bucketName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
CreateStoragePoolBucketBackupStream(pool string, bucketName string, backup api.StorageBucketBackupsPost, req *BackupFileRequest) (err error)
CreateStoragePoolBucketFromBackup(pool string, args StoragePoolBucketBackupArgs) (op Operation, err error)
// Storage volume functions ("storage" API extension)
GetStoragePoolVolumeNames(pool string) (names []string, err error)
GetStoragePoolVolumeNamesAllProjects(pool string) (names map[string][]string, err error)
GetStoragePoolVolumes(pool string) (volumes []api.StorageVolume, err error)
GetStoragePoolVolumesAllProjects(pool string) (volumes []api.StorageVolume, err error)
GetStoragePoolVolumesWithFilter(pool string, filters []string) (volumes []api.StorageVolume, err error)
GetStoragePoolVolumesWithFilterAllProjects(pool string, filters []string) (volumes []api.StorageVolume, err error)
GetStoragePoolVolumesFull(pool string) (volumes []api.StorageVolumeFull, err error)
GetStoragePoolVolumesFullAllProjects(pool string) (volumes []api.StorageVolumeFull, err error)
GetStoragePoolVolumesFullWithFilter(pool string, filters []string) (volumes []api.StorageVolumeFull, err error)
GetStoragePoolVolumesFullWithFilterAllProjects(pool string, filters []string) (volumes []api.StorageVolumeFull, err error)
GetStoragePoolVolume(pool string, volType string, name string) (volume *api.StorageVolume, ETag string, err error)
GetStoragePoolVolumeFull(pool string, volType string, name string) (volume *api.StorageVolumeFull, ETag string, err error)
GetStoragePoolVolumeState(pool string, volType string, name string) (state *api.StorageVolumeState, err error)
CreateStoragePoolVolume(pool string, volume api.StorageVolumesPost) (err error)
UpdateStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePut, ETag string) (err error)
DeleteStoragePoolVolume(pool string, volType string, name string) (err error)
RenameStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePost) (err error)
CopyStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (op RemoteOperation, err error)
MoveStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (op RemoteOperation, err error)
MigrateStoragePoolVolume(pool string, volume api.StorageVolumePost) (op Operation, err error)
// Storage volume rebuild ("storage_volumes_rebuild" API extension)
RebuildStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumeRebuildPost) (op Operation, err error)
// Storage volume snapshot functions ("storage_api_volume_snapshots" API extension)
CreateStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshot api.StorageVolumeSnapshotsPost) (op Operation, err error)
DeleteStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string) (op Operation, err error)
GetStoragePoolVolumeSnapshotNames(pool string, volumeType string, volumeName string) (names []string, err error)
GetStoragePoolVolumeSnapshots(pool string, volumeType string, volumeName string) (snapshots []api.StorageVolumeSnapshot, err error)
GetStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string) (snapshot *api.StorageVolumeSnapshot, ETag string, err error)
RenameStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string, snapshot api.StorageVolumeSnapshotPost) (op Operation, err error)
UpdateStoragePoolVolumeSnapshot(pool string, volumeType string, volumeName string, snapshotName string, volume api.StorageVolumeSnapshotPut, ETag string) (err error)
// Storage volume backup functions ("custom_volume_backup" API extension)
GetStorageVolumeBackupNames(pool string, volName string) (names []string, err error)
GetStorageVolumeBackups(pool string, volName string) (backups []api.StorageVolumeBackup, err error)
GetStorageVolumeBackup(pool string, volName string, name string) (backup *api.StorageVolumeBackup, ETag string, err error)
CreateStorageVolumeBackup(pool string, volName string, backup api.StorageVolumeBackupsPost) (op Operation, err error)
RenameStorageVolumeBackup(pool string, volName string, name string, backup api.StorageVolumeBackupPost) (op Operation, err error)
DeleteStorageVolumeBackup(pool string, volName string, name string) (op Operation, err error)
GetStorageVolumeBackupFile(pool string, volName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
CreateStorageVolumeBackupStream(pool string, volName string, backup api.StorageVolumeBackupsPost, req *BackupFileRequest) (err error)
CreateStoragePoolVolumeFromBackup(pool string, args StorageVolumeBackupArgs) (op Operation, err error)
// Storage volume bitmaps manipulations functions ("storage_volume_nbd" API extension)
GetStorageVolumeBitmapNames(pool string, volumeType string, volumeName string) ([]string, error)
GetStorageVolumeBitmaps(pool string, volumeType string, volumeName string) ([]api.StorageVolumeBitmap, error)
GetStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmapName string) (bitmap *api.StorageVolumeBitmap, err error)
CreateStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmap api.StorageVolumeBitmapsPost) error
DeleteStorageVolumeBitmap(pool string, volumeType string, volumeName string, bitmapName string) error
// Storage volume ISO import function ("custom_volume_iso" API extension)
CreateStoragePoolVolumeFromISO(pool string, args StorageVolumeBackupArgs) (op Operation, err error)
CreateStoragePoolVolumeFromMigration(pool string, volume api.StorageVolumesPost) (op Operation, err error)
// Storage volume file manipulations functions ("file_storage_volume" API extension)
GetStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string) (content io.ReadCloser, resp *InstanceFileResponse, err error)
CreateStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string, args InstanceFileArgs) (err error)
DeleteStorageVolumeFile(pool string, volumeType string, volumeName string, filePath string) (err error)
// Storage volume NBD functions ("storage_volume_nbd" API extension)
GetStoragePoolVolumeBlockNBDConn(pool string, volType string, volName string, args StorageVolumeNBDPost) (net.Conn, error)
// Storage volume SFTP functions ("custom_volume_sftp" API extension)
GetStoragePoolVolumeFileSFTPConn(pool string, volType string, volName string) (net.Conn, error)
GetStoragePoolVolumeFileSFTP(pool string, volType string, volName string) (*sftp.Client, error)
// Cluster functions ("cluster" API extensions)
GetCluster() (cluster *api.Cluster, ETag string, err error)
UpdateCluster(cluster api.ClusterPut, ETag string) (op Operation, err error)
DeleteClusterMember(name string, force bool) (err error)
DeletePendingClusterMember(name string, force bool) (err error)
GetClusterMemberNames() (names []string, err error)
GetClusterMembers() (members []api.ClusterMember, err error)
GetClusterMembersWithFilter(filters []string) ([]api.ClusterMember, error)
GetClusterMember(name string) (member *api.ClusterMember, ETag string, err error)
UpdateClusterMember(name string, member api.ClusterMemberPut, ETag string) (err error)
RenameClusterMember(name string, member api.ClusterMemberPost) (err error)
CreateClusterMember(member api.ClusterMembersPost) (op Operation, err error)
UpdateClusterCertificate(certs api.ClusterCertificatePut, ETag string) (err error)
GetClusterMemberState(name string) (*api.ClusterMemberState, string, error)
UpdateClusterMemberState(name string, state api.ClusterMemberStatePost) (op Operation, err error)
GetClusterGroups() ([]api.ClusterGroup, error)
GetClusterGroupNames() ([]string, error)
RenameClusterGroup(name string, group api.ClusterGroupPost) error
CreateClusterGroup(group api.ClusterGroupsPost) error
DeleteClusterGroup(name string) error
UpdateClusterGroup(name string, group api.ClusterGroupPut, ETag string) error
GetClusterGroup(name string) (*api.ClusterGroup, string, error)
// Warning functions
GetWarningUUIDs() (uuids []string, err error)
GetWarnings() (warnings []api.Warning, err error)
GetWarning(UUID string) (warning *api.Warning, ETag string, err error)
UpdateWarning(UUID string, warning api.WarningPut, ETag string) (err error)
DeleteWarning(UUID string) (err error)
// Internal functions (for internal use)
RawQuery(method string, path string, data any, queryETag string) (resp *api.Response, ETag string, err error)
RawWebsocket(path string) (conn *websocket.Conn, err error)
RawOperation(method string, path string, data any, queryETag string) (op Operation, ETag string, err error)
}
// The ConnectionInfo struct represents general information for a connection.
type ConnectionInfo struct {
Addresses []string
Certificate string
Protocol string
URL string
SocketPath string
Project string
Target string
}
// The BackupFileRequest struct is used for a backup download request.
type BackupFileRequest struct {
// Writer for the backup file
BackupFile io.WriteSeeker
// Progress handler (called whenever some progress is made)
ProgressHandler func(progress ioprogress.ProgressData)
// A canceler that can be used to interrupt some part of the image download request
Canceler *cancel.HTTPRequestCanceller
}
// The BackupFileResponse struct is used as the response for backup downloads.
type BackupFileResponse struct {
// Size of backup file
Size int64
}
// The ImageCreateArgs struct is used for direct image upload.
type ImageCreateArgs struct {
// Reader for the meta file
MetaFile io.Reader
// Filename for the meta file
MetaName string
// Reader for the rootfs file
RootfsFile io.Reader
// Filename for the rootfs file
RootfsName string
// Progress handler (called with upload progress)
ProgressHandler func(progress ioprogress.ProgressData)
// Type of the image (container or virtual-machine)
Type string
}
// The ImageFileRequest struct is used for an image download request.
type ImageFileRequest struct {
// Writer for the metadata file
MetaFile io.ReadWriteSeeker
// Writer for the rootfs file
RootfsFile io.ReadWriteSeeker
// Progress handler (called whenever some progress is made)
ProgressHandler func(progress ioprogress.ProgressData)
// A canceler that can be used to interrupt some part of the image download request
Canceler *cancel.HTTPRequestCanceller
// Path retriever for image delta downloads
// If set, it must return the path to the image file or an empty string if not available
DeltaSourceRetriever func(fingerprint string, file string) string
}
// The ImageFileResponse struct is used as the response for image downloads.
type ImageFileResponse struct {
// Filename for the metadata file
MetaName string
// Size of the metadata file
MetaSize int64
// Filename for the rootfs file
RootfsName string
// Size of the rootfs file
RootfsSize int64
}
// The ImageCopyArgs struct is used to pass additional options during image copy.
type ImageCopyArgs struct {
// Aliases to add to the copied image.
Aliases []api.ImageAlias
// Whether to have Incus keep this image up to date
AutoUpdate bool
// Whether to copy the source image aliases to the target
CopyAliases bool
// Whether this image is to be made available to unauthenticated users
Public bool
// The image type to use for resolution
Type string
// The transfer mode, can be "pull" (default), "push" or "relay"
Mode string
// List of profiles to apply on the target.
Profiles []string
}
// The StoragePoolVolumeCopyArgs struct is used to pass additional options
// during storage volume copy.
type StoragePoolVolumeCopyArgs struct {
// New name for the target
Name string
// The transfer mode, can be "pull" (default), "push" or "relay"
Mode string
// API extension: storage_api_volume_snapshots
VolumeOnly bool
// API extension: custom_volume_refresh
Refresh bool
// API extension: custom_volume_refresh_exclude_older_snapshots
RefreshExcludeOlder bool
}
// The StoragePoolVolumeMoveArgs struct is used to pass additional options
// during storage volume move.
type StoragePoolVolumeMoveArgs struct {
StoragePoolVolumeCopyArgs
// API extension: storage_volume_project_move
Project string
}
// The StorageVolumeBackupArgs struct is used when creating a storage volume from a backup.
// API extension: custom_volume_backup.
type StorageVolumeBackupArgs struct {
// The backup file
BackupFile io.Reader
// Name to import backup as
Name string
}
// The InstanceBackupArgs struct is used when creating a instance from a backup.
type InstanceBackupArgs struct {
// The backup file
BackupFile io.Reader
// Storage pool to use
PoolName string
// Name to import backup as
Name string
// Config overrides.
Config []string
// Device overrides.
Devices []string
}
// The InstanceCopyArgs struct is used to pass additional options during instance copy.
type InstanceCopyArgs struct {
// If set, the instance will be renamed on copy
Name string
// If set, the instance running state will be transferred (live migration)
Live bool
// If set, only the instance will copied, its snapshots won't
InstanceOnly bool
// The transfer mode, can be "pull" (default), "push" or "relay"
Mode string
// API extension: container_incremental_copy
// Perform an incremental copy
Refresh bool
// API extension: custom_volume_refresh_exclude_older_snapshots
RefreshExcludeOlder bool
// API extension: instance_allow_inconsistent_copy
AllowInconsistent bool
}
// The InstanceSnapshotCopyArgs struct is used to pass additional options during instance copy.
type InstanceSnapshotCopyArgs struct {
// If set, the instance will be renamed on copy
Name string
// The transfer mode, can be "pull" (default), "push" or "relay"
Mode string
// API extension: container_snapshot_stateful_migration
// If set, the instance running state will be transferred (live migration)
Live bool
}
// The InstanceConsoleArgs struct is used to pass additional options during a
// instance console session.
type InstanceConsoleArgs struct {
// Bidirectional fd to pass to the instance
Terminal io.ReadWriteCloser
// Control message handler (window resize)
Control func(conn *websocket.Conn)
// Closing this Channel causes a disconnect from the instance's console
ConsoleDisconnect chan bool
}
// The InstanceConsoleLogArgs struct is used to pass additional options during a
// instance console log request.
type InstanceConsoleLogArgs struct{}
// The InstanceExecArgs struct is used to pass additional options during instance exec.
type InstanceExecArgs struct {
// Standard input
Stdin io.Reader
// Standard output
Stdout io.Writer
// Standard error
Stderr io.Writer
// Control message handler (window resize, signals, ...)
Control func(conn *websocket.Conn)
// Channel that will be closed when all data operations are done
DataDone chan bool
}
// The InstanceFileArgs struct is used to pass the various options for a instance file upload.
type InstanceFileArgs struct {
// File content
Content io.ReadSeeker
// User id that owns the file
UID int64
// Group id that owns the file
GID int64
// File permissions
Mode int
// File type (file or directory)
Type string
// File write mode (overwrite or append)
WriteMode string
}
// The InstanceNBDArgs struct is used when connecting to an instance's disks over NBD.
// API extension: instance_nbd.
type InstanceNBDArgs struct {
// Whether to connect to an already running NBD session
Reuse bool
}
// The InstanceFileResponse struct is used as part of the response for a instance file download.
type InstanceFileResponse struct {
// User id that owns the file
UID int64
// Group id that owns the file
GID int64
// File permissions
Mode int
// File type (file or directory)
Type string
// If a directory, the list of files inside it
Entries []string
}
// The StoragePoolBucketBackupArgs struct is used when creating a storage volume from a backup.
// API extension: storage_bucket_backup.
type StoragePoolBucketBackupArgs struct {
// The backup file
BackupFile io.Reader
// Name to import backup as
Name string
}
// The StorageVolumeNBDPost struct is used when connecting to a storage volume over NBD.
// API extension: storage_volume_nbd.
type StorageVolumeNBDPost struct {
// Writable
Writable bool
}
incus-7.3.0/client/oci.go 0000664 0000000 0000000 00000002410 15232704312 0015215 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"net/http"
)
// ProtocolOCI implements an OCI registry API client.
type ProtocolOCI struct {
http *http.Client
httpHost string
httpUserAgent string
httpCertificate string
// Cache for images.
cache map[string]ociInfo
// Error tracking for images.
errors map[string]error
tempPath string
}
// Disconnect is a no-op for OCI.
func (r *ProtocolOCI) Disconnect() {
}
// GetConnectionInfo returns the basic connection information used to interact with the server.
func (r *ProtocolOCI) GetConnectionInfo() (*ConnectionInfo, error) {
info := ConnectionInfo{}
info.Addresses = []string{r.httpHost}
info.Certificate = r.httpCertificate
info.Protocol = "oci"
info.URL = r.httpHost
return &info, nil
}
// GetHTTPClient returns the http client used for the connection. This can be used to set custom http options.
func (r *ProtocolOCI) GetHTTPClient() (*http.Client, error) {
if r.http == nil {
return nil, errors.New("HTTP client isn't set, bad connection")
}
return r.http, nil
}
// DoHTTP performs a Request.
func (r *ProtocolOCI) DoHTTP(req *http.Request) (*http.Response, error) {
// Set the user agent.
if r.httpUserAgent != "" {
req.Header.Set("User-Agent", r.httpUserAgent)
}
return r.http.Do(req)
}
incus-7.3.0/client/oci_images.go 0000664 0000000 0000000 00000033566 15232704312 0016562 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/klauspost/pgzip"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/archive"
"github.com/lxc/incus/v7/shared/ioprogress"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/osarch"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
type ociInfo struct {
Alias string
Name string `json:"Name"`
Digest string `json:"Digest"`
Created time.Time `json:"Created"`
Architecture string `json:"Architecture"`
Layers []string `json:"Layers"`
LayersData []struct {
Size int64 `json:"Size"`
} `json:"LayersData"`
}
// Get the proxy host value.
func (r *ProtocolOCI) getProxyHost() (*url.URL, error) {
req, err := http.NewRequest("GET", r.httpHost, nil)
if err != nil {
return nil, err
}
proxy, err := r.http.Transport.(*http.Transport).Proxy(req)
if err != nil {
return nil, err
}
return proxy, nil
}
// Image handling functions
// GetImages returns a list of available images as Image structs.
func (r *ProtocolOCI) GetImages() ([]api.Image, error) {
return nil, errors.New("Can't list images from OCI registry")
}
// GetImagesAllProjects returns a list of available images as Image structs.
func (r *ProtocolOCI) GetImagesAllProjects() ([]api.Image, error) {
return nil, errors.New("Can't list images from OCI registry")
}
// GetImagesAllProjectsWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolOCI) GetImagesAllProjectsWithFilter(filters []string) ([]api.Image, error) {
return nil, errors.New("Can't list images from OCI registry")
}
// GetImageFingerprints returns a list of available image fingerprints.
func (r *ProtocolOCI) GetImageFingerprints() ([]string, error) {
return nil, errors.New("Can't list images from OCI registry")
}
// GetImagesWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolOCI) GetImagesWithFilter(_ []string) ([]api.Image, error) {
return nil, errors.New("Can't list images from OCI registry")
}
// GetImage returns an Image struct for the provided fingerprint.
func (r *ProtocolOCI) GetImage(fingerprint string) (*api.Image, string, error) {
info, ok := r.cache[fingerprint]
if !ok {
_, err := exec.LookPath("skopeo")
if err != nil {
return nil, "", errors.New("OCI container handling requires \"skopeo\" be present on the system")
}
err, ok := r.errors[fingerprint]
if ok {
return nil, "", err
}
return nil, "", errors.New("Image not found")
}
img := api.Image{
ImagePut: api.ImagePut{
Public: true,
Properties: map[string]string{
"architecture": info.Architecture,
"type": "oci",
"description": fmt.Sprintf("%s (OCI)", info.Name),
"id": info.Alias,
},
},
Aliases: []api.ImageAlias{{
Name: info.Alias,
}},
Architecture: info.Architecture,
Fingerprint: fingerprint,
Type: string(api.InstanceTypeContainer),
CreatedAt: info.Created,
UploadedAt: info.Created,
}
var size int64
for _, layer := range info.LayersData {
size += layer.Size
}
img.Size = size
return &img, "", nil
}
// GetImageFile downloads an image from the server, returning an ImageFileResponse struct.
func (r *ProtocolOCI) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
ctx := context.Background()
// Get the cached entry.
info, ok := r.cache[fingerprint]
if !ok {
_, err := exec.LookPath("skopeo")
if err != nil {
return nil, errors.New("OCI container handling requires \"skopeo\" be present on the system")
}
err, ok := r.errors[fingerprint]
if ok {
return nil, err
}
return nil, errors.New("Image not found")
}
// Quick checks.
if req.MetaFile == nil && req.RootfsFile == nil {
return nil, errors.New("No file requested")
}
if os.Geteuid() != 0 {
return nil, errors.New("OCI image export currently requires root access")
}
// Get some temporary storage.
ociPath, err := os.MkdirTemp(r.tempPath, "incus-oci-")
if err != nil {
return nil, err
}
defer logger.WarnOnError(func() error { return os.RemoveAll(ociPath) }, "Failed to remove temporary directory")
err = os.Mkdir(filepath.Join(ociPath, "oci"), 0o700)
if err != nil {
return nil, err
}
err = os.Mkdir(filepath.Join(ociPath, "image"), 0o700)
if err != nil {
return nil, err
}
// Copy the image.
if req.ProgressHandler != nil {
req.ProgressHandler(ioprogress.ProgressData{Text: "Retrieving OCI image from registry"})
}
imageTag := "latest"
stdout, err := r.runSkopeo(
"copy", info.Alias,
"--remove-signatures",
fmt.Sprintf("oci:%s:%s", filepath.Join(ociPath, "oci"), imageTag),
)
if err != nil {
logger.Debug("Error copying remote image to local", logger.Ctx{"image": info.Alias, "stdout": stdout, "stderr": err})
return nil, err
}
// Convert to something usable.
if req.ProgressHandler != nil {
req.ProgressHandler(ioprogress.ProgressData{Text: "Unpacking the OCI image"})
}
err = unpackOCIImage(filepath.Join(ociPath, "oci"), imageTag, filepath.Join(ociPath, "image"))
if err != nil {
logger.Debug("Error unpacking OCI image", logger.Ctx{"image": filepath.Join(ociPath, "oci"), "err": err})
return nil, err
}
// Generate a metadata.yaml.
if req.ProgressHandler != nil {
req.ProgressHandler(ioprogress.ProgressData{Text: "Generating image metadata"})
}
metadata := api.ImageMetadata{
Architecture: info.Architecture,
CreationDate: info.Created.Unix(),
}
data, err := json.Marshal(metadata)
if err != nil {
return nil, err
}
err = os.WriteFile(filepath.Join(ociPath, "image", "metadata.yaml"), data, 0o644)
if err != nil {
return nil, err
}
// Prepare response.
resp := &ImageFileResponse{
MetaName: "metadata.tar.gz",
RootfsName: "rootfs.tar.gz",
}
// Prepare to push the tarballs.
var pipeRead io.ReadCloser
var pipeWrite io.WriteCloser
// Push the metadata tarball.
pipeRead, pipeWrite = io.Pipe()
defer logger.WarnOnError(pipeRead.Close, "Failed to close pipe reader")
defer logger.WarnOnError(pipeWrite.Close, "Failed to close pipe writer")
if req.ProgressHandler != nil {
pipeRead = &ioprogress.ProgressReader{
ReadCloser: pipeRead,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("Generating metadata tarball: %s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
}
compressWrite := pgzip.NewWriter(pipeWrite)
err = compressWrite.SetConcurrency(1<<20, archive.CompressionThreads())
if err != nil {
return nil, err
}
metadataProcess := subprocess.NewProcessWithFds("tar", []string{"-cf", "-", "-C", filepath.Join(ociPath, "image"), "config.json", "metadata.yaml"}, nil, compressWrite, os.Stderr)
err = metadataProcess.Start(ctx)
if err != nil {
return nil, err
}
go func() {
_, _ = metadataProcess.Wait(ctx)
_ = compressWrite.Close()
_ = pipeWrite.Close()
}()
size, err := util.SafeCopy(req.MetaFile, pipeRead)
if err != nil {
return nil, err
}
resp.MetaSize = size
// Push the rootfs tarball.
pipeRead, pipeWrite = io.Pipe()
defer logger.WarnOnError(pipeRead.Close, "Failed to close pipe reader")
defer logger.WarnOnError(pipeWrite.Close, "Failed to close pipe writer")
if req.ProgressHandler != nil {
pipeRead = &ioprogress.ProgressReader{
ReadCloser: pipeRead,
Tracker: &ioprogress.ProgressTracker{
Handler: func(received int64, speed int64) {
req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("Generating rootfs tarball: %s (%s/s)", units.GetByteSizeString(received, 2), units.GetByteSizeString(speed, 2))})
},
},
}
}
compressWrite = pgzip.NewWriter(pipeWrite)
err = compressWrite.SetConcurrency(1<<20, archive.CompressionThreads())
if err != nil {
return nil, err
}
rootfsProcess := subprocess.NewProcessWithFds("tar", []string{"-cf", "-", "-C", filepath.Join(ociPath, "image", "rootfs"), "."}, nil, compressWrite, nil)
err = rootfsProcess.Start(ctx)
if err != nil {
return nil, err
}
go func() {
_, _ = rootfsProcess.Wait(ctx)
_ = compressWrite.Close()
_ = pipeWrite.Close()
}()
size, err = util.SafeCopy(req.RootfsFile, pipeRead)
if err != nil {
return nil, err
}
resp.RootfsSize = size
return resp, nil
}
// GetImageSecret isn't relevant for the simplestreams protocol.
func (r *ProtocolOCI) GetImageSecret(_ string) (string, error) {
return "", errors.New("Private images aren't supported with OCI registry")
}
// GetPrivateImage isn't relevant for the simplestreams protocol.
func (r *ProtocolOCI) GetPrivateImage(_ string, _ string) (*api.Image, string, error) {
return nil, "", errors.New("Private images aren't supported with OCI registry")
}
// GetPrivateImageFile isn't relevant for the simplestreams protocol.
func (r *ProtocolOCI) GetPrivateImageFile(_ string, _ string, _ ImageFileRequest) (*ImageFileResponse, error) {
return nil, errors.New("Private images aren't supported with OCI registry")
}
// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs.
func (r *ProtocolOCI) GetImageAliases() ([]api.ImageAliasesEntry, error) {
return nil, errors.New("Can't list image aliases from OCI registry")
}
// GetImageAliasNames returns the list of available alias names.
func (r *ProtocolOCI) GetImageAliasNames() ([]string, error) {
return nil, errors.New("Can't list image aliases from OCI registry")
}
func (r *ProtocolOCI) runSkopeo(action string, image string, args ...string) (string, error) {
// Parse and mangle the server URL.
uri, err := url.Parse(r.httpHost)
if err != nil {
return "", err
}
// Get proxy details.
proxy, err := r.getProxyHost()
if err != nil {
return "", err
}
var env []string
if proxy != nil {
env = []string{
fmt.Sprintf("HTTPS_PROXY=%s", proxy),
fmt.Sprintf("HTTP_PROXY=%s", proxy),
}
}
// Handle authentication.
if uri.User != nil {
creds, err := json.Marshal(map[string]any{
"auths": map[string]any{
uri.Scheme + "://" + uri.Host: map[string]string{
"auth": base64.StdEncoding.EncodeToString([]byte(uri.User.String())),
},
},
})
if err != nil {
return "", err
}
authFile, err := os.CreateTemp(r.tempPath, "incus_client_auth_")
if err != nil {
return "", err
}
defer logger.WarnOnError(authFile.Close, "Failed to close auth file")
defer os.Remove(authFile.Name())
err = authFile.Chmod(0o600)
if err != nil {
return "", err
}
_, err = fmt.Fprintf(authFile, "%s", creds)
if err != nil {
return "", err
}
uri.User = nil
args = append(args, fmt.Sprintf("--authfile=%s", authFile.Name()))
}
// Prepare the arguments.
uri.Scheme = "docker"
args = append([]string{"--insecure-policy", action, fmt.Sprintf("%s/%s", uri.String(), image)}, args...)
// Get the image information from skopeo.
stdout, _, err := subprocess.RunCommandSplit(
context.TODO(),
env,
nil,
"skopeo",
args...,
)
if err != nil {
return "", err
}
return stdout, nil
}
// GetImageAlias returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolOCI) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
// If image name is "IMAGE:TAG@HASH", drop ":TAG" so that skopeo uses the pinned hash instead.
imageWithoutHash, hash, hasHash := strings.Cut(name, "@")
if hasHash {
imageWithoutTag, _, _ := strings.Cut(imageWithoutHash, ":")
name = fmt.Sprintf("%s@%s", imageWithoutTag, hash)
}
// Get the image information from skopeo.
stdout, err := r.runSkopeo("inspect", name, "--no-tags")
if err != nil {
logger.Debug("Error getting image alias", logger.Ctx{"name": name, "stdout": stdout, "stderr": err})
r.errors[name] = err
return nil, "", err
}
// Parse the image info.
var info ociInfo
err = json.Unmarshal([]byte(stdout), &info)
if err != nil {
r.errors[name] = err
return nil, "", err
}
info.Alias = name
info.Digest = r.computeFingerprint(info.Layers)
archID, err := osarch.ArchitectureID(info.Architecture)
if err != nil {
r.errors[name] = err
return nil, "", err
}
archName, err := osarch.ArchitectureName(archID)
if err != nil {
r.errors[name] = err
return nil, "", err
}
info.Architecture = archName
// Store it in the cache.
r.cache[info.Digest] = info
// Prepare the alias entry.
alias := api.ImageAliasesEntry{
ImageAliasesEntryPut: api.ImageAliasesEntryPut{
Target: info.Digest,
},
Name: name,
Type: string(api.InstanceTypeContainer),
}
return &alias, "", nil
}
// GetImageAliasType returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolOCI) GetImageAliasType(imageType string, name string) (*api.ImageAliasesEntry, string, error) {
if api.InstanceType(imageType) == api.InstanceTypeVM {
return nil, "", errors.New("OCI images are only supported for containers")
}
return r.GetImageAlias(name)
}
// GetImageAliasArchitectures returns a map of architectures / targets.
func (r *ProtocolOCI) GetImageAliasArchitectures(imageType string, name string) (map[string]*api.ImageAliasesEntry, error) {
if api.InstanceType(imageType) == api.InstanceTypeVM {
return nil, errors.New("OCI images are only supported for containers")
}
alias, _, err := r.GetImageAlias(name)
if err != nil {
return nil, err
}
localArch, err := osarch.ArchitectureGetLocal()
if err != nil {
return nil, err
}
return map[string]*api.ImageAliasesEntry{localArch: alias}, nil
}
// ExportImage exports (copies) an image to a remote server.
func (r *ProtocolOCI) ExportImage(_ string, _ api.ImageExportPost) (Operation, error) {
return nil, errors.New("Exporting images is not supported with OCI registry")
}
func (r *ProtocolOCI) computeFingerprint(layers []string) string {
h := sha256.New()
for _, layer := range layers {
h.Write([]byte(layer))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
incus-7.3.0/client/oci_util.go 0000664 0000000 0000000 00000000270 15232704312 0016254 0 ustar 00root root 0000000 0000000 //go:build !linux
package incus
import (
"fmt"
)
func unpackOCIImage(imagePath string, imageTag string, bundlePath string) error {
return fmt.Errorf("Platform isn't supported")
}
incus-7.3.0/client/oci_util_linux.go 0000664 0000000 0000000 00000003261 15232704312 0017476 0 ustar 00root root 0000000 0000000 //go:build linux
package incus
import (
"fmt"
"github.com/apex/log"
"github.com/opencontainers/umoci"
"github.com/opencontainers/umoci/oci/cas/dir"
"github.com/opencontainers/umoci/oci/casext"
"github.com/opencontainers/umoci/oci/layer"
"github.com/lxc/incus/v7/shared/logger"
)
func init() {
// apex/log is only used by umoci within Incus.
// So configure its logger to forward to our logger with the relevant prefix.
// Set the custom handler.
log.SetHandler(&umociLogHandler{Message: "Unpacking OCI image"})
}
// Custom handler to intercept logs.
type umociLogHandler struct {
Message string
}
// HandleLog implements a proxy between apex/log and our logger.
func (h *umociLogHandler) HandleLog(e *log.Entry) error {
switch e.Level {
case log.DebugLevel:
logger.Debug(h.Message, logger.Ctx{"log": e.Message})
case log.InfoLevel:
logger.Info(h.Message, logger.Ctx{"log": e.Message})
case log.WarnLevel:
logger.Warn(h.Message, logger.Ctx{"log": e.Message})
case log.ErrorLevel:
logger.Error(h.Message, logger.Ctx{"log": e.Message})
case log.FatalLevel:
logger.Panic(h.Message, logger.Ctx{"log": e.Message})
default:
logger.Error("Unknown umoci log level", logger.Ctx{"log": e.Message})
}
return nil
}
func unpackOCIImage(imagePath string, imageTag string, bundlePath string) error {
var unpackOptions layer.UnpackOptions
unpackOptions.KeepDirlinks = true
// Get a reference to the CAS.
engine, err := dir.Open(imagePath)
if err != nil {
return fmt.Errorf("Open CAS: %w", err)
}
engineExt := casext.NewEngine(engine)
defer logger.WarnOnError(engine.Close, "Failed to close CAS engine")
return umoci.Unpack(engineExt, imageTag, bundlePath, unpackOptions)
}
incus-7.3.0/client/operations.go 0000664 0000000 0000000 00000020540 15232704312 0016632 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"encoding/json"
"errors"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/lxc/incus/v7/shared/api"
)
// The Operation type represents an ongoing Incus operation (asynchronous processing).
type operation struct {
api.Operation
r *ProtocolIncus
listener *EventListener
handlerReady bool
handlerLock sync.Mutex
skipListener bool
chActive chan bool
chActiveOnce sync.Once
}
// closeChActive closes the chActive channel exactly once.
func (op *operation) closeChActive() {
op.chActiveOnce.Do(func() {
close(op.chActive)
})
}
// AddHandler adds a function to be called whenever an event is received.
func (op *operation) AddHandler(function func(api.Operation)) (*EventTarget, error) {
if op.skipListener {
return nil, errors.New("Cannot add handler, client operation does not support event listeners")
}
// Make sure we have a listener setup
err := op.setupListener()
if err != nil {
return nil, err
}
// Make sure we're not racing with ourselves
op.handlerLock.Lock()
defer op.handlerLock.Unlock()
// If we're done already, just return
if op.StatusCode.IsFinal() {
return nil, nil
}
// Wrap the function to filter unwanted messages
wrapped := func(event api.Event) {
op.handlerLock.Lock()
newOp := api.Operation{}
err := json.Unmarshal(event.Metadata, &newOp)
if err != nil || newOp.ID != op.ID {
op.handlerLock.Unlock()
return
}
op.handlerLock.Unlock()
function(newOp)
}
return op.listener.AddHandler([]string{"operation"}, wrapped)
}
// Cancel will request that Incus cancels the operation (if supported).
func (op *operation) Cancel() error {
return op.r.DeleteOperation(op.ID)
}
// Get returns the API operation struct.
func (op *operation) Get() api.Operation {
return op.Operation
}
// GetWebsocket returns a raw websocket connection from the operation.
func (op *operation) GetWebsocket(secret string) (*websocket.Conn, error) {
return op.r.GetOperationWebsocket(op.ID, secret)
}
// RemoveHandler removes a function to be called whenever an event is received.
func (op *operation) RemoveHandler(target *EventTarget) error {
if op.skipListener {
return errors.New("Cannot remove handler, client operation does not support event listeners")
}
// Make sure we're not racing with ourselves
op.handlerLock.Lock()
defer op.handlerLock.Unlock()
// If the listener is gone, just return
if op.listener == nil {
return nil
}
return op.listener.RemoveHandler(target)
}
// Refresh pulls the current version of the operation and updates the struct.
func (op *operation) Refresh() error {
// Get the current version of the operation
newOp, _, err := op.r.GetOperation(op.ID)
if err != nil {
return err
}
// Update the operation struct
op.Operation = *newOp
return nil
}
// Wait lets you wait until the operation reaches a final state.
func (op *operation) Wait() error {
return op.WaitContext(context.Background())
}
// WaitContext lets you wait until the operation reaches a final state with context.Context.
func (op *operation) WaitContext(ctx context.Context) error {
if op.skipListener {
timeout := -1
deadline, ok := ctx.Deadline()
if ok {
timeout = int(time.Until(deadline).Seconds())
}
opAPI, _, err := op.r.GetOperationWait(op.ID, timeout)
if err != nil {
return err
}
op.Operation = *opAPI
if opAPI.Err != "" {
return errors.New(opAPI.Err)
}
return nil
}
op.handlerLock.Lock()
// Check if not done already
if op.StatusCode.IsFinal() {
if op.Err != "" {
op.handlerLock.Unlock()
return errors.New(op.Err)
}
op.handlerLock.Unlock()
return nil
}
op.handlerLock.Unlock()
// Make sure we have a listener setup
err := op.setupListener()
if err != nil {
return err
}
select {
case <-ctx.Done():
// Tear down the listener, cancel the server-side operation and unblock the monitor.
op.handlerLock.Lock()
if op.listener != nil {
op.listener.Disconnect()
op.listener = nil
}
op.handlerLock.Unlock()
_ = op.Cancel()
op.closeChActive()
return ctx.Err()
case <-op.chActive:
}
// We're done, parse the result
if op.Err != "" {
return errors.New(op.Err)
}
return nil
}
// setupListener initiates an event listener for an operation and manages updates to the operation's state.
// It adds handlers to process events, monitors the listener for completion or errors,
// and triggers a manual refresh of the operation's state to prevent race conditions.
func (op *operation) setupListener() error {
if op.skipListener {
return errors.New("Cannot set up event listener, client operation does not support event listeners")
}
// Make sure we're not racing with ourselves
op.handlerLock.Lock()
defer op.handlerLock.Unlock()
// We already have a listener setup
if op.handlerReady {
return nil
}
op.handlerReady = true
// Get a new listener
if op.listener == nil {
listener, err := op.r.GetEvents()
if err != nil {
return err
}
op.listener = listener
}
// Setup the handler
chReady := make(chan bool)
_, err := op.listener.AddHandler([]string{"operation"}, func(event api.Event) {
<-chReady
// We don't want concurrency while processing events
op.handlerLock.Lock()
defer op.handlerLock.Unlock()
// Check if we're done already (because of another event)
if op.listener == nil {
return
}
// Get an operation struct out of this data
newOp := api.Operation{}
err := json.Unmarshal(event.Metadata, &newOp)
if err != nil || newOp.ID != op.ID {
return
}
// Update the struct
op.Operation = newOp
// And check if we're done
if op.StatusCode.IsFinal() {
op.listener.Disconnect()
op.listener = nil
op.closeChActive()
return
}
})
if err != nil {
op.listener.Disconnect()
op.listener = nil
op.closeChActive()
close(chReady)
return err
}
// Monitor event listener
go func() {
<-chReady
// We don't want concurrency while accessing the listener
op.handlerLock.Lock()
// Check if we're done already (because of another event)
listener := op.listener
if listener == nil {
op.handlerLock.Unlock()
return
}
op.handlerLock.Unlock()
// Wait for the listener or operation to be done
select {
case <-listener.ctx.Done():
op.handlerLock.Lock()
if op.listener != nil {
op.Err = listener.err.Error()
op.closeChActive()
}
op.handlerLock.Unlock()
case <-op.chActive:
return
}
}()
// And do a manual refresh to avoid races
err = op.Refresh()
if err != nil {
op.listener.Disconnect()
op.listener = nil
op.closeChActive()
close(chReady)
return err
}
// Check if not done already
if op.StatusCode.IsFinal() {
op.listener.Disconnect()
op.listener = nil
op.closeChActive()
close(chReady)
if op.Err != "" {
return errors.New(op.Err)
}
return nil
}
// Start processing background updates
close(chReady)
return nil
}
// The remoteOperation type represents an ongoing Incus operation between two servers.
type remoteOperation struct {
targetOp Operation
handlers []func(api.Operation)
handlerLock sync.Mutex
chDone chan bool
chPost chan bool
err error
}
// AddHandler adds a function to be called whenever an event is received.
func (op *remoteOperation) AddHandler(function func(api.Operation)) (*EventTarget, error) {
var err error
var target *EventTarget
op.handlerLock.Lock()
defer op.handlerLock.Unlock()
// Attach to the existing target operation
if op.targetOp != nil {
target, err = op.targetOp.AddHandler(function)
if err != nil {
return nil, err
}
} else {
// Generate a mock EventTarget
target = &EventTarget{
function: func(api.Event) { function(api.Operation{}) },
types: []string{"operation"},
}
}
// Add the handler to our list
op.handlers = append(op.handlers, function)
return target, nil
}
// CancelTarget attempts to cancel the target operation.
func (op *remoteOperation) CancelTarget() error {
if op.targetOp == nil {
return errors.New("No associated target operation")
}
return op.targetOp.Cancel()
}
// GetTarget returns the target operation.
func (op *remoteOperation) GetTarget() (*api.Operation, error) {
if op.targetOp == nil {
return nil, errors.New("No associated target operation")
}
opAPI := op.targetOp.Get()
return &opAPI, nil
}
// Wait lets you wait until the operation reaches a final state.
func (op *remoteOperation) Wait() error {
<-op.chDone
if op.chPost != nil {
<-op.chPost
}
return op.err
}
incus-7.3.0/client/simplestreams.go 0000664 0000000 0000000 00000002506 15232704312 0017341 0 ustar 00root root 0000000 0000000 package incus
import (
"errors"
"net/http"
"github.com/lxc/incus/v7/shared/simplestreams"
)
// ProtocolSimpleStreams implements a SimpleStreams API client.
type ProtocolSimpleStreams struct {
ssClient *simplestreams.SimpleStreams
http *http.Client
httpHost string
httpUserAgent string
httpCertificate string
tempPath string
}
// Disconnect is a no-op for simplestreams.
func (r *ProtocolSimpleStreams) Disconnect() {
}
// GetConnectionInfo returns the basic connection information used to interact with the server.
func (r *ProtocolSimpleStreams) GetConnectionInfo() (*ConnectionInfo, error) {
info := ConnectionInfo{}
info.Addresses = []string{r.httpHost}
info.Certificate = r.httpCertificate
info.Protocol = "simplestreams"
info.URL = r.httpHost
return &info, nil
}
// GetHTTPClient returns the http client used for the connection. This can be used to set custom http options.
func (r *ProtocolSimpleStreams) GetHTTPClient() (*http.Client, error) {
if r.http == nil {
return nil, errors.New("HTTP client isn't set, bad connection")
}
return r.http, nil
}
// DoHTTP performs a Request.
func (r *ProtocolSimpleStreams) DoHTTP(req *http.Request) (*http.Response, error) {
// Set the user agent
if r.httpUserAgent != "" {
req.Header.Set("User-Agent", r.httpUserAgent)
}
return r.http.Do(req)
}
incus-7.3.0/client/simplestreams_images.go 0000664 0000000 0000000 00000026771 15232704312 0020700 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
"github.com/lxc/incus/v7/shared/simplestreams"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
// Image handling functions
// GetImages returns a list of available images as Image structs.
func (r *ProtocolSimpleStreams) GetImages() ([]api.Image, error) {
return r.ssClient.ListImages()
}
// GetImagesAllProjects returns a list of available images as Image structs.
func (r *ProtocolSimpleStreams) GetImagesAllProjects() ([]api.Image, error) {
return r.GetImages()
}
// GetImagesAllProjectsWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolSimpleStreams) GetImagesAllProjectsWithFilter(filters []string) ([]api.Image, error) {
return nil, errors.New("GetImagesWithFilter is not supported by the simplestreams protocol")
}
// GetImageFingerprints returns a list of available image fingerprints.
func (r *ProtocolSimpleStreams) GetImageFingerprints() ([]string, error) {
// Get all the images from simplestreams
images, err := r.ssClient.ListImages()
if err != nil {
return nil, err
}
// And now extract just the fingerprints
fingerprints := []string{}
for _, img := range images {
fingerprints = append(fingerprints, img.Fingerprint)
}
return fingerprints, nil
}
// GetImagesWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolSimpleStreams) GetImagesWithFilter(_ []string) ([]api.Image, error) {
return nil, errors.New("GetImagesWithFilter is not supported by the simplestreams protocol")
}
// GetImage returns an Image struct for the provided fingerprint.
func (r *ProtocolSimpleStreams) GetImage(fingerprint string) (*api.Image, string, error) {
image, err := r.ssClient.GetImage(fingerprint)
if err != nil {
return nil, "", fmt.Errorf("Failed getting image: %w", err)
}
return image, "", err
}
// GetImageFile downloads an image from the server, returning an ImageFileResponse struct.
func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
// Quick checks.
if req.MetaFile == nil && req.RootfsFile == nil {
return nil, errors.New("No file requested")
}
// Attempt to download from host
if util.PathExists("/dev/incus/sock") && os.Geteuid() == 0 {
unixURI := fmt.Sprintf("http://unix.socket/1.0/images/%s/export", url.PathEscape(fingerprint))
// Setup the HTTP client
devIncusHTTP, err := unixHTTPClient(nil, "/dev/incus/sock")
if err == nil {
resp, err := incusDownloadImage(fingerprint, unixURI, r.httpUserAgent, devIncusHTTP.Do, req)
if err == nil {
return resp, nil
}
}
}
// Use relatively short response header timeout so as not to hold the image lock open too long.
// Deference client and transport in order to clone them so as to not modify timeout of base client.
httpClient := *r.http
httpTransport := httpClient.Transport.(*http.Transport).Clone()
httpTransport.ResponseHeaderTimeout = 30 * time.Second
httpClient.Transport = httpTransport
// Get the image and expand the fingerprint.
image, err := r.ssClient.GetImage(fingerprint)
if err != nil {
return nil, err
}
fingerprint = image.Fingerprint
// Get the file list
files, err := r.ssClient.GetFiles(fingerprint)
if err != nil {
return nil, err
}
// Prepare the response
resp := ImageFileResponse{}
// Download function
download := func(path string, filename string, hash string, target io.WriteSeeker) (int64, error) {
// Try over http
uri, err := urlJoinPathAbsolute(fmt.Sprintf("http://%s", strings.TrimPrefix(r.httpHost, "https://")), path)
if err != nil {
return -1, err
}
size, err := util.DownloadFileHash(context.TODO(), &httpClient, r.httpUserAgent, req.ProgressHandler, req.Canceler, filename, uri, hash, sha256.New(), target)
if err != nil {
// Handle cancellation
if err.Error() == "net/http: request canceled" {
return -1, err
}
// Try over https
uri, err := urlJoinPathAbsolute(r.httpHost, path)
if err != nil {
return -1, err
}
size, err = util.DownloadFileHash(context.TODO(), &httpClient, r.httpUserAgent, req.ProgressHandler, req.Canceler, filename, uri, hash, sha256.New(), target)
if err != nil {
if errors.Is(err, util.ErrNotFound) {
logger.Info("Unable to download file by hash, invalidate potentially outdated cache", logger.Ctx{"filename": filename, "uri": uri, "hash": hash})
r.ssClient.InvalidateCache()
}
return -1, err
}
}
return size, nil
}
// Download the Incus image file
meta, ok := files["meta"]
if ok && req.MetaFile != nil {
size, err := download(meta.Path, "metadata", meta.Sha256, req.MetaFile)
if err != nil {
return nil, err
}
parts := strings.Split(meta.Path, "/")
resp.MetaName = parts[len(parts)-1]
resp.MetaSize = size
}
// Download the rootfs
rootfs, ok := files["root"]
if ok && req.RootfsFile != nil {
// Look for deltas (requires xdelta3)
downloaded := false
_, err := exec.LookPath("xdelta3")
if err == nil && req.DeltaSourceRetriever != nil {
applyDelta := func(file simplestreams.DownloadableFile, srcPath string, target io.Writer) (int64, error) {
// Create temporary file for the delta
deltaFile, err := os.CreateTemp(r.tempPath, "incus_image_")
if err != nil {
return -1, err
}
defer logger.WarnOnError(deltaFile.Close, "Failed to close temporary file")
defer logger.WarnOnError(func() error { return os.Remove(deltaFile.Name()) }, "Failed to remove temporary file")
// Download the delta
_, err = download(file.Path, "rootfs delta", file.Sha256, deltaFile)
if err != nil {
return -1, err
}
// Create temporary file for the delta
patchedFile, err := os.CreateTemp(r.tempPath, "incus_image_")
if err != nil {
return -1, err
}
defer logger.WarnOnError(patchedFile.Close, "Failed to close temporary file")
defer logger.WarnOnError(func() error { return os.Remove(patchedFile.Name()) }, "Failed to remove temporary file")
// Apply it
_, err = subprocess.RunCommand("xdelta3", "-f", "-d", "-s", srcPath, deltaFile.Name(), patchedFile.Name())
if err != nil {
return -1, err
}
// Copy to the target
size, err := util.SafeCopy(req.RootfsFile, patchedFile)
if err != nil {
return -1, err
}
return size, nil
}
for filename, file := range files {
_, srcFingerprint, prefixFound := strings.Cut(filename, "root.delta-")
if !prefixFound {
continue
}
// Check if we have the source file for the delta
srcPath := req.DeltaSourceRetriever(srcFingerprint, "rootfs")
if srcPath == "" {
continue
}
size, err := applyDelta(file, srcPath, req.RootfsFile)
if err != nil {
return nil, err
}
parts := strings.Split(rootfs.Path, "/")
resp.RootfsName = parts[len(parts)-1]
resp.RootfsSize = size
downloaded = true
}
}
// Download the whole file
if !downloaded {
size, err := download(rootfs.Path, "rootfs", rootfs.Sha256, req.RootfsFile)
if err != nil {
return nil, err
}
parts := strings.Split(rootfs.Path, "/")
resp.RootfsName = parts[len(parts)-1]
resp.RootfsSize = size
}
}
// Validate the full image hash.
//
// Normally we'd do that as we download the image to avoid having to
// re-read the data, but because the simplestreams allows retries (HTTP to HTTPS),
// we don't have a clean reader that can be used for that.
//
// Another situation where we couldn't do a streaming hash anyway is when processing delta images.
hash256 := sha256.New()
if resp.MetaSize > 0 && req.MetaFile != nil {
_, err = req.MetaFile.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
_, err := util.SafeCopy(hash256, req.MetaFile)
if err != nil {
return nil, err
}
}
if resp.RootfsSize > 0 && req.RootfsFile != nil {
_, err = req.RootfsFile.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
_, err := util.SafeCopy(hash256, req.RootfsFile)
if err != nil {
return nil, err
}
}
hash := fmt.Sprintf("%x", hash256.Sum(nil))
if hash != fingerprint {
return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
}
return &resp, nil
}
// GetImageSecret isn't relevant for the simplestreams protocol.
func (r *ProtocolSimpleStreams) GetImageSecret(_ string) (string, error) {
return "", errors.New("Private images aren't supported by the simplestreams protocol")
}
// GetPrivateImage isn't relevant for the simplestreams protocol.
func (r *ProtocolSimpleStreams) GetPrivateImage(_ string, _ string) (*api.Image, string, error) {
return nil, "", errors.New("Private images aren't supported by the simplestreams protocol")
}
// GetPrivateImageFile isn't relevant for the simplestreams protocol.
func (r *ProtocolSimpleStreams) GetPrivateImageFile(_ string, _ string, _ ImageFileRequest) (*ImageFileResponse, error) {
return nil, errors.New("Private images aren't supported by the simplestreams protocol")
}
// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs.
func (r *ProtocolSimpleStreams) GetImageAliases() ([]api.ImageAliasesEntry, error) {
return r.ssClient.ListAliases()
}
// GetImageAliasNames returns the list of available alias names.
func (r *ProtocolSimpleStreams) GetImageAliasNames() ([]string, error) {
// Get all the images from simplestreams
aliases, err := r.ssClient.ListAliases()
if err != nil {
return nil, err
}
// And now extract just the names
names := []string{}
for _, alias := range aliases {
names = append(names, alias.Name)
}
return names, nil
}
// GetImageAlias returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolSimpleStreams) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
alias, err := r.ssClient.GetAlias("container", name)
if err != nil {
alias, err = r.ssClient.GetAlias("virtual-machine", name)
if err != nil {
return nil, "", err
}
}
return alias, "", err
}
// GetImageAliasType returns an existing alias as an ImageAliasesEntry struct.
func (r *ProtocolSimpleStreams) GetImageAliasType(imageType string, name string) (*api.ImageAliasesEntry, string, error) {
if imageType == "" {
return r.GetImageAlias(name)
}
alias, err := r.ssClient.GetAlias(imageType, name)
if err != nil {
return nil, "", err
}
return alias, "", err
}
// GetImageAliasArchitectures returns a map of architectures / targets.
func (r *ProtocolSimpleStreams) GetImageAliasArchitectures(imageType string, name string) (map[string]*api.ImageAliasesEntry, error) {
if imageType == "" {
aliases, err := r.ssClient.GetAliasArchitectures("container", name)
if err != nil {
aliases, err = r.ssClient.GetAliasArchitectures("virtual-machine", name)
if err != nil {
return nil, err
}
}
return aliases, nil
}
return r.ssClient.GetAliasArchitectures(imageType, name)
}
// ExportImage exports (copies) an image to a remote server.
func (r *ProtocolSimpleStreams) ExportImage(_ string, _ api.ImageExportPost) (Operation, error) {
return nil, errors.New("Exporting images is not supported by the simplestreams protocol")
}
func urlJoinPathAbsolute(baseHost string, path string) (result string, err error) {
if strings.HasPrefix("/", path) {
// absolute path
baseHostURL, err := url.ParseRequestURI(baseHost)
if err != nil {
return "", err
}
baseHostURL.Path = path
return baseHostURL.String(), nil
}
// relative path
return url.JoinPath(baseHost, path)
}
incus-7.3.0/client/util.go 0000664 0000000 0000000 00000020520 15232704312 0015422 0 ustar 00root root 0000000 0000000 package incus
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/lxc/incus/v7/shared/proxy"
localtls "github.com/lxc/incus/v7/shared/tls"
)
// tlsHTTPClient creates an HTTP client with a specified Transport Layer Security (TLS) configuration.
// It takes in parameters for client certificates, keys, Certificate Authority, server certificates,
// a boolean for skipping verification, a proxy function, and a transport wrapper function.
// It returns the HTTP client with the provided configurations and handles any errors that might occur during the setup process.
func tlsHTTPClient(client *http.Client, tlsClientCert string, tlsClientKey string, tlsCA string, tlsServerCert string, insecureSkipVerify bool, identicalCertificate bool, proxyFunc func(req *http.Request) (*url.URL, error), transportWrapper func(t *http.Transport) HTTPTransporter) (*http.Client, error) {
// Get the TLS configuration
tlsConfig, err := localtls.GetTLSConfigMem(tlsClientCert, tlsClientKey, tlsCA, tlsServerCert, insecureSkipVerify)
if err != nil {
return nil, err
}
// If asked for an exact match, skip normal validation.
if identicalCertificate {
tlsConfig.InsecureSkipVerify = true
}
// Define the http transport
transport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: proxy.FromEnvironment,
DisableKeepAlives: true,
ExpectContinueTimeout: time.Second * 30,
ResponseHeaderTimeout: time.Second * 3600,
TLSHandshakeTimeout: time.Second * 5,
}
// Allow overriding the proxy
if proxyFunc != nil {
transport.Proxy = proxyFunc
}
// Special TLS handling
transport.DialTLSContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
tlsDial := func(network string, addr string, config *tls.Config, resetName bool) (net.Conn, error) {
conn, err := localtls.RFC3493Dialer(ctx, network, addr)
if err != nil {
return nil, err
}
// Setup TLS
if resetName || config.ServerName == "" {
hostName, _, err := net.SplitHostPort(addr)
if err != nil {
hostName = addr
}
config = config.Clone()
config.ServerName = hostName
}
tlsConn := tls.Client(conn, config)
// Validate the connection
err = tlsConn.Handshake()
if err != nil {
_ = conn.Close()
return nil, err
}
if identicalCertificate {
// Look for an exact match with the certificate provided.
// But ignore any other issue (validity, scope, ...).
cs := tlsConn.ConnectionState()
if len(cs.PeerCertificates) < 1 {
return nil, errors.New("Couldn't validate peer certificate")
}
if tlsServerCert == "" {
return nil, errors.New("Peer certificate wasn't provided")
}
certBlock, _ := pem.Decode([]byte(tlsServerCert))
if certBlock == nil {
return nil, errors.New("Invalid remote certificate")
}
expectedRemoteCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, err
}
if !cs.PeerCertificates[0].Equal(expectedRemoteCert) {
return nil, errors.New("Remote certificate differs from expected")
}
}
if !config.InsecureSkipVerify {
// Check certificate validity.
err := tlsConn.VerifyHostname(config.ServerName)
if err != nil {
_ = conn.Close()
return nil, err
}
}
return tlsConn, nil
}
conn, err := tlsDial(network, addr, transport.TLSClientConfig, false)
if err != nil {
// On certificate verification failure, we may have gotten redirected to a
// non-Incus machine, retry with the dialed address as the server name.
var certVerifyErr *tls.CertificateVerificationError
hostnameErr := x509.HostnameError{}
if errors.As(err, &certVerifyErr) || errors.As(err, &hostnameErr) {
conn, retryErr := tlsDial(network, addr, transport.TLSClientConfig, true)
if retryErr == nil {
return conn, nil
}
}
// Return the initial error as the retry error may be misleading.
return nil, err
}
return conn, nil
}
// Define the http client
if client == nil {
client = &http.Client{}
}
if transportWrapper != nil {
client.Transport = transportWrapper(transport)
} else {
client.Transport = transport
}
// Setup redirect policy
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Replicate the headers
req.Header = via[len(via)-1].Header
return nil
}
return client, nil
}
// unixHTTPClient creates an HTTP client that communicates over a Unix socket.
// It takes in the connection arguments and the Unix socket path as parameters.
// The function sets up a Unix socket dialer, configures the HTTP transport, and returns the HTTP client with the specified configurations.
// Any errors encountered during the setup process are also handled by the function.
func unixHTTPClient(args *ConnectionArgs, path string) (*http.Client, error) {
// Setup a Unix socket dialer
unixDial := func(_ context.Context, _ string, _ string) (net.Conn, error) {
raddr, err := net.ResolveUnixAddr("unix", path)
if err != nil {
return nil, err
}
return net.DialUnix("unix", nil, raddr)
}
if args == nil {
args = &ConnectionArgs{}
}
// Define the http transport
transport := &http.Transport{
DialContext: unixDial,
DisableKeepAlives: true,
Proxy: args.Proxy,
ExpectContinueTimeout: time.Second * 30,
ResponseHeaderTimeout: time.Second * 3600,
TLSHandshakeTimeout: time.Second * 5,
}
// Define the http client
client := args.HTTPClient
if client == nil {
client = &http.Client{}
}
client.Transport = transport
// Setup redirect policy
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Replicate the headers
req.Header = via[len(via)-1].Header
return nil
}
return client, nil
}
// remoteOperationResult used for storing the error that occurred for a particular remote URL.
type remoteOperationResult struct {
URL string
Error error
}
func remoteOperationError(msg string, errorOperationResults []remoteOperationResult) error {
// Check if empty
if len(errorOperationResults) == 0 {
return nil
}
// Check if all identical
var err error
for _, entry := range errorOperationResults {
if err != nil && entry.Error.Error() != err.Error() {
errorStrings := make([]string, 0, len(errorOperationResults))
for _, operationResult := range errorOperationResults {
errorStrings = append(errorStrings, fmt.Sprintf("%s: %v", operationResult.URL, operationResult.Error))
}
return fmt.Errorf("%s:\n - %s", msg, strings.Join(errorStrings, "\n - "))
}
err = entry.Error
}
// Check if successful
if err != nil {
return fmt.Errorf("%s: %w", msg, err)
}
return nil
}
// Set the value of a query parameter in the given URI.
func setQueryParam(uri, param, value string) (string, error) {
fields, err := url.Parse(uri)
if err != nil {
return "", err
}
values := fields.Query()
values.Set(param, url.QueryEscape(value))
fields.RawQuery = values.Encode()
return fields.String(), nil
}
// urlsToResourceNames returns a list of resource names extracted from one or more URLs of the same resource type.
// The resource type path prefix to match is provided by the matchPathPrefix argument.
func urlsToResourceNames(matchPathPrefix string, urls ...string) ([]string, error) {
resourceNames := make([]string, 0, len(urls))
for _, urlRaw := range urls {
u, err := url.Parse(urlRaw)
if err != nil {
return nil, fmt.Errorf("Failed parsing URL %q: %w", urlRaw, err)
}
_, after, found := strings.Cut(u.Path, fmt.Sprintf("%s/", matchPathPrefix))
if !found {
return nil, fmt.Errorf("Unexpected URL path %q", u)
}
resourceNames = append(resourceNames, after)
}
return resourceNames, nil
}
// parseFilters translates filters passed at client side to form acceptable by server-side API.
func parseFilters(filters []string) string {
var result []string
for _, filter := range filters {
if strings.Contains(filter, "=") {
membs := strings.SplitN(filter, "=", 2)
result = append(result, fmt.Sprintf("%s eq %s", membs[0], membs[1]))
}
}
return strings.Join(result, " and ")
}
// HTTPTransporter represents a wrapper around *http.Transport.
// It is used to add some pre and postprocessing logic to http requests / responses.
type HTTPTransporter interface {
http.RoundTripper
// Transport what this struct wraps
Transport() *http.Transport
}
incus-7.3.0/cmd/ 0000775 0000000 0000000 00000000000 15232704312 0013404 5 ustar 00root root 0000000 0000000 incus-7.3.0/cmd/fuidshift/ 0000775 0000000 0000000 00000000000 15232704312 0015371 5 ustar 00root root 0000000 0000000 incus-7.3.0/cmd/fuidshift/main.go 0000664 0000000 0000000 00000001452 15232704312 0016646 0 ustar 00root root 0000000 0000000 package main
import (
"os"
"github.com/spf13/cobra"
"github.com/lxc/incus/v7/internal/version"
)
type cmdGlobal struct {
flagVersion bool
flagHelp bool
}
func main() {
// shift command (main)
shiftCmd := cmdShift{}
app := shiftCmd.command()
app.SilenceUsage = true
app.CompletionOptions = cobra.CompletionOptions{DisableDefaultCmd: true}
// Global flags
globalCmd := cmdGlobal{}
shiftCmd.global = &globalCmd
app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, "Print version number")
app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, "Print help")
// Version handling
app.SetVersionTemplate("{{.Version}}\n")
app.Version = version.Version
// Run the main command and handle errors
err := app.Execute()
if err != nil {
os.Exit(1)
}
}
incus-7.3.0/cmd/fuidshift/main_shift.go 0000664 0000000 0000000 00000004447 15232704312 0020052 0 ustar 00root root 0000000 0000000 package main
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/lxc/incus/v7/shared/idmap"
)
type cmdShift struct {
global *cmdGlobal
flagReverse bool
flagTestMode bool
}
func (c *cmdShift) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = "fuidshift [...]"
cmd.Short = "UID/GID shifter"
cmd.Long = `Description:
UID/GID shifter
This tool lets you remap a filesystem tree, switching it from one
set of UID/GID ranges to another.
This is mostly useful when retrieving a wrongly shifted filesystem tree
from a backup or broken system and having to remap everything either to
the host UID/GID range (uid/gid 0 is root) or to an existing container's
range.
A range is represented as :::.
Where "u" means shift uid, "g" means shift gid and "b" means shift uid and gid.
`
cmd.Example = ` fuidshift my-dir/ b:0:100000:65536 u:10000:1000:1`
cmd.RunE = c.run
cmd.Flags().BoolVarP(&c.flagTestMode, "test", "t", false, "Test mode (no change to files)")
cmd.Flags().BoolVarP(&c.flagReverse, "reverse", "r", false, "Perform a reverse mapping")
return cmd
}
func (c *cmdShift) run(cmd *cobra.Command, args []string) error {
// Help and usage
if len(args) == 0 {
return cmd.Help()
}
// Quick checks.
if !c.flagTestMode && os.Geteuid() != 0 {
return errors.New("This tool must be run as root")
}
// Handle mandatory arguments
if len(args) < 2 {
_ = cmd.Help()
return errors.New("Missing required arguments")
}
directory := args[0]
var skipper func(dir string, absPath string, fi os.FileInfo, newuid int64, newgid int64) error
if c.flagTestMode {
skipper = func(dir string, absPath string, fi os.FileInfo, newuid int64, newgid int64) error {
fmt.Printf("I would shift %q to %d %d\n", absPath, newuid, newgid)
return errors.New("dry run")
}
}
// Parse the maps
idmapSet := &idmap.Set{}
for _, arg := range args[1:] {
var err error
idmapSet, err = idmapSet.Append(arg)
if err != nil {
return err
}
}
// Reverse shifting
if c.flagReverse {
err := idmapSet.UnshiftPath(directory, skipper)
if err != nil {
return err
}
return nil
}
// Normal shifting
err := idmapSet.ShiftPath(directory, skipper)
if err != nil {
return err
}
return nil
}
incus-7.3.0/cmd/generate-config/ 0000775 0000000 0000000 00000000000 15232704312 0016441 5 ustar 00root root 0000000 0000000 incus-7.3.0/cmd/generate-config/README.md 0000664 0000000 0000000 00000016177 15232704312 0017734 0 ustar 00root root 0000000 0000000 # generate-config
A small CLI to parse comments in a Golang codebase meant to be used for a documentation tool (like Sphinx for example).
It parses the comments from the AST and extracts their documentation.
## Disclaimer
`generate-config` is intended for internal use within the
[Incus](https://github.com/lxc/incus) code base. There are no guarantees regarding
backwards compatibility, API stability, or long-term availability. It may change
or be removed at any time without prior notice. Use at your own discretion.
## Usage
```shell
$ generate-config -h
Usage of generate-config:
-e value
Path that will be excluded from the process
```
## Formatting
A comment is formatted this way:
```go
// gendoc:generate(entity=cluster, group=cluster, key=scheduler.instance)
//
//
// ---
// shortdesc: Possible values are all, manual and group. See Automatic placement of instances for more information.
// condition: container
// defaultdesc: `all`
// type: integer
// liveupdate: `yes`
// :
clusterConfigKeys := map[string]func(value string) error{
"scheduler.instance": validate.Optional(validate.IsOneOf("all", "group", "manual")),
}
for k, v := range config {
// gendoc:generate(entity=cluster, group=cluster, key=user.*)
//
// This is the real long desc.
//
// With two paragraphs.
//
// And a list:
//
// - Item
// - Item
// - Item
//
// example of a table:
//
// Key | Type | Scope | Default | Description
// :-- | :--- | :---- | :------ | :----------
// `acme.agree_tos` | bool | global | `false` | Agree to ACME terms of service
// `acme.ca_url` | string | global | `https://acme-v02.api.letsencrypt.org/directory` | URL to the directory resource of the ACME service
// `acme.domain` | string | global | - | Domain for which the certificate is issued
// `acme.email` | string | global | - | Email address used for the account registration
//
// ---
// shortdesc: Free form user key/value storage (can be used in search).
// condition: container
// default: -
// type: string
// liveupdate: `yes`
if strings.HasPrefix(k, "user.") {
continue
}
validator, ok := clusterConfigKeys[k]
if !ok {
return fmt.Errorf("Invalid cluster configuration key %q", k)
}
err := validator(v)
if err != nil {
return fmt.Errorf("Invalid cluster configuration key %q value", k)
}
}
return nil
```
The go-swagger spec from source generator can only handles `swagger:meta` (global file/package level documentation), `swagger:route` (API endpoints), `swagger:params` (function parameters), `swagger:operation` (method documentation), `swagger:response` (API response content documentation), `swagger:model` (struct documentation) generation. In our use case, we would want a config variable spec generator that can bundle any key-value data pairs alongside metadata to build a sense of hierarchy and identity (we want to associate a unique key to each gendoc comment group that will also be displayed in the generated documentation)
In a swagger fashion, `generate-config` can associate metadata key-value pairs (here for example, `group` and `key`) to data key-value pairs. As a result, it can generate a YAML tree out of the code documentation and also a Markdown document.
### Output
Here is the JSON output of the example shown above:
```json
{
"configs": {
"cluster": [
{
"scheduler.instance": {
"condition": "container",
"defaultdesc": "`all`",
"liveupdate": "`yes`",
"longdesc": "",
"shortdesc": " Possible values are all, manual and group. See Automatic placement of instances for more",
"type": "integer"
}
},
{
"user.*": {
"condition": "container",
"defaultdesc": "-",
"liveupdate": "`yes`",
"longdesc": "
This is the real long desc.
With two paragraphs.
And a list:
- Item
- Item
- Item
And a table:
Key | Type | Scope | Default | Description
:-- | :--- | :---- | :------ | :----------
`acme.agree_tos` | bool | global | `false` | Agree to ACME terms of service
`acme.ca_url` | string | global | `https://acme-v02.api.letsencrypt.org/directory` | URL to the directory resource of the ACME service
`acme.domain` | string | global | - | Domain for which the certificate is issued
`acme.email` | string | global | - | Email address used for the account registration
",
"shortdesc": "Free form user key/value storage (can be used in search).",
"type": "string"
}
}
],
}
}
```
Here is the `.txt` output of the example shown above:
```plain
\`\`\`{config:option} user.* cluster
:type: string
:liveupdate: `yes`
:shortdesc: Free form user key/value storage (can be used in search).
:condition: container
:default: -
This is the real long desc.
With two paragraphs.
And a list:
- Item
- Item
- Item
example of a table:
Key | Type | Scope | Default | Description
:-- | :--- | :---- | :------ | :----------
`acme.agree_tos` | bool | global | `false` | Agree to ACME terms of service
`acme.ca_url` | string | global | `https://acme-v02.api.letsencrypt.org/directory` | URL to the directory resource of the ACME service
`acme.domain` | string | global | - | Domain for which the certificate is issued
`acme.email` | string | global | - | Email address used for the account registration
\`\`\`
\`\`\`{config:option} scheduler.instance cluster
:liveupdate: `yes`
:shortdesc: Possible values are all, manual and group. See Automatic placement of instances for more information.
:condition: container
:default: `all`
:type: integer
\`\`\`
```
incus-7.3.0/cmd/generate-config/incus_doc.go 0000664 0000000 0000000 00000026544 15232704312 0020751 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"time"
)
var (
globalGenDocRegex = regexp.MustCompile(`(?m)gendoc:generate\((.*)\)([\S\s]+)\s+---\n([\S\s]+)`)
genDocMetadataRegex = regexp.MustCompile(`(?m)([^,\s]+)=([^,\s]+)`)
genDocDataRegex = regexp.MustCompile(`(?m)([\S]+):[\s]+([\S \"\']+)`)
)
var mdKeys = []string{"entity", "group", "key"}
// IterableAny is a generic type that represents a type or an iterable container.
type IterableAny interface {
any | []any
}
// doc is the structure of the JSON file that contains the generated configuration metadata.
type doc struct {
Configs map[string]any `json:"configs"`
}
// sortConfigKeys alphabetically sorts the entries by key (config option key) within each config group in an entity.
func sortConfigKeys(projectEntries map[string]any) {
for _, entityValue := range projectEntries {
groupValues, ok := entityValue.(map[string]any)
if !ok {
continue
}
for _, groupValue := range groupValues {
configEntries, ok := groupValue.(map[string]any)["keys"].([]any)
if !ok {
continue
}
sort.Slice(configEntries, func(i, j int) bool {
// Get the only key for each map element in the slice
var keyI, keyJ string
confI, confJ := configEntries[i].(map[string]any), configEntries[j].(map[string]any)
for k := range confI {
keyI = k
break // There is only one key-value pair in each map
}
for k := range confJ {
keyJ = k
break // There is only one key-value pair in each map
}
// Compare the keys
return keyI < keyJ
})
}
}
}
// getSortedKeysFromMap returns the keys of a map sorted alphabetically.
func getSortedKeysFromMap[K string, V IterableAny](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
return keys
}
func parse(path string, outputJSONPath string, excludedPaths []string) (*doc, error) {
jsonDoc := &doc{}
docKeys := make(map[string]struct{})
projectEntries := make(map[string]any)
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip excluded paths
if slices.Contains(excludedPaths, path) {
if info.IsDir() {
log.Printf("Skipping excluded directory: %v", path)
return filepath.SkipDir
}
log.Printf("Skipping excluded file: %v", path)
return nil
}
// Only process go files
if !info.IsDir() && filepath.Ext(path) != ".go" {
return nil
}
// Continue walking if directory
if info.IsDir() {
return nil
}
// Parse file and create the AST
fset := token.NewFileSet()
var f *ast.File
f, err = parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return err
}
fileEntries := make([]map[string]any, 0)
// Loop in comment groups
for _, cg := range f.Comments {
s := cg.Text()
entry := make(map[string]any)
groupKeyEntry := make(map[string]any)
for _, match := range globalGenDocRegex.FindAllStringSubmatch(s, -1) {
// check that the match contains the expected number of groups
if len(match) != 4 {
continue
}
log.Printf("Found gendoc at %s", fset.Position(cg.Pos()).String())
metadata := match[1]
longdesc := match[2]
data := match[3]
// process metadata
metadataMap := make(map[string]string)
var entityKey string
var groupKey string
var simpleKey string
for _, mdKVMatch := range genDocMetadataRegex.FindAllStringSubmatch(metadata, -1) {
if len(mdKVMatch) != 3 {
continue
}
mdKey := mdKVMatch[1]
mdValue := mdKVMatch[2]
// check that the metadata key is among the expected ones
if !slices.Contains(mdKeys, mdKey) {
continue
}
if mdKey == "entity" {
entityKey = mdValue
}
if mdKey == "group" {
groupKey = mdValue
}
if mdKey == "key" {
simpleKey = mdValue
}
metadataMap[mdKey] = mdValue
}
// Check that this metadata is not already present
mdKeyHash := fmt.Sprintf("%s/%s/%s", entityKey, groupKey, simpleKey)
_, ok := docKeys[mdKeyHash]
if ok {
return fmt.Errorf("Duplicate key '%s' found at %s", mdKeyHash, fset.Position(cg.Pos()).String())
}
docKeys[mdKeyHash] = struct{}{}
configKeyEntry := make(map[string]any)
configKeyEntry[metadataMap["key"]] = make(map[string]any)
configKeyEntry[metadataMap["key"]].(map[string]any)["longdesc"] = strings.TrimLeft(longdesc, "\n\t\v\f\r")
for _, dataKVMatch := range genDocDataRegex.FindAllStringSubmatch(data, -1) {
if len(dataKVMatch) != 3 {
continue
}
configKeyEntry[metadataMap["key"]].(map[string]any)[dataKVMatch[1]] = dataKVMatch[2]
}
_, ok = groupKeyEntry[metadataMap["group"]]
if ok {
_, ok = groupKeyEntry[metadataMap["group"]].(map[string]any)["keys"]
if ok {
groupKeyEntry[metadataMap["group"]].(map[string]any)["keys"] = append(
groupKeyEntry[metadataMap["group"]].(map[string]any)["keys"].([]any),
configKeyEntry,
)
} else {
groupKeyEntry[metadataMap["group"]].(map[string]any)["keys"] = []any{configKeyEntry}
}
} else {
groupKeyEntry[metadataMap["group"]] = make(map[string]any)
groupKeyEntry[metadataMap["group"]].(map[string]any)["keys"] = []any{configKeyEntry}
}
entry[metadataMap["entity"]] = groupKeyEntry
}
if len(entry) > 0 {
fileEntries = append(fileEntries, entry)
}
}
// Update projectEntries
for _, entry := range fileEntries {
for entityKey, entityValue := range entry {
_, ok := projectEntries[entityKey]
if !ok {
projectEntries[entityKey] = entityValue
} else {
groupValues, ok := entityValue.(map[string]any)
if !ok {
continue
}
for groupKey, groupValue := range groupValues {
_, ok := projectEntries[entityKey].(map[string]any)[groupKey]
if !ok {
projectEntries[entityKey].(map[string]any)[groupKey] = groupValue
} else {
// merge the config keys
configKeys, ok := groupValue.(map[string]any)["keys"].([]any)
if !ok {
continue
}
projectEntries[entityKey].(map[string]any)[groupKey].(map[string]any)["keys"] = append(
projectEntries[entityKey].(map[string]any)[groupKey].(map[string]any)["keys"].([]any),
configKeys...,
)
}
}
}
}
}
return nil
})
if err != nil {
return nil, err
}
// sort the config keys alphabetically
sortConfigKeys(projectEntries)
jsonDoc.Configs = projectEntries
data, err := json.MarshalIndent(jsonDoc, "", "\t")
if err != nil {
return nil, fmt.Errorf("Error while marshaling project documentation: %v", err)
}
if outputJSONPath != "" {
buf := bytes.NewBufferString("")
_, err = buf.Write(data)
if err != nil {
return nil, fmt.Errorf("Error while writing the JSON project documentation: %v", err)
}
err := os.WriteFile(outputJSONPath, buf.Bytes(), 0o644)
if err != nil {
return nil, fmt.Errorf("Error while writing the JSON project documentation: %v", err)
}
}
return jsonDoc, nil
}
func writeDocFile(inputJSONPath, outputTxtPath string) error {
countMaxBackTicks := func(s string) int {
count, currCount := 0, 0
n := len(s)
for i := range n {
if s[i] == '`' {
currCount++
continue
}
if currCount > count {
count = currCount
}
currCount = 0
}
return count
}
specialChars := []string{"", "*", "_", "#", "+", "-", ".", "!", "no", "yes"}
// read the JSON file which is the source of truth for the generation of the .txt file
jsonData, err := os.ReadFile(inputJSONPath)
if err != nil {
return err
}
var jsonDoc doc
err = json.Unmarshal(jsonData, &jsonDoc)
if err != nil {
return err
}
sortedEntityKeys := getSortedKeysFromMap(jsonDoc.Configs)
// create a string buffer
buffer := bytes.NewBufferString("// Code generated by generate-config from the incus project; DO NOT EDIT.\n\n")
for _, entityKey := range sortedEntityKeys {
entityEntries := jsonDoc.Configs[entityKey]
sortedGroupKeys := getSortedKeysFromMap(entityEntries.(map[string]any))
for _, groupKey := range sortedGroupKeys {
groupEntries := entityEntries.(map[string]any)[groupKey]
fmt.Fprintf(buffer, "\n", entityKey, groupKey)
groupKeys, ok := groupEntries.(map[string]any)["keys"].([]any)
if !ok {
continue
}
for _, configEntry := range groupKeys {
configEntry, ok := configEntry.(map[string]any)
if !ok {
continue
}
for configKey, configContent := range configEntry {
// There is only one key-value pair in each map
kvBuffer := bytes.NewBufferString("")
var backticksCount int
var longDescContent string
sortedConfigContentKeys := getSortedKeysFromMap(configContent.(map[string]any))
for _, configEntryContentKey := range sortedConfigContentKeys {
configContentValue := configContent.(map[string]any)[configEntryContentKey]
if configEntryContentKey == "longdesc" {
backticksCount = countMaxBackTicks(configContentValue.(string))
c, ok := configContentValue.(string)
if ok {
longDescContent = c
}
continue
}
configContentValueStr, ok := configContentValue.(string)
if ok {
if (strings.HasSuffix(configContentValueStr, "`") && strings.HasPrefix(configContentValueStr, "`")) || slices.Contains(specialChars, configContentValueStr) {
configContentValueStr = fmt.Sprintf("\"%s\"", configContentValueStr)
}
} else {
switch configEntryContentTyped := configContentValue.(type) {
case int, float64, bool:
configContentValueStr = fmt.Sprint(configEntryContentTyped)
case time.Time:
configContentValueStr = fmt.Sprint(configEntryContentTyped.Format(time.RFC3339))
}
}
var quoteFormattedValue string
if strings.Contains(configContentValueStr, `"`) {
if strings.HasPrefix(configContentValueStr, `"`) && strings.HasSuffix(configContentValueStr, `"`) {
for i, s := range configContentValueStr[1 : len(configContentValueStr)-1] {
if s == '"' {
_ = strings.Replace(configContentValueStr, `"`, `\"`, i)
}
}
quoteFormattedValue = configContentValueStr
} else {
quoteFormattedValue = strings.ReplaceAll(configContentValueStr, `"`, `\"`)
}
} else {
quoteFormattedValue = fmt.Sprintf("\"%s\"", configContentValueStr)
}
fmt.Fprintf(kvBuffer,
":%s: %s\n",
configEntryContentKey,
quoteFormattedValue)
}
if backticksCount < 3 {
fmt.Fprintf(buffer,
"```{config:option} %s %s-%s\n%s%s\n```\n\n",
configKey,
entityKey,
groupKey,
kvBuffer.String(),
strings.TrimLeft(longDescContent, "\n"))
} else {
configQuotes := strings.Repeat("`", backticksCount+1)
fmt.Fprintf(buffer,
"%s{config:option} %s %s-%s\n%s%s\n%s\n\n",
configQuotes,
configKey,
entityKey,
groupKey,
kvBuffer.String(),
strings.TrimLeft(longDescContent, "\n"),
configQuotes)
}
}
}
fmt.Fprintf(buffer, "\n", entityKey, groupKey)
}
}
err = os.WriteFile(outputTxtPath, buffer.Bytes(), 0o644)
if err != nil {
return fmt.Errorf("Error while writing the Markdown project documentation: %v", err)
}
return nil
}
incus-7.3.0/cmd/generate-config/incus_doc_test.go 0000664 0000000 0000000 00000004435 15232704312 0022003 0 ustar 00root root 0000000 0000000 package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test the alphabetical sorting of a `generate-config` JSON structure.
func TestJSONSorted(t *testing.T) {
projectEntries := make(map[string]any)
projectEntries["entityKey1"] = map[string]any{
"groupKey1": map[string]any{
"keys": []any{
map[string]any{
"a.core.server.test.b": map[string]string{
"todo5": "stuff",
"todo6": "stuff",
},
},
map[string]any{
"a.core.server.test.c": map[string]string{
"todo3": "stuff",
"todo4": "stuff",
},
},
map[string]any{
"b.core.server.test.a": map[string]string{
"todo1": "stuff",
"todo2": "stuff",
},
},
},
},
}
projectEntries["entityKey2"] = map[string]any{
"groupKey2": map[string]any{
"keys": []any{
map[string]any{
"000.111.222": map[string]string{
"todo9": "stuff",
"todo10": "stuff",
},
},
map[string]any{
"aaa.ccc.bbb": map[string]string{
"todo7": "stuff",
"todo8": "stuff",
},
},
map[string]any{
"zzz.*": map[string]string{
"todo11": "stuff",
"todo12": "stuff",
},
},
},
},
}
sortedProjectEntries := make(map[string]any)
sortedProjectEntries["entityKey1"] = map[string]any{
"groupKey1": map[string]any{
"keys": []any{
map[string]any{
"a.core.server.test.b": map[string]string{
"todo5": "stuff",
"todo6": "stuff",
},
},
map[string]any{
"a.core.server.test.c": map[string]string{
"todo3": "stuff",
"todo4": "stuff",
},
},
map[string]any{
"b.core.server.test.a": map[string]string{
"todo1": "stuff",
"todo2": "stuff",
},
},
},
},
}
sortedProjectEntries["entityKey2"] = map[string]any{
"groupKey2": map[string]any{
"keys": []any{
map[string]any{
"000.111.222": map[string]string{
"todo9": "stuff",
"todo10": "stuff",
},
},
map[string]any{
"aaa.ccc.bbb": map[string]string{
"todo7": "stuff",
"todo8": "stuff",
},
},
map[string]any{
"zzz.*": map[string]string{
"todo11": "stuff",
"todo12": "stuff",
},
},
},
},
}
sortConfigKeys(projectEntries)
assert.Equal(t, sortedProjectEntries, projectEntries)
}
incus-7.3.0/cmd/generate-config/main.go 0000664 0000000 0000000 00000002654 15232704312 0017723 0 ustar 00root root 0000000 0000000 package main
import (
"errors"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
)
var (
exclude []string
jsonOutput string
txtOutput string
rootCmd = &cobra.Command{
Use: "generate-config",
Short: "generate-config - a simple tool to generate documentation for Incus",
Long: "generate-config - a simple tool to generate documentation for Incus. It outputs a YAML and a Markdown file that contain the content of all `gendoc:generate` statements in the project.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("Please provide a path to the project")
}
path := args[0]
_, err := parse(path, jsonOutput, exclude)
if err != nil {
return err
}
if txtOutput != "" {
err = writeDocFile(jsonOutput, txtOutput)
if err != nil {
return err
}
}
return nil
},
}
)
func main() {
rootCmd.Flags().StringSliceVarP(&exclude, "exclude", "e", []string{}, "Path to exclude from the process")
rootCmd.Flags().StringVarP(&jsonOutput, "json", "j", "configuration.json", "Output JSON file containing the generated configuration")
rootCmd.Flags().StringVarP(&txtOutput, "txt", "t", "", "Output TXT file containing the generated documentation")
err := rootCmd.Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "generate-config failed: %v", err)
os.Exit(1)
}
log.Println("generate-config finished successfully")
}
incus-7.3.0/cmd/generate-database/ 0000775 0000000 0000000 00000000000 15232704312 0016740 5 ustar 00root root 0000000 0000000 incus-7.3.0/cmd/generate-database/README.md 0000664 0000000 0000000 00000034055 15232704312 0020226 0 ustar 00root root 0000000 0000000 # `generate-database`
## Introduction
`generate-database` is a database statement and associated `go` function generator
for Incus and related projects. `generate-database` utilizes `go`'s code generation
directives (`//go:generate ...`) alongside go's [ast](https://pkg.go.dev/go/ast)
and [types](https://pkg.go.dev/go/types) packages for parsing the syntax tree for
go structs and variables. We use `generate-database` for the majority of our
SQL statements and database interactions on the `go` side for consistency and
predictability.
## Disclaimer
`generate-database` is intended for internal use within the
[Incus](https://github.com/lxc/incus) code base. There are no guarantees regarding
backwards compatibility, API stability, or long-term availability. It may change
or be removed at any time without prior notice. Use at your own discretion.
## Usage
### Initialization
#### Package global
Once per package, that uses `generate-database` for generation of database
statements and associated `go` functions, `generate-database` needs to be invoked
using the following `go:generate` instruction:
```go
//go:generate generate-database db mapper generate
```
This will initiate a call to `generate-database db mapper generate`,
which will then search for `//generate-database:mapper` directives in the same file
and process those.
The following flags are available:
* `--package` / `-p`: Package import paths to search for structs to parse. Defaults to the caller package. Can be used more than once.
#### File
Generally the first thing we will want to do for any newly generated file is to
ensure the file has been cleared of content:
```go
//generate-database:mapper target instances.mapper.go
//generate-database:mapper reset -i -b "//go:build linux && cgo && !agent"
```
### Generation Directive Arguments
The generation directive arguments have the following form:
`//generate-database:mapper flags `
The following flags are available:
* `--build` / `-b`: build comment to include (commands: `reset`)
* `--interface` / `-i`: create interface files (commands: `reset`, `method`)
* `--entity` / `-e`: database entity to generate the method or statement for (commands: `stmt`, `method`)
Example:
* `//generate-database:mapper stmt -e instance objects table=table_name`
The `table` key can be used to override the generated table name for a specified one.
* `//generate-database:mapper method -i -e instance Create references=Config,Device`
For some tables (defined below under [Additional Information](#Additional-Information) as [EntityTable](#EntityTable), the `references=` key can be provided with the name of
a [ReferenceTable](#ReferenceTable) or [MapTable](#MapTable) struct. This directive would produce `CreateInstance` in addition to `CreateInstanceConfig` and `CreateInstanceDevices`:
* `//generate-database:mapper method -i -e instance_profile Create struct=Instance`
* `//generate-database:mapper method -i -e instance_profile Create struct=Profile`
For some tables (defined below under [Additional Information](#Additional-Information) as [AssociationTable](#AssociationTable), `method` declarations must
include a `struct=` to indicate the directionality of the function. An invocation can be called for each direction.
This would produce `CreateInstanceProfiles` and `CreateProfileInstances` respectively.
### SQL Statement Generation
SQL generation supports the following SQL statement types:
Type | Description
:--- | :----
`objects` | Creates a basic SELECT statement of the form `SELECT FROM
ORDER BY `.
`objects-by--and-...` | Parses a pre-existing SELECT statement variable declaration of the form produced by`objects`, and appends a `WHERE` clause with the given fields located in the associated struct. Specifically looks for a variable declaration of the form `var Objects = RegisterStmt("SQL String")`
`names` | Creates a basic SELECT statement of the form `SELECT FROM
ORDER BY `.
`names-by--and-...` | Parses a pre-existing SELECT statement variable declaration of the form produced by`names`, and appends a `WHERE` clause with the given fields located in the associated struct. Specifically looks for a variable declaration of the form `var Objects = RegisterStmt("SQL String")`
`create` | Creates a basic INSERT statement of the form `INSERT INTO
VALUES`.
`create-or-replace` | Creates a basic INSERT statement of the form `INSERT OR REPLACE INTO
VALUES`.
`delete-by--and-...` | Creates a DELETE statement of the form `DELETE FROM
WHERE ` where the constraint is based on the given fields of the associated struct.
`id` | Creates a basic SELECT statement that returns just the internal ID of the table.
`rename` | Creates an UPDATE statement that updates the primary key of a table: `UPDATE
SET WHERE `.
`update` | Creates an UPDATE statement of the form `UPDATE
SET WHERE `.
#### Examples
```go
//generate-database:mapper stmt -e instance objects
//generate-database:mapper stmt -e instance objects-by-Name-and-Project
//generate-database:mapper stmt -e instance create
//generate-database:mapper stmt -e instance update
//generate-database:mapper stmt -e instance delete-by-Name-and-Project
```
#### Statement Related Go Tags
There are several tags that can be added to fields of a struct that will be parsed by the `ast` package.
Tag | Description
:-- | :----
`sql=
.` | Supply an explicit table and column name to use for this struct field.
`coalesce=` | Generates a SQL coalesce function with the given value `coalesce(, value)`.
`order=yes` | Override the default `ORDER BY` columns with all fields specifying this tag.
`join=` | Applies a `JOIN` of the form `JOIN ON
. = `.
`leftjoin=` | Applies a `LEFT JOIN` of the same form as a `JOIN`.
`joinon=
.` | Overrides the default `JOIN ON` clause with the given table and column, replacing `
.` above.
`jointo=` | Overrides the default target column `id` with the given column, replacing the `id` in `` above. This is intended for "loose" foreign keys, not using the ID column. Therefore, this is intended to be used in conjunction with `joinon` and `omit=create,update` to get the expected behavior.
`joinas=` | Sets an alias for the joined table name, in case it clashes with another table.
`primary=yes` | Assigns column associated with the field to be sufficient for returning a row from the table. Will default to `Name` if unspecified. Fields with this key will be included in the default 'ORDER BY' clause.
`omit=` | Omits a given field from consideration for the comma separated list of statement types (`create`, `objects-by-Name`, `update`).
`ignore` | Outright ignore the struct field as though it does not exist. `ignore` needs to be the only tag value in order to be recognized.
`marshal=` | Marshal/Unmarshal data into the field. The column must be a TEXT column. If `marshal=yes`, then the type must implement both `Marshal` and `Unmarshal`. If `marshal=json`, the type is marshaled to JSON using the standard library ([json.Marshal](https://pkg.go.dev/encoding/json#Marshal)). This works for entity tables only, and not for association or mapping tables.
`create_timestamp` | Automatically set the value of this column to the current time (UTC) when the respective record is created, namely in `Create` and `CreateOrReplace` (regardless if the record is actually created or updated).
`update_timestamp` | Automatically set the value of this column to the current time (UTC) for every operation altering the record, namely `Create`, `CreateOrReplace`, `Rename` and `Update`.
### Go Function Generation
Go function generation supports the following types:
Type | Description
:--- | :----
`GetNames` | Return a slice of primary keys for all rows in a table matching the filter. Cannot be used with composite keys.
`GetMany` | Return a slice of structs for all rows in a table matching the filter.
`GetOne` | Return a single struct corresponding to a row with the given primary keys. Depends on `GetMany`.
`ID` | Return the ID column from the table corresponding to the given primary keys.
`Exists` | Returns whether there is an row in the table with the given primary keys. Depends on `ID.`
`Create` | Insert a row from the given struct into the table if not already present. Depends on `Exists`
`CreateOrReplace` | Insert a row from the given struct into the table, regardless of if an entry already exists.
`Rename` | Update the primary key for a table row.
`Update` | Update the columns at a given row, specified by primary key.
`DeleteOne` | Delete exactly one row from the table.
`DeleteMany` | Delete one or more rows from the table.
```go
//generate-database:mapper method -i -e instance GetMany
//generate-database:mapper method -i -e instance GetOne
//generate-database:mapper method -i -e instance ID
//generate-database:mapper method -i -e instance Exist
//generate-database:mapper method -i -e instance Create
//generate-database:mapper method -i -e instance Update
//generate-database:mapper method -i -e instance DeleteOne-by-Project-and-Name
//generate-database:mapper method -i -e instance DeleteMany-by-Name
```
### Additional Information
All structs should have an `ID` field, as well as an additional `Filter` struct prefixed with the original struct name.
This should include any fields that should be considered for filtering in `WHERE` clauses.
These fields should be pointers to facilitate omission and inclusion without setting default values.
Example:
```go
type Instance struct {
ID int
Name string
Project string
}
type InstanceFilter struct {
Name *string
Project *string
}
```
`generate-database` will handle parsing of structs differently based on the composition of the struct in four different ways.
Non-`EntityType` structs will only support `GetMany`, `Create`, `Update`, and `Delete` functions.
### EntityTable
Most structs will get treated this way, and represent a normal table.
* If a table has an associated table for which a `ReferenceTable` or `MapTable` as defined below is applicable, functions specific to this entity can be generated by
including a comma separated list to `references=` in the code generation directive for `GetMany`, `Create`, or `Update` directives.
* The `Create` method directive for `EntityTable` will expect on the `ID` and `Exist` method directives to be present.
* All `CREATE`, `UPDATE`, and `DELETE` statements that include a joined table will expect a `var ID = RegisterStmt('SQL String')` to exist for the joining table.
### ReferenceTable
A struct that contains a field named `ReferenceID` will be parsed this way.
`generate-database` will use this struct to generate more abstract SQL statements and functions of the form `_`.
The associated `Filter` struct may include a `ReferenceID []int` field.
This generates an `IN` clause matching on the parent column, with the integer values inlined into the query to avoid query parameter count limits.
A nil slice leaves the filter unset while an empty (non-nil) slice matches nothing.
When the field is present, the generated per-parent helpers and nested reference fetches are automatically scoped to the relevant parent IDs rather than fetching the whole table.
Real world invocation of these statements and functions should be done through an `EntityTable` `method` call with the tag `references=`. This `EntityTable` will replace the `` above.
Example:
```go
//generate-database:mapper stmt -e device create
//generate-database:mapper method -e device Create
type Device struct {
ID int
ReferenceID int
Name string
Type string
}
//...
//generate-database:mapper method -e instance Create references=Device
// This will produce a function called `CreateInstanceDevices`.
```
### MapTable
This is a special type of `ReferenceTable` with fields named `Key` and `Value`.
On the SQL side, this is treated exactly like a `ReferenceTable`, but on the `go` side, the return values will be a map.
Example:
```go
//generate-database:mapper stmt -e config create
//generate-database:mapper method -e config Create
type Config struct {
ID int
ReferenceID int
Key string
Value string
}
//...
//generate-database:mapper method -e instance Create references=Config
// This will produce a function called `CreateInstanceConfig`, which will return a `map[string]string`.
```
### AssociationTable
This is a special type of table that contains two fields of the form `ID`, where `` corresponds to two other structs present in the same package.
This will generate code for compound tables of the form `_` that are generally used to associate two tables together by their IDs.
`method` generation declarations for these statements should include a `struct=` to indicate the directionality of the function.
An invocation can be called for each direction.
Example:
```go
//generate-database:mapper method -i -e instance_profile Create struct=Instance
//generate-database:mapper method -i -e instance_profile Create struct=Profile
type InstanceProfile struct {
InstanceID int
ProfileID int
}
```
incus-7.3.0/cmd/generate-database/db.go 0000664 0000000 0000000 00000016677 15232704312 0017675 0 ustar 00root root 0000000 0000000 //go:build linux && cgo && !agent
package main
import (
"encoding/csv"
"errors"
"fmt"
"go/build"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/tools/go/packages"
"github.com/lxc/incus/v7/cmd/generate-database/db"
"github.com/lxc/incus/v7/cmd/generate-database/file"
"github.com/lxc/incus/v7/cmd/generate-database/lex"
)
// Return a new db command.
func newDb() *cobra.Command {
cmd := &cobra.Command{
Use: "db [sub-command]",
Short: "Database-related code generation.",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("Not implemented")
},
}
cmd.AddCommand(newDbSchema())
cmd.AddCommand(newDbMapper())
// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706
cmd.Args = cobra.NoArgs
cmd.Run = func(cmd *cobra.Command, args []string) { _ = cmd.Usage() }
return cmd
}
func newDbSchema() *cobra.Command {
cmd := &cobra.Command{
Use: "schema",
Short: "Generate database schema by applying updates.",
RunE: func(cmd *cobra.Command, args []string) error {
return db.UpdateSchema()
},
}
return cmd
}
func newDbMapper() *cobra.Command {
cmd := &cobra.Command{
Use: "mapper [sub-command]",
Short: "Generate code mapping database rows to Go structs.",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("Not implemented")
},
}
cmd.AddCommand(newDbMapperGenerate())
return cmd
}
func newDbMapperGenerate() *cobra.Command {
var pkgs *[]string
var boilerplateFilename string
cmd := &cobra.Command{
Use: "generate",
Short: "Generate database statememnts and transaction method and interface signature.",
RunE: func(cmd *cobra.Command, args []string) error {
if os.Getenv("GOPACKAGE") == "" {
return errors.New("GOPACKAGE environment variable is not set")
}
return generate(*pkgs, boilerplateFilename)
},
}
flags := cmd.Flags()
pkgs = flags.StringArrayP("package", "p", []string{}, "Go package where the entity struct is declared")
flags.StringVarP(&boilerplateFilename, "boilerplate-file", "b", "-", "Filename of the file where the mapper boilerplate is written to")
return cmd
}
const prefix = "//generate-database:mapper "
func generate(pkgs []string, boilerplateFilename string) error {
localPath, err := os.Getwd()
if err != nil {
return err
}
localPkg, err := packages.Load(&packages.Config{Mode: packages.NeedName}, localPath)
if err != nil {
return err
}
localPkgPath := localPkg[0].PkgPath
if len(pkgs) == 0 {
pkgs = []string{localPkgPath}
}
parsedPkgs, err := packageLoad(pkgs)
if err != nil {
return err
}
err = file.Boilerplate(boilerplateFilename)
if err != nil {
return err
}
registeredSQLStmts := map[string]string{}
for _, parsedPkg := range parsedPkgs {
for _, goFile := range parsedPkg.CompiledGoFiles {
body, err := os.ReadFile(goFile)
if err != nil {
return err
}
// Reset target to stdout
target := "-"
lines := strings.Split(string(body), "\n")
for _, line := range lines {
// Lazy matching for prefix, does not consider Go syntax and therefore
// lines starting with prefix, that are part of e.g. multiline strings
// match as well. This is highly unlikely to cause false positives.
after, ok := strings.CutPrefix(line, prefix)
if ok {
line = after
// Use csv parser to properly handle arguments surrounded by double quotes.
r := csv.NewReader(strings.NewReader(line))
r.Comma = ' ' // space
args, err := r.Read()
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("command missing")
}
command := args[0]
switch command {
case "target":
if len(args) != 2 {
return fmt.Errorf("invalid arguments for command target, one argument for the target filename: %s", line)
}
target = args[1]
case "reset":
err = commandReset(args[1:], parsedPkgs, target, localPkgPath)
case "stmt":
err = commandStmt(args[1:], target, parsedPkgs, registeredSQLStmts, localPkgPath)
case "method":
err = commandMethod(args[1:], target, parsedPkgs, registeredSQLStmts, localPkgPath)
default:
err = fmt.Errorf("unknown command: %s", command)
}
if err != nil {
return err
}
}
}
}
}
return nil
}
func commandReset(commandLine []string, parsedPkgs []*packages.Package, target string, localPkgPath string) error {
var err error
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
iface := flags.BoolP("interface", "i", false, "create interface files")
buildComment := flags.StringP("build", "b", "", "build comment to include")
err = flags.Parse(commandLine)
if err != nil {
return err
}
imports := db.Imports
for _, pkg := range parsedPkgs {
if pkg.PkgPath == localPkgPath {
continue
}
imports = append(imports, pkg.PkgPath)
}
err = file.Reset(target, imports, *buildComment, *iface)
if err != nil {
return err
}
return nil
}
func commandStmt(commandLine []string, target string, parsedPkgs []*packages.Package, registeredSQLStmts map[string]string, localPkgPath string) error {
var err error
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
entity := flags.StringP("entity", "e", "", "database entity to generate the statement for")
err = flags.Parse(commandLine)
if err != nil {
return err
}
if len(flags.Args()) < 1 {
return errors.New("argument missing for stmt command")
}
kind := flags.Arg(0)
config, err := parseParams(flags.Args()[1:])
if err != nil {
return err
}
stmt, err := db.NewStmt(localPkgPath, parsedPkgs, *entity, kind, config, registeredSQLStmts)
if err != nil {
return err
}
return file.Append(*entity, target, stmt, false)
}
func commandMethod(commandLine []string, target string, parsedPkgs []*packages.Package, registeredSQLStmts map[string]string, localPkgPath string) error {
var err error
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
iface := flags.BoolP("interface", "i", false, "create interface files")
entity := flags.StringP("entity", "e", "", "database entity to generate the method for")
err = flags.Parse(commandLine)
if err != nil {
return err
}
if len(flags.Args()) < 1 {
return errors.New("argument missing for method command")
}
kind := flags.Arg(0)
config, err := parseParams(flags.Args()[1:])
if err != nil {
return err
}
method, err := db.NewMethod(localPkgPath, parsedPkgs, *entity, kind, config, registeredSQLStmts)
if err != nil {
return err
}
return file.Append(*entity, target, method, *iface)
}
func packageLoad(pkgs []string) ([]*packages.Package, error) {
pkgPaths := []string{}
for _, pkg := range pkgs {
if pkg == "" {
var err error
localPath, err := os.Getwd()
if err != nil {
return nil, err
}
pkgPaths = append(pkgPaths, localPath)
} else {
importPkg, err := build.Import(pkg, "", build.FindOnly)
if err != nil {
return nil, fmt.Errorf("Invalid import path %q: %w", pkg, err)
}
pkgPaths = append(pkgPaths, importPkg.Dir)
}
}
parsedPkgs, err := packages.Load(&packages.Config{
Mode: packages.LoadTypes | packages.NeedTypesInfo,
}, pkgPaths...)
if err != nil {
return nil, err
}
return parsedPkgs, nil
}
func parseParams(args []string) (map[string]string, error) {
config := map[string]string{}
for _, arg := range args {
key, value, err := lex.KeyValue(arg)
if err != nil {
return nil, fmt.Errorf("Invalid config parameter: %w", err)
}
config[key] = value
}
return config, nil
}
incus-7.3.0/cmd/generate-database/db/ 0000775 0000000 0000000 00000000000 15232704312 0017325 5 ustar 00root root 0000000 0000000 incus-7.3.0/cmd/generate-database/db/constants.go 0000664 0000000 0000000 00000000402 15232704312 0021664 0 ustar 00root root 0000000 0000000 //go:build linux && cgo && !agent
package db
// Imports is a list of the package imports every generated source file has.
var Imports = []string{
"context",
"database/sql",
"fmt",
"strings",
"github.com/mattn/go-sqlite3",
"github.com/google/uuid",
}
incus-7.3.0/cmd/generate-database/db/lex.go 0000664 0000000 0000000 00000007345 15232704312 0020455 0 ustar 00root root 0000000 0000000 package db
import (
"fmt"
"strings"
"github.com/lxc/incus/v7/cmd/generate-database/lex"
"github.com/lxc/incus/v7/shared/util"
)
// Return the table name for the given database entity.
func entityTable(entity string, override string) string {
if override != "" {
return override
}
entityParts := strings.Split(lex.SnakeCase(entity), "_")
tableParts := make([]string, len(entityParts))
for i, part := range entityParts {
if strings.HasSuffix(part, "ty") || strings.HasSuffix(part, "ly") {
tableParts[i] = part
} else {
tableParts[i] = lex.Plural(part)
}
}
return strings.Join(tableParts, "_")
}
// Return the name of the Filter struct for the given database entity.
func entityFilter(entity string) string {
return fmt.Sprintf("%sFilter", lex.PascalCase(entity))
}
// Return the name of the global variable holding the registration code for
// the given kind of statement aganst the given entity.
func stmtCodeVar(entity string, kind string, filters ...string) string {
prefix := lex.CamelCase(entity)
name := fmt.Sprintf("%s%s", prefix, lex.PascalCase(kind))
if len(filters) > 0 {
name += "By"
name += strings.Join(filters, "And")
}
return name
}
// operation returns the kind of operation being performed, without filter fields.
func operation(kind string) string {
return strings.Split(kind, "-by-")[0]
}
// activeFilters returns the filters mentioned in the command name.
func activeFilters(kind string) []string {
startIndex := strings.Index(kind, "-by-") + len("-by-")
return strings.Split(kind[startIndex:], "-and-")
}
// Return an expression evaluating if a filter should be used (based on active
// criteria).
func activeCriteria(filter []string, ignoredFilter []string) string {
expr := ""
for i, name := range filter {
if i > 0 {
expr += " && "
}
expr += fmt.Sprintf("filter.%s != nil", name)
}
for _, name := range ignoredFilter {
if len(expr) > 0 {
expr += " && "
}
expr += fmt.Sprintf("filter.%s == nil", name)
}
return expr
}
// Return the code for a "dest" function, to be passed as parameter to
// selectObjects in order to scan a single row.
func destFunc(slice string, entity string, importType string, fields []*Field) string {
var builder strings.Builder
writeLine := func(line string) { fmt.Fprintf(&builder, "%s\n", line) }
writeLine(`func(scan func(dest ...any) error) error {`)
varName := lex.Minuscule(string(entity[0]))
writeLine(fmt.Sprintf("%s := %s{}", varName, importType))
checkErr := func() {
writeLine("if err != nil {\nreturn err\n}")
writeLine("")
}
unmarshal := func(declVarName string, field *Field) {
unmarshalFunc := "unmarshal"
if field.Config.Get("marshal") == "json" {
unmarshalFunc = "unmarshalJSON"
}
writeLine(fmt.Sprintf("err = %s(%s, &%s.%s)", unmarshalFunc, declVarName, varName, field.Name))
checkErr()
}
args := make([]string, len(fields))
declVars := make(map[string]*Field, len(fields))
declVarNames := make([]string, 0, len(fields))
for i, field := range fields {
var arg string
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
declVarName := fmt.Sprintf("%sStr", lex.Minuscule(field.Name))
declVarNames = append(declVarNames, declVarName)
declVars[declVarName] = field
arg = fmt.Sprintf("&%s", declVarName)
} else {
arg = fmt.Sprintf("&%s.%s", varName, field.Name)
}
args[i] = arg
}
for _, declVarName := range declVarNames {
writeLine(fmt.Sprintf("var %s string", declVarName))
}
writeLine(fmt.Sprintf("err := scan(%s)", strings.Join(args, ", ")))
checkErr()
for _, declVarName := range declVarNames {
unmarshal(declVarName, declVars[declVarName])
}
writeLine(fmt.Sprintf("%s = append(%s, %s)\n", slice, slice, varName))
writeLine("return nil")
writeLine("}")
return builder.String()
}
incus-7.3.0/cmd/generate-database/db/mapping.go 0000664 0000000 0000000 00000043051 15232704312 0021312 0 ustar 00root root 0000000 0000000 package db
import (
"fmt"
"go/ast"
"go/types"
"net/url"
"slices"
"strings"
"github.com/lxc/incus/v7/cmd/generate-database/lex"
"github.com/lxc/incus/v7/shared/util"
)
// Mapping holds information for mapping database tables to a Go structure.
type Mapping struct {
Local bool // Whether the entity is in the same package as the generated code.
FilterLocal bool // Whether the entity is in the same package as the generated code.
Package string // Package of the Go struct
Name string // Name of the Go struct.
Fields []*Field // Metadata about the Go struct.
Filterable bool // Whether the Go struct has a Filter companion struct for filtering queries.
Filters []*Field // Metadata about the Go struct used for filter fields.
Type TableType // Type of table structure for this Go struct.
}
// TableType represents the logical type of the table defined by the Go struct.
type TableType int
// EntityTable represents the type for any entity that maps to a Go struct.
var EntityTable = TableType(0)
// ReferenceTable represents the type for for any entity that contains an
// 'entity_id' field mapping to a parent entity.
var ReferenceTable = TableType(1)
// AssociationTable represents the type for an entity that associates two
// other entities.
var AssociationTable = TableType(2)
// MapTable represents the type for a table storing key/value pairs.
var MapTable = TableType(3)
// NaturalKey returns the struct fields that can be used as natural key for
// uniquely identifying a row in the underlying table (==.
//
// By convention the natural key field is the one called "Name", unless
// specified otherwise with the `db:natural_key` tags.
func (m *Mapping) NaturalKey() []*Field {
key := []*Field{}
for _, field := range m.Fields {
if field.Config.Get("primary") != "" {
key = append(key, field)
}
}
if len(key) == 0 {
// Default primary key.
key = append(key, m.FieldByName("Name"))
}
return key
}
// Identifier returns the field that uniquely identifies this entity.
func (m *Mapping) Identifier() *Field {
var fallback *Field
for _, field := range m.NaturalKey() {
if field.Config.Get("primary") != "" {
return field
}
if field.Name == "Name" || field.Name == "Fingerprint" {
fallback = field
}
}
return fallback
}
// TableName determines the table associated to the struct.
// - Individual fields may bypass this with their own `sql=
.` tags.
// - The override `table=` directive key is checked first.
// - The struct name itself is used to approximate the table name if none of the above apply.
func (m *Mapping) TableName(entity string, override string) string {
table := entityTable(entity, override)
if m.Type == ReferenceTable || m.Type == MapTable {
table = "%s_" + table
}
return table
}
// ContainsFields checks that the mapping contains fields with the same type
// and name of given ones.
func (m *Mapping) ContainsFields(fields []*Field) bool {
matches := map[*Field]bool{}
for _, field := range m.Fields {
for _, other := range fields {
if field.Name == other.Name && field.Type.Name == other.Type.Name {
matches[field] = true
}
}
}
return len(matches) == len(fields)
}
// FieldByName returns the field with the given name, if any.
func (m *Mapping) FieldByName(name string) *Field {
for _, field := range m.Fields {
if field.Name == name {
return field
}
}
return nil
}
// ActiveFilters returns the active filter fields for the kind of method.
func (m *Mapping) ActiveFilters(kind string) []*Field {
names := activeFilters(kind)
fields := []*Field{}
for _, name := range names {
field := m.FieldByName(name)
if field != nil {
fields = append(fields, field)
}
}
return fields
}
// FieldColumnName returns the column name of the field with the given name,
// prefixed with the entity's table name.
func (m *Mapping) FieldColumnName(name string, table string) string {
field := m.FieldByName(name)
return fmt.Sprintf("%s.%s", table, field.Column())
}
// FilterFieldByName returns the field with the given name if that field can be
// used as query filter, an error otherwise.
func (m *Mapping) FilterFieldByName(name string) (*Field, error) {
for _, filter := range m.Filters {
if name == filter.Name {
if filter.Type.Code != TypeColumn {
return nil, fmt.Errorf("Unknown filter %q not a column", name)
}
return filter, nil
}
}
return nil, fmt.Errorf("Unknown filter %q", name)
}
// ColumnFields returns the fields that map directly to a database column,
// either on this table or on a joined one.
func (m *Mapping) ColumnFields(exclude ...string) []*Field {
fields := []*Field{}
for _, field := range m.Fields {
if slices.Contains(exclude, field.Name) {
continue
}
if field.Type.Code == TypeColumn {
fields = append(fields, field)
}
}
return fields
}
// ScalarFields returns the fields that map directly to a single database
// column on another table that can be joined to this one.
func (m *Mapping) ScalarFields() []*Field {
fields := []*Field{}
for _, field := range m.Fields {
if field.Config.Get("join") != "" || field.Config.Get("leftjoin") != "" {
fields = append(fields, field)
}
}
return fields
}
// RefFields returns the fields that are one-to-many references to other
// tables.
func (m *Mapping) RefFields() []*Field {
fields := []*Field{}
for _, field := range m.Fields {
if field.Type.Code == TypeSlice || field.Type.Code == TypeMap {
fields = append(fields, field)
}
}
return fields
}
// FieldArgs converts the given fields to function arguments, rendering their
// name and type.
func (m *Mapping) FieldArgs(fields []*Field, extra ...string) string {
args := []string{}
for _, field := range fields {
name := lex.Minuscule(field.Name)
if name == "type" {
name = lex.Minuscule(m.Name) + field.Name
}
arg := fmt.Sprintf("%s %s", name, field.Type.Name)
args = append(args, arg)
}
args = append(args, extra...)
return strings.Join(args, ", ")
}
// FieldParams converts the given fields to function parameters, rendering their
// name.
func (m *Mapping) FieldParams(fields []*Field) string {
args := make([]string, len(fields))
for i, field := range fields {
name := lex.Minuscule(field.Name)
if name == "type" {
name = lex.Minuscule(m.Name) + field.Name
}
args[i] = name
}
return strings.Join(args, ", ")
}
// FieldParamsMarshal converts the given fields to function parameters, rendering their
// name. If the field is configured to marshal input/output, the name will be `marshaled{name}`.
func (m *Mapping) FieldParamsMarshal(fields []*Field) string {
args := make([]string, len(fields))
for i, field := range fields {
name := lex.Minuscule(field.Name)
if name == "type" {
name = lex.Minuscule(m.Name) + field.Name
}
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
name = fmt.Sprintf("marshaled%s", field.Name)
}
args[i] = name
}
return strings.Join(args, ", ")
}
// ImportType returns the type of the entity for the mapping, prefixing the import package if necessary.
func (m *Mapping) ImportType() string {
name := lex.PascalCase(m.Name)
if m.Local {
return name
}
return m.Package + "." + lex.PascalCase(name)
}
// ImportFilterType returns the Filter type of the entity for the mapping, prefixing the import package if necessary.
func (m *Mapping) ImportFilterType() string {
name := lex.PascalCase(entityFilter(m.Name))
if m.FilterLocal {
return name
}
return m.Package + "." + name
}
// Field holds all information about a field in a Go struct that is relevant
// for database code generation.
type Field struct {
Name string
Type Type
Primary bool // Whether this field is part of the natural primary key.
Config url.Values
}
// Stmt must be used only on a non-columnar field. It returns the name of
// statement that should be used to fetch this field. A statement with that
// name must have been generated for the entity at hand.
func (f *Field) Stmt() string {
switch f.Name {
case "UsedBy":
return "used_by"
default:
return ""
}
}
// IsScalar returns true if the field is a scalar column value from a joined table.
func (f *Field) IsScalar() bool {
return f.joinConfig() != ""
}
// IsIndirect returns true if the field is a scalar column value from a joined
// table that in turn requires another join.
func (f *Field) IsIndirect() bool {
return f.IsScalar() && f.Config.Get("via") != ""
}
// IsPrimary returns true if the field part of the natural key.
func (f *Field) IsPrimary() bool {
return f.Config.Get("primary") != "" || f.Name == "Name"
}
// Column returns the name of the database column the field maps to. The type
// code of the field must be TypeColumn.
func (f *Field) Column() string {
if f.Type.Code != TypeColumn {
panic("attempt to get column name of non-column field")
}
column := lex.SnakeCase(f.Name)
join := f.joinConfig()
if join != "" {
column = fmt.Sprintf("%s AS %s", join, column)
}
return column
}
// SelectColumn returns a column name suitable for use with 'SELECT' statements.
// - Applies a `coalesce()` function if the 'coalesce' tag is present.
// - Returns the column in the form '. AS ' if the `join` tag is present.
func (f *Field) SelectColumn(mapping *Mapping, primaryTable string) (string, error) {
// ReferenceTable and MapTable require specific fields, so parse those instead of checking tags.
if mapping.Type == ReferenceTable || mapping.Type == MapTable {
table := primaryTable
column := fmt.Sprintf("%s.%s", table, lex.SnakeCase(f.Name))
column = strings.ReplaceAll(column, "reference", "%s")
return column, nil
}
tableName, columnName, err := f.SQLConfig()
if err != nil {
return "", err
}
if tableName == "" {
tableName = primaryTable
}
if columnName == "" {
columnName = lex.SnakeCase(f.Name)
}
var column string
join := f.joinConfig()
if join != "" {
joinAs := f.Config.Get("joinas")
if joinAs != "" {
join = joinAs + "." + strings.Split(join, ".")[1]
}
column = join
} else {
column = fmt.Sprintf("%s.%s", tableName, columnName)
}
coalesce, ok := f.Config["coalesce"]
if ok {
column = fmt.Sprintf("coalesce(%s, %s)", column, coalesce[0])
}
if join != "" {
column = fmt.Sprintf("%s AS %s", column, columnName)
}
return column, nil
}
// OrderBy returns a column name suitable for use with the 'ORDER BY' clause.
func (f *Field) OrderBy(mapping *Mapping, primaryTable string) (string, error) {
// ReferenceTable and MapTable require specific fields, so parse those instead of checking tags.
if mapping.Type == ReferenceTable || mapping.Type == MapTable {
table := primaryTable
column := fmt.Sprintf("%s.%s", table, lex.SnakeCase(f.Name))
column = strings.ReplaceAll(column, "reference", "%s")
return column, nil
}
if f.IsScalar() {
tableName, _, err := f.ScalarTableColumn()
if err != nil {
return "", err
}
return tableName + ".id", nil
}
tableName, columnName, err := f.SQLConfig()
if err != nil {
return "", nil
}
if columnName == "" {
columnName = lex.SnakeCase(f.Name)
}
if tableName == "" {
tableName = primaryTable
}
if tableName != "" {
return fmt.Sprintf("%s.%s", tableName, columnName), nil
}
return fmt.Sprintf("%s.%s", entityTable(mapping.Name, tableName), columnName), nil
}
// JoinClause returns an SQL 'JOIN' clause using the 'join' and 'joinon' tags, if present.
func (f *Field) JoinClause(mapping *Mapping, table string) (string, error) {
joinTemplate := "\n JOIN %s ON %s = %s.%s"
if f.Config.Get("join") != "" && f.Config.Get("leftjoin") != "" {
return "", fmt.Errorf("Cannot join and leftjoin at the same time for field %q of struct %q", f.Name, mapping.Name)
}
join := f.joinConfig()
if f.Config.Get("leftjoin") != "" {
joinTemplate = strings.ReplaceAll(joinTemplate, "JOIN", "LEFT JOIN")
}
joinTable, _, ok := strings.Cut(join, ".")
if !ok {
return "", fmt.Errorf("'join' tag for field %q of struct %q must be of form
.", f.Name, mapping.Name)
}
joinOn := f.Config.Get("joinon")
if joinOn == "" {
tableName, columnName, err := f.SQLConfig()
if err != nil {
return "", err
}
if tableName != "" && columnName != "" {
joinOn = fmt.Sprintf("%s.%s", tableName, columnName)
} else {
joinOn = fmt.Sprintf("%s.%s_id", table, lex.Singular(joinTable))
}
}
_, _, ok = strings.Cut(joinOn, ".")
if !ok {
return "", fmt.Errorf("'joinon' tag of field %q of struct %q must be of form '
.'", f.Name, mapping.Name)
}
joinTo := "id"
if f.Config.Get("jointo") != "" {
joinTo = f.Config.Get("jointo")
}
joinAs := f.Config.Get("joinas")
if joinAs == "" {
joinAs = joinTable
} else {
joinTable = joinTable + " " + joinAs
}
return fmt.Sprintf(joinTemplate, joinTable, joinOn, joinAs, joinTo), nil
}
// InsertColumn returns a column name and parameter value suitable for an 'INSERT', 'UPDATE', or 'DELETE' statement.
// - If a 'join' tag is present, the package will be searched for the corresponding 'jointableID' registered statement
// to select the ID to insert into this table.
// - If a 'joinon' tag is present, but this table is not among the conditions, then the join will be considered indirect,
// and an empty string will be returned.
func (f *Field) InsertColumn(mapping *Mapping, primaryTable string, defs map[*ast.Ident]types.Object, registeredSQLStmts map[string]string, allFields []*Field) (string, string, error) {
var column string
var value string
var err error
if f.IsScalar() {
tableName, columnName, err := f.SQLConfig()
if err != nil {
return "", "", err
}
if tableName == "" {
tableName = primaryTable
}
// If there is a 'joinon' tag present without this table in the condition, then assume there is no column for this field.
joinOn := f.Config.Get("joinon")
if joinOn != "" {
before, after, ok := strings.Cut(joinOn, ".")
if !ok {
return "", "", fmt.Errorf("'joinon' tag of field %q of struct %q must be of form '
.'", f.Name, mapping.Name)
}
columnName = after
if tableName != before {
return "", "", nil
}
}
table, _, ok := strings.Cut(f.joinConfig(), ".")
if !ok {
return "", "", fmt.Errorf("'join' tag of field %q of struct %q must be of form
.", f.Name, mapping.Name)
}
if columnName != "" {
column = columnName
} else {
column = lex.Singular(table) + "_id"
}
varName := stmtCodeVar(lex.Singular(table), "ID")
joinStmt, err := ParseStmt(varName, defs, registeredSQLStmts)
if err != nil {
return "", "", fmt.Errorf("Failed to find registered statement %q for field %q of struct %q: %w", varName, f.Name, mapping.Name, err)
}
// Keep track of the join config of other fields that have a corresponding table column.
otherJoins := map[string]string{}
for _, otherField := range allFields {
if f.Name == otherField.Name {
continue
}
joinCfg := otherField.joinConfig()
table, _, ok := strings.Cut(joinCfg, ".")
if !ok {
continue
}
// If 'joinon' points to a different table, then there is no column on the table for this field.
joinOn := otherField.Config.Get("joinon")
if joinOn == "" || strings.HasPrefix(joinOn, tableName+".") {
otherJoins[table] = joinCfg
}
}
// If the field maps to a column with a foreign key to table A, but table A has a composite key with table B,
// then if we already have a field mapping to table B, just reuse its ID.
if strings.Contains(joinStmt, "JOIN ") && strings.Contains(joinStmt, "WHERE ") {
wheres := strings.Split(joinStmt, "WHERE ")
for table, joinCfg := range otherJoins {
wheres[1] = strings.ReplaceAll(wheres[1], joinCfg+" = ?", table+".id = "+lex.Singular(table)+"_id")
}
joinStmt = strings.Join(wheres, "WHERE ")
}
value = fmt.Sprintf("(%s)", strings.ReplaceAll(strings.ReplaceAll(joinStmt, "`", ""), "\n", ""))
value = strings.ReplaceAll(value, " ", " ")
} else {
column, err = f.SelectColumn(mapping, primaryTable)
if err != nil {
return "", "", err
}
// Strip the table name and coalesce function if present.
_, column, _ = strings.Cut(column, ".")
column, _, _ = strings.Cut(column, ",")
if mapping.Type == ReferenceTable || mapping.Type == MapTable {
column = strings.ReplaceAll(column, "reference", "%s")
}
value = "?"
}
return column, value, nil
}
func (f *Field) joinConfig() string {
join := f.Config.Get("join")
if join == "" {
join = f.Config.Get("leftjoin")
}
return join
}
// SQLConfig returns the table and column specified by the 'sql' config key, if present.
func (f *Field) SQLConfig() (string, string, error) {
where := f.Config.Get("sql")
if where == "" {
return "", "", nil
}
table, column, ok := strings.Cut(where, ".")
if !ok {
return "", "", fmt.Errorf("'sql' config for field %q should be of the form
.", f.Name)
}
return table, column, nil
}
// ScalarTableColumn gets the table and column from the join configuration.
func (f *Field) ScalarTableColumn() (string, string, error) {
join := f.joinConfig()
if join == "" {
return "", "", fmt.Errorf("Missing join config for field %q", f.Name)
}
joinFields := strings.Split(join, ".")
if len(joinFields) != 2 {
return "", "", fmt.Errorf("Join config must be of the format