diff --git a/.asf.yaml b/.asf.yaml index 092e06d97168..1b922d6e9d84 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -16,7 +16,6 @@ # under the License. # https://cwiki.apache.org/confluence/display/INFRA/git+-+.asf.yaml+features ---- github: description: "Apache CloudStack is an opensource Infrastructure as a Service (IaaS) cloud computing platform" homepage: https://cloudstack.apache.org/ @@ -50,18 +49,32 @@ github: rebase: false collaborators: - - acs-robot - - gpordeus - - hsato03 - - FelipeM525 - - lucas-a-martins - - nicoschmdt - - abh1sar - - rosi-shapeblue - - sudo87 + - ingox + - gp-santos - erikbocks + - Imvedansh + - Damans227 + - jmsperu + - GaOrtiga + - bhouse-nexthop + - Dogface2k - protected_branches: ~ + rulesets: + - name: "Default Branch Protection" + type: branch + branches: + includes: + - "~DEFAULT_BRANCH" + excludes: [] + bypass_teams: + - root + restrict_deletion: true + restrict_force_push: true + + copilot_code_review: + enabled: true + review_drafts: true + review_on_push: true notifications: commits: commits@cloudstack.apache.org diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 000000000000..3c632f8ba534 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +[codespell] +ignore-words = .github/linters/codespell.txt +skip = systemvm/agent/noVNC/*,ui/package.json,ui/package-lock.json,ui/public/js/less.min.js,ui/public/locales/*.json,server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java,test/integration/smoke/test_ssl_offloading.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..1b06f3ebf53c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2af9f54edc44..375912667d63 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,9 @@ /plugins/storage/volume/linstor @rp- /plugins/storage/volume/storpool @slavkap +/plugins/storage/volume/ontap @rajiv-jain-netapp @sandeeplocharla @piyush5netapp @suryag1201 .pre-commit-config.yaml @jbampton /.github/linters/ @jbampton + +/plugins/network-elements/nsx/ @Pearl1594 @nvazquez diff --git a/.github/COPILOT_TOKENS.md b/.github/COPILOT_TOKENS.md new file mode 100644 index 000000000000..7bf5993a8627 --- /dev/null +++ b/.github/COPILOT_TOKENS.md @@ -0,0 +1,90 @@ + + +# Contributing a Copilot token for the agentic workflows + +This repo runs scheduled [GitHub Agentic Workflows](https://github.github.com/gh-aw/) (the +`*.lock.yml` files compiled from `*.md` in `.github/workflows/`) that drive the GitHub Copilot +CLI. Each run needs a GitHub token from an account with an active Copilot license. So that no +single person's Copilot quota gets burned through, runs rotate day by day across a pool of +volunteer tokens. + +If you have a Copilot license and want to help share the load, add your token to the pool. + +## What kind of token + +- A fine-grained personal access token. Classic PATs don't work with the Copilot CLI. +- Resource owner: your own personal account. +- Permission: Account permissions > "Copilot Requests" > Read. That's the only permission it + needs, no repo access. +- Your account must have an active Copilot seat. + +Create it at . Give it a sensible +expiration; when it lapses the health check (below) will flag it and you can re-add it. + +## How to add it + +1. Pick a short alias for yourself, e.g. `t1`, `t2`, `vol3`. The alias shows up in workflow logs, + so keep it non-identifying if you prefer. +2. Add your token as a repository secret named `COPILOT_GITHUB_TOKEN_` + (e.g. `COPILOT_GITHUB_TOKEN_t1`). Repo admins do this via + *Settings > Secrets and variables > Actions > New repository secret*, or: + ``` + gh secret set COPILOT_GITHUB_TOKEN_t1 --body "github_pat_xxx" + ``` +3. Ask a repo admin to register the alias by appending it to the repository variable + `GH_AW_COPILOT_TOKEN_NAMES`, which is a JSON array: + ``` + gh variable set GH_AW_COPILOT_TOKEN_NAMES --body '["t1","t2","t3"]' + ``` + The workflows can't enumerate secrets, so this variable is the source of truth for the pool. + A token isn't used until its alias is listed there. + +## How rotation works (for maintainers) + +Each agent workflow (`weekly-repo-status`, `daily-issue-triage`) defines a `pick_copilot_token` +job in its `.md` source. The job has to run outside the agent job because strict mode forbids +reading secrets there. It picks today's alias by day-of-year mod N, checks the token is live +(`GET /user` returns 200, otherwise it moves on to the next candidate) and outputs the chosen +alias. The token value itself never crosses jobs. The two workflows use different +`ROTATION_SLOT`s, which start them half the pool apart so they don't land on the same +volunteer on the same day (with at least two tokens in the pool). + +The agent job resolves the secret itself via +`secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)]` and falls +back to the base `COPILOT_GITHUB_TOKEN` when the pick job outputs an empty name. Keep the base +secret set to one reliable token. + +`gh aw compile` doesn't know about this wiring, so after editing the `.md` sources run: + +``` +gh aw compile && bash .github/scripts/post-compile.sh +``` + +See the header of `.github/scripts/post-compile.sh` for what it patches. + +To check the pool, trigger the "Copilot token health" workflow +(`.github/workflows/copilot-token-health.yml`) from the Actions tab. It prints an HTTP status +code per alias and nothing else, so no account identities end up in logs. Note it can't tell +when a token is live but has used up its monthly Copilot requests. + +## Removing a token + +Delete the `COPILOT_GITHUB_TOKEN_` secret and remove `` from +`GH_AW_COPILOT_TOKEN_NAMES`. diff --git a/.github/actions/install-nonoss/action.yml b/.github/actions/install-nonoss/action.yml new file mode 100644 index 000000000000..39a03213c29d --- /dev/null +++ b/.github/actions/install-nonoss/action.yml @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +name: 'Install CloudStack Non-OSS' +description: 'Clones and installs the shapeblue/cloudstack-nonoss repository.' + +runs: + using: "composite" + steps: + - name: Install cloudstack-nonoss + shell: bash + run: | + git clone --depth 1 https://github.com/shapeblue/cloudstack-nonoss.git nonoss + cd nonoss + bash -x install-non-oss.sh + cd .. + rm -fr nonoss diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml new file mode 100644 index 000000000000..0f8425229242 --- /dev/null +++ b/.github/actions/setup-env/action.yml @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +name: 'Setup CloudStack Environment' +description: 'Sets up JDK (with Maven cache), optionally Python, and optionally APT build dependencies for CloudStack.' + +inputs: + java-version: + description: 'The JDK version to use' + required: false + default: '17' + install-python: + description: 'Whether to install Python 3.10' + required: false + default: 'false' + install-apt-deps: + description: 'Whether to install CloudStack APT build dependencies' + required: false + default: 'false' + +runs: + using: "composite" + steps: + - name: Set up JDK ${{ inputs.java-version }} + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: ${{ inputs.java-version }} + distribution: 'adopt' + architecture: x64 + cache: 'maven' + + - name: Set up Python + if: ${{ inputs.install-python == 'true' }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.10' + architecture: x64 + + - name: Install Build Dependencies + if: ${{ inputs.install-apt-deps == 'true' }} + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y git uuid-runtime genisoimage netcat-openbsd ipmitool build-essential libgcrypt20 libgpg-error-dev libgpg-error0 libopenipmi0 libpython3-dev libssl-dev libffi-dev python3-openssl python3-dev python3-setuptools diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 000000000000..ea25ffab6b9e --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,56 @@ +{ + "entries": { + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" + }, + "github/gh-aw-actions/setup@v0.76.1": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.76.1", + "sha": "46d564922b082d0db93244972e8005ea6904ee5f" + } + }, + "containers": { + "ghcr.io/github/gh-aw-firewall/agent:0.18.0": { + "image": "ghcr.io/github/gh-aw-firewall/agent:0.18.0", + "digest": "sha256:ab84dfc7f5998cb8cd0c596526dd573b7e7d06c6a740266a1e6df879fa16c866", + "pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.18.0@sha256:ab84dfc7f5998cb8cd0c596526dd573b7e7d06c6a740266a1e6df879fa16c866" + }, + "ghcr.io/github/gh-aw-firewall/agent:0.25.55": { + "image": "ghcr.io/github/gh-aw-firewall/agent:0.25.55", + "digest": "sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731", + "pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731" + }, + "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55": { + "image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55", + "digest": "sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3", + "pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3" + }, + "ghcr.io/github/gh-aw-firewall/squid:0.18.0": { + "image": "ghcr.io/github/gh-aw-firewall/squid:0.18.0", + "digest": "sha256:82a5d062a5612a57a43a171a5b79ddbb690a86a8ddda02339cc1675131ae9f8b", + "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.18.0@sha256:82a5d062a5612a57a43a171a5b79ddbb690a86a8ddda02339cc1675131ae9f8b" + }, + "ghcr.io/github/gh-aw-firewall/squid:0.25.55": { + "image": "ghcr.io/github/gh-aw-firewall/squid:0.25.55", + "digest": "sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca", + "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca" + }, + "ghcr.io/github/gh-aw-mcpg:v0.1.4": { + "image": "ghcr.io/github/gh-aw-mcpg:v0.1.4", + "digest": "sha256:0acf25aa1d409f9c73be9e39ac84f4bd4b90d8bfa1db4dc6d7f47d38ccd58914", + "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.1.4@sha256:0acf25aa1d409f9c73be9e39ac84f4bd4b90d8bfa1db4dc6d7f47d38ccd58914" + }, + "ghcr.io/github/gh-aw-mcpg:v0.3.19": { + "image": "ghcr.io/github/gh-aw-mcpg:v0.3.19", + "digest": "sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f", + "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f" + }, + "ghcr.io/github/github-mcp-server:v0.30.3": { + "image": "ghcr.io/github/github-mcp-server:v0.30.3", + "digest": "sha256:a2b5fb79b1cee851bfc3532dfe480c3dc5736974ca9d93a7a9f68e52ce4b62a0", + "pinned_image": "ghcr.io/github/github-mcp-server:v0.30.3@sha256:a2b5fb79b1cee851bfc3532dfe480c3dc5736974ca9d93a7a9f68e52ce4b62a0" + } + } +} diff --git a/.github/aw/imports/.gitattributes b/.github/aw/imports/.gitattributes new file mode 100644 index 000000000000..f0516fad90e4 --- /dev/null +++ b/.github/aw/imports/.gitattributes @@ -0,0 +1,5 @@ +# Mark all cached import files as generated +* linguist-generated=true + +# Use 'ours' merge strategy to keep local cached versions +* merge=ours diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 41b307863fc3..6ffb926f6fa0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,8 +22,21 @@ version: 2 updates: - - package-ecosystem: "maven" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "github-actions" + directory: "/" + open-pull-requests-limit: 2 + schedule: + interval: "weekly" + groups: + github-actions-dependencies: + patterns: + - "*" + ignore: + - dependency-name: "github/gh-aw-actions/**" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump. + cooldown: + default-days: 7 + - package-ecosystem: "maven" + directory: "/" schedule: interval: "daily" cooldown: diff --git a/.github/scripts/post-compile.sh b/.github/scripts/post-compile.sh new file mode 100755 index 000000000000..a93a34701e00 --- /dev/null +++ b/.github/scripts/post-compile.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# Re-applies the token round-robin wiring to the gh-aw generated .lock.yml files, +# which `gh aw compile` doesn't know about. Run after every compile: +# +# gh aw compile && bash .github/scripts/post-compile.sh +# +# Three edits per lock file (see .github/COPILOT_TOKENS.md for the design): +# - point the agent execute step's COPILOT_GITHUB_TOKEN at the pick_copilot_token +# job's output, falling back to the base secret +# - point the agent job's "Redact secrets in logs" step at the same rotated token, +# so a volunteer token is scrubbed from uploaded artifacts, not just the base one +# - make the agent job depend on pick_copilot_token, and strip the self-reference +# gh-aw sometimes adds to pick_copilot_token's own needs (that would be a cycle) +# +# Safe to re-run; a second run is a no-op. + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Kept in an env var so perl doesn't try to interpolate the ${{ }} bits. +export NEWVAL='${{ needs.pick_copilot_token.outputs.name != '"'"''"'"' && secrets[format('"'"'COPILOT_GITHUB_TOKEN_{0}'"'"', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }}' + +FILES=( + ".github/workflows/weekly-repo-status.lock.yml" + ".github/workflows/daily-issue-triage.lock.yml" +) + +fail() { echo "ERROR: $1" >&2; exit 1; } + +# Fixes up `needs:` for the agent and pick_copilot_token jobs only; gh-aw adds +# pick_copilot_token to several other jobs' needs and those must stay as-is. +# Reads stdin, writes stdout. gh-aw emits inline needs (needs: foo) for single +# dependencies and block form for lists; inline is fine unless it needs editing, +# in which case "INLINE_NEEDS:" is printed to stderr and the caller bails. +normalise_needs() { + awk ' + function isjob(l){ return (l ~ /^ [A-Za-z0-9_-]+:[ \t]*$/) } + BEGIN { job=""; inneeds=0; agentpick=0 } + { + line=$0 + if (isjob(line)) { + if (inneeds && job=="agent" && !agentpick) print " - pick_copilot_token" + inneeds=0; agentpick=0 + name=line; sub(/^ /,"",name); sub(/:[ \t]*$/,"",name); job=name + print line; next + } + if (line ~ /^ needs:[ \t]*[^ \t]/) { + if (job=="agent" && line !~ /pick_copilot_token/) print "INLINE_NEEDS:" job > "/dev/stderr" + if (job=="pick_copilot_token" && line ~ /pick_copilot_token/) print "INLINE_NEEDS:" job > "/dev/stderr" + print line; next + } + if (line ~ /^ needs:[ \t]*$/) { inneeds=1; agentpick=0; print line; next } + if (inneeds) { + if (line ~ /^ - /) { + item=line; sub(/^ - /,"",item); gsub(/[ \t\r]/,"",item) + if (job=="pick_copilot_token" && item=="pick_copilot_token") next + if (job=="agent" && item=="pick_copilot_token") agentpick=1 + print line; next + } else { + if (job=="agent" && !agentpick) print " - pick_copilot_token" + inneeds=0 + print line; next + } + } + print line + } + END { if (inneeds && job=="agent" && !agentpick) print " - pick_copilot_token" } + ' +} + +for f in "${FILES[@]}"; do + if [ ! -f "$f" ]; then + echo "WARN: $f not found, run 'gh aw compile' first? Skipping" >&2 + continue + fi + + # Repoint the agent execute step's token. The anchor is the GH_AW_PHASE: agent env + # var further down the same env block; the detection job's block has + # GH_AW_PHASE: detection so it doesn't match and keeps the base token. + before=$(grep -cF 'COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true) + perl -0pi -e \ + 's/^([ \t]*)COPILOT_GITHUB_TOKEN:[ \t]*\$\{\{[ \t]*secrets\.COPILOT_GITHUB_TOKEN[ \t]*\}\}[ \t]*\n(?=(?:[ \t]+[A-Z][A-Za-z0-9_]*:[^\n]*\n)*?[ \t]+GH_AW_PHASE:[ \t]*agent[ \t]*\n)/$1."COPILOT_GITHUB_TOKEN: ".$ENV{NEWVAL}."\n"/me' \ + "$f" + after=$(grep -cF 'COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true) + removed=$(( before - after )) + if [ "$removed" -eq 1 ]; then token_edit="applied" + elif [ "$removed" -eq 0 ] && grep -qE '^[ \t]+COPILOT_GITHUB_TOKEN: \$\{\{ needs\.pick_copilot_token' "$f"; then token_edit="already" + else fail "$f: execute-step token line not patched as expected (removed=$removed), anchor drifted?" + fi + + # Repoint the redact step's SECRET_COPILOT_GITHUB_TOKEN the same way; the line is + # unique to the agent job's "Redact secrets in logs" step. + before=$(grep -cF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true) + if [ "$before" -gt 1 ]; then + fail "$f: expected at most one redact-step SECRET_COPILOT_GITHUB_TOKEN line, found $before" + fi + perl -0pi -e \ + 's/^([ \t]*)SECRET_COPILOT_GITHUB_TOKEN:[ \t]*\$\{\{[ \t]*secrets\.COPILOT_GITHUB_TOKEN[ \t]*\}\}[ \t]*$/$1."SECRET_COPILOT_GITHUB_TOKEN: ".$ENV{NEWVAL}/me' \ + "$f" + after=$(grep -cF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}' "$f" || true) + if [ "$before" -eq 1 ] && [ "$after" -eq 0 ]; then redact_edit="applied" + elif [ "$before" -eq 0 ] && grep -qF 'SECRET_COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name' "$f"; then redact_edit="already" + else fail "$f: redact-step SECRET_COPILOT_GITHUB_TOKEN line not patched (before=$before after=$after)" + fi + + errf="$(mktemp)" + normalise_needs < "$f" > "$f.tmp" 2>"$errf" + if grep -q '^INLINE_NEEDS:' "$errf"; then + rm -f "$f.tmp"; rm -f "$errf" + fail "$f: agent/pick_copilot_token have inline 'needs:' that would need editing, update normalise_needs" + fi + rm -f "$errf" + mv "$f.tmp" "$f" + + # sanity checks + self_refs=$(awk ' + /^ pick_copilot_token:[ \t]*$/{p=1;next} + /^ [A-Za-z0-9_-]+:[ \t]*$/{p=0} + p && /^ - pick_copilot_token[ \t]*$/{c++} + END{print c+0}' "$f") + [ "$self_refs" -eq 0 ] || fail "$f: pick_copilot_token still self-references (cycle)" + awk '/^ agent:[ \t]*$/{a=1} /^ [A-Za-z0-9_-]+:[ \t]*$/ && !/agent/{if(a&&!seen)exit 3} a && /^ - pick_copilot_token/{seen=1} END{exit (seen?0:3)}' "$f" \ + || fail "$f: agent job does not depend on pick_copilot_token" + grep -qF 'Validate COPILOT_GITHUB_TOKEN secret' "$f" || echo "WARN: validate-secret step missing in $f" >&2 + grep -qE '^ pick_copilot_token:$' "$f" || echo "WARN: pick_copilot_token job missing in $f, did compile include the .md jobs: block?" >&2 + + echo "$f: token-ref=$token_edit, redact-ref=$redact_edit, needs=normalised (self-refs=0, agent->pick ok)" +done + +echo "Done." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84020f4a6b06..9c8ca10f85e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,40 +16,31 @@ # under the License. name: Build - -on: [push, pull_request] - +on: + push: + branches: + - main + - 4.22 + - 4.20 + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read - jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' + persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v6 + - name: Setup Environment + uses: ./.github/actions/setup-env with: - python-version: '3.10' - architecture: 'x64' - - - name: Install Build Dependencies - run: | - sudo apt-get update - sudo apt-get install -y git uuid-runtime genisoimage netcat ipmitool build-essential libgcrypt20 libgpg-error-dev libgpg-error0 libopenipmi0 ipmitool libpython3-dev libssl-dev libffi-dev python3-openssl python3-dev python3-setuptools - + install-python: 'true' + install-apt-deps: 'true' - name: Env details run: | uname -a @@ -60,9 +51,8 @@ jobs: free -m nproc git status - + - name: Install Non-OSS + uses: ./.github/actions/install-nonoss - name: Noredist Build run: | - git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss && cd nonoss && bash -x install-non-oss.sh && cd .. - rm -fr nonoss mvn -B -P developer,systemvm -Dsimulator -Dnoredist clean install -T$(nproc) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4edd448067ae..7f6645f4af4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,21 +16,60 @@ # under the License. name: Simulator CI - -on: [push, pull_request] - +on: + push: + branches: + - main + - 4.22 + - 4.20 + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read - jobs: build: if: github.repository == 'apache/cloudstack' runs-on: ubuntu-24.04 - + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Environment + uses: ./.github/actions/setup-env + with: + install-python: 'true' + install-apt-deps: 'true' + - name: Env details + run: | + uname -a + whoami + javac -version + mvn -v + python3 --version + free -m + nproc + git status + ipmitool -V + - name: Build with Maven + run: | + mvn -B -P developer,systemvm -Dsimulator clean install -DskipTests=true -T$(nproc) + - name: Archive artifacts + run: | + mkdir -p /tmp/artifacts + tar -czf /tmp/artifacts/targets.tar.gz $(find . -name "target" -type d) tools/marvin/dist engine/schema/dist utils/conf + tar -czf /tmp/artifacts/m2-cloudstack.tar.gz -C ~/.m2/repository org/apache/cloudstack + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-artifacts + path: /tmp/artifacts/ + test: + needs: build + if: github.repository == 'apache/cloudstack' + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -146,6 +185,7 @@ jobs: smoke/test_vm_snapshot_kvm smoke/test_vm_snapshots smoke/test_volumes + smoke/test_vpc_conserve_mode smoke/test_vpc_ipv6 smoke/test_vpc_redundant smoke/test_vpc_router_nics @@ -214,30 +254,16 @@ jobs: smoke/test_list_service_offerings smoke/test_list_storage_pools smoke/test_list_volumes"] - steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 + persist-credentials: false + - name: Setup Environment + uses: ./.github/actions/setup-env with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.10' - architecture: 'x64' - - - name: Install Build Dependencies - run: | - sudo apt-get update - sudo apt-get install -y git uuid-runtime genisoimage netcat-openbsd ipmitool build-essential libgcrypt20 libgpg-error-dev libgpg-error0 libopenipmi0 ipmitool libpython3-dev libssl-dev libffi-dev python3-openssl python3-dev python3-setuptools - + install-python: 'true' + install-apt-deps: 'true' - name: Setup IPMI Tool for CloudStack run: | # Create cloudstack-common directory if it doesn't exist @@ -255,55 +281,43 @@ jobs: /usr/share/cloudstack-common/ipmitool -C3 $@ EOF sudo chmod 755 /usr/bin/ipmitool - - name: Install Python dependencies run: | python3 -m pip install --user --upgrade urllib3 lxml paramiko nose texttable ipmisim pyopenssl pycryptodome mock flask netaddr pylint pycodestyle six astroid pynose - - name: Install jacoco dependencies run: | wget https://github.com/jacoco/jacoco/releases/download/v0.8.10/jacoco-0.8.10.zip unzip jacoco-0.8.10.zip -d jacoco - - - name: Env details - run: | - uname -a - whoami - javac -version - mvn -v - python3 --version - free -m - nproc - git status - ipmitool -V - - name: Setup MySQL Server run: | # https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2004-Readme.md#mysql sudo apt-get install -y mysql-server sudo systemctl start mysql - sudo mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY ''; FLUSH PRIVILEGES;" + sudo mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY ''; FLUSH PRIVILEGES;" sudo systemctl restart mysql sudo mysql -uroot -e "SELECT VERSION();" - - - name: Build with Maven + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: build-artifacts + path: /tmp/artifacts/ + - name: Extract artifacts run: | - mvn -B -P developer,systemvm -Dsimulator clean install -DskipTests=true -T$(nproc) - + tar -xzf /tmp/artifacts/targets.tar.gz + mkdir -p ~/.m2/repository + tar -xzf /tmp/artifacts/m2-cloudstack.tar.gz -C ~/.m2/repository - name: Setup Simulator Prerequisites run: | sudo python3 -m pip install --upgrade netaddr mysql-connector-python python3 -m pip install --user --upgrade tools/marvin/dist/[mM]arvin-*.tar.gz mvn -q -Pdeveloper -pl developer -Ddeploydb mvn -q -Pdeveloper -pl developer -Ddeploydb-simulator - - name: Generate jacoco-coverage.sh run: | echo "java -jar jacoco/lib/jacococli.jar report jacoco-it.exec \\" > jacoco-report.sh find . | grep "target/classes" | sed 's/\/classes\//\/classes /g' | awk '{print "--classfiles", $1, "\\"}' | sort |uniq >> jacoco-report.sh find . | grep "src/main/java" | sed 's/\/java\//\/java /g' | awk '{print "--sourcefiles", $1, "\\"}' | sort | uniq >> jacoco-report.sh echo "--xml jacoco-coverage.xml" >> jacoco-report.sh - - name: Start CloudStack Management Server with Simulator run: | export MAVEN_OPTS="-Xmx4096m -XX:MaxMetaspaceSize=800m -Djava.security.egd=file:/dev/urandom -javaagent:jacoco/lib/jacocoagent.jar=address=*,port=36320,output=tcpserver --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED" @@ -314,7 +328,6 @@ jobs: set -e echo -e "\nStarting Advanced Zone DataCenter deployment" python3 tools/marvin/marvin/deployDataCenter.py -i setup/dev/advdualzone.cfg 2>&1 || true - - name: Run Integration Tests with Simulator run: | mkdir -p integration-test-results/smoke/misc @@ -334,13 +347,12 @@ jobs: bash jacoco-report.sh mvn -Dsimulator -pl client jetty:stop 2>&1 find /tmp//MarvinLogs -type f -exec echo -e "Printing marvin logs {} :\n" \; -exec cat {} \; - - name: Integration Tests Result run: | echo -e "Simulator CI Test Results: (only failures listed)\n" python3 ./tools/marvin/xunit-reader.py integration-test-results/ - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: jacoco-coverage.xml fail_ci_if_error: true diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index fbd944a758f9..000000000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. - -name: Coverage Check - -on: [pull_request, push] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.repository == 'apache/cloudstack' - name: codecov - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - - - name: Build CloudStack with Quality Checks - run: | - git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss - cd nonoss && bash -x install-non-oss.sh && cd .. - mvn -P quality -Dsimulator -Dnoredist clean install -T$(nproc) - - - uses: codecov/codecov-action@v4 - with: - files: ./client/target/site/jacoco-aggregate/jacoco.xml - fail_ci_if_error: true - flags: unittests - verbose: true - name: codecov - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 74e59aa821d1..f6bc65ad4b5b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,14 +35,16 @@ jobs: language: ["actions"] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "Security" diff --git a/.github/workflows/copilot-token-health.yml b/.github/workflows/copilot-token-health.yml new file mode 100644 index 000000000000..f421dfbc8375 --- /dev/null +++ b/.github/workflows/copilot-token-health.yml @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# Manual health check for the pool of volunteer Copilot tokens (see .github/COPILOT_TOKENS.md). +# Trigger it from the Actions tab to find dead tokens in GH_AW_COPILOT_TOKEN_NAMES so they can be +# pruned. Only HTTP status codes are printed, never account logins. A 200 just means the token is +# live; there is no endpoint to check whether its monthly Copilot requests are used up. +name: Copilot token health + +on: + workflow_dispatch: {} + +permissions: {} + +jobs: + resolve: + runs-on: ubuntu-latest + outputs: + names: ${{ steps.list.outputs.names }} + steps: + - id: list + env: + NAMES: ${{ vars.GH_AW_COPILOT_TOKEN_NAMES || '[]' }} + run: echo "names=$NAMES" >> "$GITHUB_OUTPUT" + + check: + needs: resolve + if: ${{ needs.resolve.outputs.names != '[]' && needs.resolve.outputs.names != '' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + name: ${{ fromJson(needs.resolve.outputs.names) }} + steps: + - name: Check token liveness + env: + TOKEN: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', matrix.name)] }} + run: | + set -euo pipefail + if [ -z "${TOKEN:-}" ]; then + echo "::error::no secret COPILOT_GITHUB_TOKEN_${{ matrix.name }} found for registered alias '${{ matrix.name }}'" + exit 1 + fi + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $TOKEN" https://api.github.com/user || echo 000) + echo "token '${{ matrix.name }}': HTTP $code" + if [ "$code" != "200" ]; then + echo "::error::token '${{ matrix.name }}' is not live (HTTP $code), consider removing it from GH_AW_COPILOT_TOKEN_NAMES" + exit 1 + fi diff --git a/.github/workflows/daily-issue-triage.lock.yml b/.github/workflows/daily-issue-triage.lock.yml new file mode 100644 index 000000000000..bc9716b7b1fa --- /dev/null +++ b/.github/workflows/daily-issue-triage.lock.yml @@ -0,0 +1,1478 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"44bf96d0f69a352a086fae0139fbae37562f8568a3095458e0bb0d80eaf73a98","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot","agent_model":"claude-sonnet-5"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55","digest":"sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55","digest":"sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55","digest":"sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19","digest":"sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Scheduled daily triage that processes untriaged CloudStack issues in batches. +# Detects duplicates, filters spam, and assigns CloudStack-specific labels +# (type:*, component:*, Severity:*, status:*), then posts a structured triage report. +# +# Source: githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca +# - ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Daily Issue Triage" +on: + schedule: + - cron: "26 13 * * 1-5" + # Friendly format: daily around 14:00 on weekdays (scattered) + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Daily Issue Triage" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: "claude-sonnet-5" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "daily-issue-triage.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' + + GH_AW_PROMPT_7885f18f67e7c010_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' + + Tools: add_comment(max:10), add_labels(max:10), missing_tool, missing_data, noop + + GH_AW_PROMPT_7885f18f67e7c010_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_7885f18f67e7c010_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' + + {{#runtime-import .github/workflows/daily-issue-triage.md}} + GH_AW_PROMPT_7885f18f67e7c010_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - pick_copilot_token + runs-on: ubuntu-latest + permissions: read-all + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: dailyissuetriage + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_12891f19d2c5a9d1_EOF' + {"add_comment":{"max":10,"target":"*"},"add_labels":{"max":10,"target":"*"},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_12891f19d2c5a9d1_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Maximum 10 label(s) can be added. Target: *." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_f4f9bbb59e3fefdc_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "issues,labels" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_f4f9bbb59e3fefdc_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 60 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55,squid=sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca,agent=sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731,api-proxy=sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name != '' && secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: claude-sonnet-5 + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name != '' && secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - pick_copilot_token + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-daily-issue-triage" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "false" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "false" + GH_AW_REPORT_INCOMPLETE_TITLE_PREFIX: "[incomplete]" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "daily-issue-triage" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "false" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "60" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Daily Issue Triage" + WORKFLOW_DESCRIPTION: "Scheduled daily triage that processes untriaged CloudStack issues in batches.\nDetects duplicates, filters spam, and assigns CloudStack-specific labels\n(type:*, component:*, Severity:*, status:*), then posts a structured triage report." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: claude-sonnet-5 + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + pick_copilot_token: + needs: activation + runs-on: ubuntu-latest + outputs: + name: ${{ steps.pick.outputs.name }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Compute candidate names by date + id: names + run: | + set -euo pipefail + NAMES=() + if [ -n "${NAMES_JSON:-}" ]; then + mapfile -t NAMES < <(printf '%s' "$NAMES_JSON" | jq -r '.[]') + fi + N=${#NAMES[@]} + K=3 # today's pick plus 2 fallbacks in case it's dead + if [ "$N" -eq 0 ]; then + for o in $(seq 0 $((K-1))); do echo "name_$o=" >> "$GITHUB_OUTPUT"; done + echo "GH_AW_COPILOT_TOKEN_NAMES is empty -> agent will use base COPILOT_GITHUB_TOKEN" + exit 0 + fi + DOY=$(date -u +%-j) + # slot 1 starts half the pool away from slot 0 so the two workflows + # pick different tokens whenever the pool has at least 2 + START=$(( (DOY - 1 + ROTATION_SLOT * ((N + 1) / 2)) % N )) + for o in $(seq 0 $((K-1))); do + i=$(( (START + o) % N )) + echo "name_$o=${NAMES[$i]}" >> "$GITHUB_OUTPUT" + done + env: + NAMES_JSON: ${{ vars.GH_AW_COPILOT_TOKEN_NAMES }} + ROTATION_SLOT: "1" + - name: Pick first live token name + id: pick + run: | + set -euo pipefail + live() { + [ -n "$1" ] && [ "$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $1" https://api.github.com/user || echo 000)" = "200" ] + } + for pair in "$NAME_0|$CAND_0" "$NAME_1|$CAND_1" "$NAME_2|$CAND_2"; do + nm="${pair%%|*}"; tok="${pair#*|}" + if [ -z "$tok" ]; then continue; fi + echo "::add-mask::$tok" + if live "$tok"; then + echo "name=$nm" >> "$GITHUB_OUTPUT" + echo "Selected rotated token '$nm'" + exit 0 + fi + done + # empty name makes the agent job fall back to the base COPILOT_GITHUB_TOKEN secret + [ -n "$BASE" ] && echo "::add-mask::$BASE" + echo "name=" >> "$GITHUB_OUTPUT" + if live "$BASE"; then echo "Falling back to base COPILOT_GITHUB_TOKEN"; else + echo "WARNING: no live Copilot token (rotated or base)" >&2; fi + env: + BASE: ${{ secrets.COPILOT_GITHUB_TOKEN }} + CAND_0: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_0)] }} + CAND_1: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_1)] }} + CAND_2: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_2)] }} + NAME_0: ${{ steps.names.outputs.name_0 }} + NAME_1: ${{ steps.names.outputs.name_1 }} + NAME_2: ${{ steps.names.outputs.name_2 }} + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-issue-triage" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: "claude-sonnet-5" + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "daily-issue-triage" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"add_labels\":{\"max\":10,\"target\":\"*\"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore diff --git a/.github/workflows/daily-issue-triage.md b/.github/workflows/daily-issue-triage.md new file mode 100644 index 000000000000..c3bf7b485335 --- /dev/null +++ b/.github/workflows/daily-issue-triage.md @@ -0,0 +1,255 @@ +--- +description: | + Scheduled daily triage that processes untriaged CloudStack issues in batches. + Detects duplicates, filters spam, and assigns CloudStack-specific labels + (type:*, component:*, Severity:*, status:*), then posts a structured triage report. + +name: Daily Issue Triage + +on: + schedule: daily around 14:00 on weekdays + workflow_dispatch: + +permissions: read-all + +network: defaults + +# claude-sonnet-5: frontier agentic model at promotional $2/$10 per 1M tokens — +# better and cheaper than the claude-sonnet-4.6 engine default this workflow +# previously fell back to. Triage needs real judgment (duplicates, severity). +engine: + id: copilot + model: claude-sonnet-5 + +# Rotates the Copilot token across volunteer PATs, see .github/COPILOT_TOKENS.md. +# Strict mode forbids reading secrets in the agent job, so this job picks today's +# token and outputs its alias only; the agent job resolves the secret itself. +# ROTATION_SLOT 1 staggers this workflow half the pool away from +# weekly-repo-status so the two pick different tokens (pool of 2 or more). +# After `gh aw compile`, run `bash .github/scripts/post-compile.sh` to re-wire the +# agent job to this output. +jobs: + pick_copilot_token: + runs-on: ubuntu-latest + outputs: + name: ${{ steps.pick.outputs.name }} + steps: + - name: Compute candidate names by date + id: names + env: + NAMES_JSON: "${{ vars.GH_AW_COPILOT_TOKEN_NAMES }}" + ROTATION_SLOT: "1" + run: | + set -euo pipefail + NAMES=() + if [ -n "${NAMES_JSON:-}" ]; then + mapfile -t NAMES < <(printf '%s' "$NAMES_JSON" | jq -r '.[]') + fi + N=${#NAMES[@]} + K=3 # today's pick plus 2 fallbacks in case it's dead + if [ "$N" -eq 0 ]; then + for o in $(seq 0 $((K-1))); do echo "name_$o=" >> "$GITHUB_OUTPUT"; done + echo "GH_AW_COPILOT_TOKEN_NAMES is empty -> agent will use base COPILOT_GITHUB_TOKEN" + exit 0 + fi + DOY=$(date -u +%-j) + # slot 1 starts half the pool away from slot 0 so the two workflows + # pick different tokens whenever the pool has at least 2 + START=$(( (DOY - 1 + ROTATION_SLOT * ((N + 1) / 2)) % N )) + for o in $(seq 0 $((K-1))); do + i=$(( (START + o) % N )) + echo "name_$o=${NAMES[$i]}" >> "$GITHUB_OUTPUT" + done + - name: Pick first live token name + id: pick + env: + NAME_0: "${{ steps.names.outputs.name_0 }}" + NAME_1: "${{ steps.names.outputs.name_1 }}" + NAME_2: "${{ steps.names.outputs.name_2 }}" + CAND_0: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_0)] }}" + CAND_1: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_1)] }}" + CAND_2: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_2)] }}" + BASE: "${{ secrets.COPILOT_GITHUB_TOKEN }}" + run: | + set -euo pipefail + live() { + [ -n "$1" ] && [ "$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $1" https://api.github.com/user || echo 000)" = "200" ] + } + for pair in "$NAME_0|$CAND_0" "$NAME_1|$CAND_1" "$NAME_2|$CAND_2"; do + nm="${pair%%|*}"; tok="${pair#*|}" + if [ -z "$tok" ]; then continue; fi + echo "::add-mask::$tok" + if live "$tok"; then + echo "name=$nm" >> "$GITHUB_OUTPUT" + echo "Selected rotated token '$nm'" + exit 0 + fi + done + # empty name makes the agent job fall back to the base COPILOT_GITHUB_TOKEN secret + [ -n "$BASE" ] && echo "::add-mask::$BASE" + echo "name=" >> "$GITHUB_OUTPUT" + if live "$BASE"; then echo "Falling back to base COPILOT_GITHUB_TOKEN"; else + echo "WARNING: no live Copilot token (rotated or base)" >&2; fi + +safe-outputs: + # Don't open tracking issues when the agentic run itself fails or is unhealthy + report-failure-as-issue: false + missing-tool: + create-issue: false + report-incomplete: + create-issue: false + add-labels: + target: "*" + max: 10 + add-comment: + target: "*" + max: 10 + +tools: + web-fetch: + github: + toolsets: [issues, labels] + min-integrity: none + +source: githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 +timeout-minutes: 60 +--- + +# Daily Issue Triage + + + +You are a batch triage assistant for GitHub issues in **${{ github.repository }}** (Apache CloudStack). Your task is to find untriaged issues and triage them one by one. Your triage comments are written for maintainers reviewing the triage, not for the issue author. + +Do not make assumptions beyond what the issue content supports. Do not invent missing context. + +## Step 1: Find untriaged issues + +Use the `search_issues` tool to find open issues that need triage. An issue is considered untriaged if it has **no labels applied**. + +Query: `repo:${{ github.repository }} is:issue is:open no:label` + +Paginate through all results to find untriaged issues. Do not stop at the first page. + +From the results, filter out: +- Issues that already have a triage comment (look for "🎯 Triage report" in comments). **Never retriage an issue that has already been triaged.** +- Issues created by bots (unless they look like real user issues). +- Issues that have any labels already applied (even if they weren't applied by this workflow). + +Process the **oldest untriaged issues first**. Note: this workflow is capped at 10 label-sets and 10 comments per run, so the backlog will drain over several daily runs — that is intentional. + +## Step 2: Fetch labels (once) + +Before triaging any issues, fetch the list of labels available in this repository using the `list_labels` tool. Use this live list for all issues in the batch — only apply labels that actually exist in the repository. + +CloudStack uses a prefixed label taxonomy. Choose from these families: + +- **Type** (pick the single best one): `type:bug`, `type:new-feature`, `type:enhancement`, `type:improvement`, `type:regression`, `type:security`, `type:question`, `type:config`, `type:cleanup` +- **Component** (apply when clearly identifiable; more than one is allowed): e.g. `component:kvm`, `component:vmware`, `component:XenServer`, `component:api`, `component:UI`, `component:networking`, `component:virtual-router`, `component:management-server`, `component:primary-storage`, `component:secondary-storage`, `component:kubernetes`, `component:database`, and others — use the full list returned by `list_labels`. +- **Severity** (bugs only, when assessable): `Severity:BLOCKER`, `Severity:Critical`, `Severity:Major`, `Severity:Minor`, `Severity:Trivial` +- **Duplicate / invalid**: `status:duplicate`, `status:invalid` +- **Help wanted / newcomer-friendly**: `status:Help-wanted` + +## Step 3: Triage each issue + +For each untriaged issue, perform the following steps: + +### 3a: Gather context + +1. Retrieve the full issue content using the `get_issue` tool. +2. Fetch any comments on the issue using the `get_issue_comments` tool. +3. Search for similar issues using the `search_issues` tool. + +### 3b: Spam and quality check + +**Spam and invalid issues:** If the issue is obviously spam, bot-generated, gibberish, or a test issue: +- Apply the `status:invalid` label. +- **Do not close the issue** — closing is a human decision. Note in the report that it looks like spam/invalid so a maintainer can act. +- Move to the next issue. + +**Incomplete issues:** If the issue lacks enough detail for meaningful triage, add a comment that politely asks the author to provide the missing information: +- For bugs: steps to reproduce, expected vs actual behavior, logs/errors, environment details (CloudStack version, hypervisor, etc.). +- For other issue types: equivalent details that would make the report actionable. +- Apply a `type:question` label if appropriate. +- Be specific about what is missing and why it is needed. +- Move to the next issue. + +### 3c: Select labels + +- Be cautious with labels; they can trigger automation. +- Choose a single `type:*` label that best reflects the issue's nature. +- Add `component:*` label(s) when the affected area is clear from the content. +- Add a `Severity:*` label for bugs when severity can be reasonably assessed. +- Do not apply labels that do not exist in the repository. +- It is better to under-label than to speculatively add labels. + +### 3d: Detect duplicates and related issues + +- Review the similar issues found in Step 3a. +- Classify matches as: + - **Duplicate** (high confidence): the issue describes the same problem as an existing open issue. Include up to 3. + - **Related**: similar domain or adjacent problem, but not a duplicate. Include up to 3. +- If a high-confidence duplicate is found, apply the `status:duplicate` label. +- If no similar issues are found, state that explicitly in your report. + +### 3e: Assess coding agent suitability + +Assess whether the issue is suitable for automated coding agent assignment: +- **Suitable**: clear requirements, sufficient context, well-defined success criteria, self-contained scope. +- **Needs more info**: potentially suitable but missing details needed to start. +- **Not suitable**: requires investigation, design decisions, extensive coordination, or policy/architectural choices. + +### 3f: Additional analysis + +- Search the web for relevant documentation, error messages, or known solutions if applicable. +- Write notes, debugging strategies, and/or reproduction steps relevant to the issue. +- Suggest resources or links that might help resolve the issue. + +### 3g: Apply results and post comment + +Apply all triage results for this issue: +- Use `update_issue` to apply the chosen labels. +- Add an issue comment with the triage report using the format below. + +Then move to the next issue. + +## Processing order + +1. Fetch available labels (Step 2, once at the start). +2. Find untriaged issues (Step 1). +3. For each issue (oldest first), run Step 3 (gather, check, label, detect duplicates, comment). + +## Comment format + +Use this structure for each triage comment. Use collapsed sections to keep it tidy. + +```markdown +## 🎯 Triage report + +{2-3 sentence summary to help a maintainer quickly grasp the issue.} + +### 📊 Assessment + +| Dimension | Value | Reasoning | +|---|---|---| +| **Type** | [type:* label or "none"] | [brief] | +| **Component** | [component:* label(s) or "none"] | [brief] | +| **Severity** | [Severity:* label or "n/a"] | [brief] | +| **Labels** | [all labels applied or "none"] | [brief] | +| **Coding agent** | [Suitable / Needs more info / Not suitable] | [brief] | + +### 🔗 Similar issues + +- issue-url (duplicate/related) — [brief explanation] + +
💡 Notes and suggestions + +{Debugging strategies, reproduction steps, resource links, sub-task checklists, nudges for the team.} + +
+``` + +If no similar issues were found, omit the "Similar issues" section. If there are no notes to add, omit the collapsed section. + +**Important**: Never close issues. Only apply labels and post comments. diff --git a/.github/workflows/docker-cloudstack-simulator.yml b/.github/workflows/docker-cloudstack-simulator.yml index af6cbf49f5ef..ac778a48af54 100644 --- a/.github/workflows/docker-cloudstack-simulator.yml +++ b/.github/workflows/docker-cloudstack-simulator.yml @@ -35,10 +35,10 @@ concurrency: jobs: build: if: github.repository == 'apache/cloudstack' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Login to Docker Registry - uses: docker/login-action@v2 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKERHUB_USER }} @@ -47,7 +47,9 @@ jobs: - name: Set Docker repository name run: echo "DOCKER_REPOSITORY=apache" >> $GITHUB_ENV - - uses: actions/checkout@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Set ACS version run: echo "ACS_VERSION=$(grep '' pom.xml | head -2 | tail -1 | cut -d'>' -f2 |cut -d'<' -f1)" >> $GITHUB_ENV diff --git a/.github/workflows/main-sonar-check.yml b/.github/workflows/main-sonar-check.yml index 224ea2cde801..f48409fbc0c3 100644 --- a/.github/workflows/main-sonar-check.yml +++ b/.github/workflows/main-sonar-check.yml @@ -15,54 +15,53 @@ # specific language governing permissions and limitations # under the License. -name: Main Branch Sonar Quality Check - +name: Sonar Quality Check (Main) +permissions: + contents: read on: push: branches: - main - -permissions: - contents: read # to fetch code (actions/checkout) - pull-requests: write # for sonar to comment on pull-request - +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: build: if: github.repository == 'apache/cloudstack' - name: Main Sonar JaCoCo Build - runs-on: ubuntu-22.04 + name: Sonar JaCoCo Coverage + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - - - name: Set up JDK17 - uses: actions/setup-java@v5 + persist-credentials: false + - name: Setup Environment + uses: ./.github/actions/setup-env with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - + install-python: 'true' + install-apt-deps: 'true' - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - - name: Cache local Maven repository - uses: actions/cache@v5 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('pom.xml', '*/pom.xml', '*/*/pom.xml', '*/*/*/pom.xml') }} - restore-keys: | - ${{ runner.os }}-m2 - - - name: Run Tests with Coverage + - name: Install Non-OSS + uses: ./.github/actions/install-nonoss + - name: Run Build and Tests with Coverage + run: mvn -B -T$(nproc) -P developer,systemvm,quality -Dsimulator -Dnoredist clean install + - name: Upload to SonarQube env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss - cd nonoss && bash -x install-non-oss.sh && cd .. - mvn -T$(nproc) -P quality -Dsimulator -Dnoredist clean install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack + run: mvn -B -P quality org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.branch.name=${{ github.ref_name }} + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + files: ./client/target/site/jacoco-aggregate/jacoco.xml + fail_ci_if_error: true + flags: unittests + verbose: true + name: codecov + token: ${{ secrets.CODECOV_TOKEN }} + - name: Compute Coverage Grade + run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml diff --git a/.github/workflows/merge-conflict-checker.yml b/.github/workflows/merge-conflict-checker.yml index 860e1c1b5614..badf8c4b4c5b 100644 --- a/.github/workflows/merge-conflict-checker.yml +++ b/.github/workflows/merge-conflict-checker.yml @@ -17,28 +17,26 @@ name: "PR Merge Conflict Check" on: - push: - pull_request_target: - types: [synchronize] + schedule: + - cron: '*/10 * * * *' + workflow_dispatch: -permissions: # added using https://github.com/step-security/secure-workflows - contents: read +permissions: {} concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: "gh-aw-${{ github.workflow }}" jobs: triage: permissions: - pull-requests: write # for eps1lon/actions-label-merge-conflict to label PRs - runs-on: ubuntu-22.04 + pull-requests: write # for eps1lon/actions-label-merge-conflict to label PRs + runs-on: ubuntu-24.04 steps: - - name: Conflict Check - uses: eps1lon/actions-label-merge-conflict@v2.0.0 - with: - repoToken: "${{ secrets.GITHUB_TOKEN }}" - dirtyLabel: "status:has-conflicts" - removeOnDirtyLabel: "status:ready-for-review" - continueOnMissingPermissions: true - commentOnDirty: "This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch." + - name: Conflict Check + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + dirtyLabel: "status:has-conflicts" + removeOnDirtyLabel: "status:ready-for-review" + continueOnMissingPermissions: true + commentOnDirty: "This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch." diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 11fe5c068814..4de995479c59 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -29,17 +29,23 @@ concurrency: jobs: pre-commit: name: Run pre-commit - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Check Out - uses: actions/checkout@v5 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.11' + cache: 'pip' - name: Install - run: | - python -m pip install --upgrade pip - pip install pre-commit + run: pip install pre-commit - name: Set PY run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - - uses: actions/cache@v5 + - name: Cache pre-commit environments + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} diff --git a/.github/workflows/rat.yml b/.github/workflows/rat.yml index d71f4b0852d8..b75ff42b9122 100644 --- a/.github/workflows/rat.yml +++ b/.github/workflows/rat.yml @@ -16,32 +16,31 @@ # under the License. name: License Check - -on: [push, pull_request] - +on: + push: + branches: + - main + - 4.22 + - 4.20 + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read - jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 - - name: Set up JDK 17 - uses: actions/setup-java@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - java-version: '17' - distribution: 'adopt' - architecture: x64 - cache: maven + persist-credentials: false + - name: Setup Environment + uses: ./.github/actions/setup-env + - name: Install Non-OSS + uses: ./.github/actions/install-nonoss - name: RAT licence checks run: | - git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss && cd nonoss && bash -x install-non-oss.sh && cd .. - rm -fr nonoss mvn -P developer,systemvm -Dsimulator -Dnoredist -pl . org.apache.rat:apache-rat-plugin:0.12:check - name: Rat Report if: always() diff --git a/.github/workflows/sonar-check.yml b/.github/workflows/sonar-check.yml index 31fb671cc58f..7f2a1bdb2935 100644 --- a/.github/workflows/sonar-check.yml +++ b/.github/workflows/sonar-check.yml @@ -16,58 +16,118 @@ # under the License. name: Sonar Quality Check - -on: [pull_request] - permissions: - contents: read # to fetch code (actions/checkout) - pull-requests: write # for sonar to comment on pull-request - + contents: read + pull-requests: write +on: + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: build: - if: github.repository == 'apache/cloudstack' && github.event.pull_request.head.repo.full_name == github.repository name: Sonar JaCoCo Coverage - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: "refs/pull/${{ github.event.number }}/merge" fetch-depth: 0 - - - name: Set up JDK17 - uses: actions/setup-java@v5 + persist-credentials: false + - name: Setup Environment + uses: ./.github/actions/setup-env with: - distribution: 'temurin' - java-version: '17' - cache: 'maven' - + install-python: 'true' + install-apt-deps: 'true' - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - - name: Cache local Maven repository - uses: actions/cache@v5 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('pom.xml', '*/pom.xml', '*/*/pom.xml', '*/*/*/pom.xml') }} - restore-keys: | - ${{ runner.os }}-m2 - + - name: Install Non-OSS + uses: ./.github/actions/install-nonoss - name: Run Build and Tests with Coverage - id: coverage + run: mvn -B -T$(nproc) -P developer,systemvm,quality -Dsimulator -Dnoredist clean install + - name: Upload to SonarQube + if: github.repository == 'apache/cloudstack' && github.event.pull_request.head.repo.full_name == github.repository env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} PR_ID: ${{ github.event.pull_request.number }} HEADREF: ${{ github.event.pull_request.head.ref }} run: | - git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss - cd nonoss && bash -x install-non-oss.sh && cd .. - mvn -T$(nproc) -P quality -Dsimulator -Dnoredist clean install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.pullrequest.key="$PR_ID" -Dsonar.pullrequest.branch="$HEADREF" -Dsonar.pullrequest.github.repository=apache/cloudstack -Dsonar.pullrequest.provider=GitHub -Dsonar.pullrequest.github.summary_comment=true + mvn -B -P quality org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.pullrequest.key="$PR_ID" -Dsonar.pullrequest.branch="$HEADREF" -Dsonar.pullrequest.github.repository=apache/cloudstack -Dsonar.pullrequest.provider=GitHub -Dsonar.pullrequest.github.summary_comment=true + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + files: ./client/target/site/jacoco-aggregate/jacoco.xml + fail_ci_if_error: true + flags: unittests + verbose: true + name: codecov + token: ${{ secrets.CODECOV_TOKEN }} + - name: Compute Coverage Grade + id: grade + run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml + - name: Post Coverage Grade Comment on PR + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const grade = '${{ steps.grade.outputs.coverage_grade }}'; + const label = '${{ steps.grade.outputs.coverage_grade_label }}'; + const linePct = '${{ steps.grade.outputs.line_coverage }}'; + const branchPct = '${{ steps.grade.outputs.branch_coverage }}'; + const emojiMap = { A: '🟢', B: '🟡', C: '🟠', D: '🔴', F: '⛔' }; + const emoji = emojiMap[grade] ?? '❓'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const marker = ''; + + const branchRow = branchPct !== 'N/A' + ? `| Branch coverage | **${branchPct}%** |` + : ''; + + const body = [ + marker, + `## ${emoji} Test Coverage Grade: \`${grade}\` — ${label}`, + '', + '| Metric | Value |', + '|--------|-------|', + `| Line coverage | **${linePct}%** |`, + branchRow, + '', + '### Grade Scale', + '| Grade | Line Coverage | Meaning |', + '|-------|--------------|---------|', + '| 🟢 A | ≥ 80% | Excellent - this code sleeps well at night 😴 |', + '| 🟡 B | 60-79% | Good - almost there, don\'t stop now 😉 |', + '| 🟠 C | 40-59% | Acceptable - your code is wearing a seatbelt, but no airbags 😬 |', + '| 🔴 D | 20-39% | Marginal - boldly shipping where no test has gone before 🖖 |', + '| ⛔ F | < 20% | Failing - tests? what tests? 🔥 |', + '', + '> Branch coverage is shown as a secondary signal. Grade is determined by **line coverage**.', + `> [View full Actions run](${runUrl})`, + ].filter(l => l !== undefined).join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + console.log('Updated existing coverage grade comment'); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + console.log('Posted coverage grade comment'); + } diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 842e4497a4ad..c0da5f98cc8e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -28,7 +28,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v10 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. It may be removed by administrators of this project at any time. Remove the stale label or comment to request for removal of it to prevent this.' stale-pr-message: 'This PR is stale because it has been open for 120 days with no activity. It may be removed by administrators of this project at any time. Remove the stale label or comment to request for removal of it to prevent this.' @@ -41,7 +41,7 @@ jobs: days-before-pr-close: 240 exempt-issue-labels: 'gsoc,good-first-issue,long-term-plan' exempt-pr-labels: 'status:ready-for-merge,status:needs-testing,status:on-hold' - - uses: actions/stale@v10 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: stale-issue-label: 'archive' days-before-stale: 240 diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 56b04a6f9c96..7f23add660f0 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -17,7 +17,13 @@ name: UI Build -on: [push, pull_request] +on: + push: + branches: + - main + - 4.22 + - 4.20 + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -28,15 +34,19 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Set up Node - uses: actions/setup-node@v5 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 16 + cache: 'npm' + cache-dependency-path: 'ui/package-lock.json' - name: Env details run: | @@ -55,7 +65,7 @@ jobs: npm run lint npm run test:unit - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 if: github.repository == 'apache/cloudstack' with: working-directory: ui diff --git a/.github/workflows/weekly-repo-status.lock.yml b/.github/workflows/weekly-repo-status.lock.yml new file mode 100644 index 000000000000..63198d8602dd --- /dev/null +++ b/.github/workflows/weekly-repo-status.lock.yml @@ -0,0 +1,1471 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b5de248d6646003240bf108bb14834943aea8fd06334c62d6c08e396400ab2dc","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot","agent_model":"gpt-5.6-luna"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"46d564922b082d0db93244972e8005ea6904ee5f","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55","digest":"sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55","digest":"sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55","digest":"sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19","digest":"sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit githubnext/agentics/workflows/repo-status.md@main and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# This workflow creates weekly repo status reports. It gathers recent repository +# activity (issues, PRs, discussions, releases, code changes) and generates +# engaging GitHub issues with productivity insights, community highlights, +# and project recommendations. +# +# Source: githubnext/agentics/workflows/repo-status.md@main +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca +# - ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + +name: "Weekly Repo Status" +on: + schedule: + - cron: "0 12 * * 0" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Weekly Repo Status" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/weekly-repo-status.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: "gpt-5.6-luna" + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "weekly-repo-status.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' + + GH_AW_PROMPT_4dd73a0378be2614_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + + GH_AW_PROMPT_4dd73a0378be2614_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_4dd73a0378be2614_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' + + {{#runtime-import .github/workflows/weekly-repo-status.md}} + GH_AW_PROMPT_4dd73a0378be2614_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: + - activation + - pick_copilot_token + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: weeklyrepostatus + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/weekly-repo-status.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca ghcr.io/github/gh-aw-mcpg:v0.3.19@sha256:a6c890d7c24d7190c9ef97b9c954cc4cffaae6b01c371ced1f959f1370b1f68f ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_47d10a8743973435_EOF' + {"create_issue":{"close_older_issues":true,"labels":["report","weekly-status"],"max":1,"title_prefix":"[repo-status] "},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_47d10a8743973435_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[repo-status] \". Labels [\"report\" \"weekly-status\"] will be automatically added." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "fields": { + "type": "array" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_3ad3d264c5e0c6f9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_3ad3d264c5e0c6f9_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55,squid=sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca,agent=sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731,api-proxy=sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name != '' && secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: gpt-5.6-luna + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ needs.pick_copilot_token.outputs.name != '' && secrets[format('COPILOT_GITHUB_TOKEN_{0}', needs.pick_copilot_token.outputs.name)] || secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GH_AW_ALLOWED_GITHUB_REFS: "" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - pick_copilot_token + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + concurrency: + group: "gh-aw-conclusion-weekly-repo-status" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/weekly-repo-status.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "false" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "false" + GH_AW_REPORT_INCOMPLETE_TITLE_PREFIX: "[incomplete]" + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "weekly-repo-status" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "false" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "20" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/weekly-repo-status.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55@sha256:4142b873b678cd3279b98dcbe464857d56ea2f2348719b00379cdf35dd843ff3 ghcr.io/github/gh-aw-firewall/squid:0.25.55@sha256:74084b704d8d3664a363655986664d70bd9cdb4830532d0b35cd784d867aabca + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Weekly Repo Status" + WORKFLOW_DESCRIPTION: "This workflow creates weekly repo status reports. It gathers recent repository\nactivity (issues, PRs, discussions, releases, code changes) and generates\nengaging GitHub issues with productivity insights, community highlights,\nand project recommendations." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: gpt-5.6-luna + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + pick_copilot_token: + needs: activation + runs-on: ubuntu-latest + outputs: + name: ${{ steps.pick.outputs.name }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Compute candidate names by date + id: names + run: | + set -euo pipefail + NAMES=() + if [ -n "${NAMES_JSON:-}" ]; then + mapfile -t NAMES < <(printf '%s' "$NAMES_JSON" | jq -r '.[]') + fi + N=${#NAMES[@]} + K=3 # today's pick plus 2 fallbacks in case it's dead + if [ "$N" -eq 0 ]; then + for o in $(seq 0 $((K-1))); do echo "name_$o=" >> "$GITHUB_OUTPUT"; done + echo "GH_AW_COPILOT_TOKEN_NAMES is empty -> agent will use base COPILOT_GITHUB_TOKEN" + exit 0 + fi + DOY=$(date -u +%-j) + # slot 1 starts half the pool away from slot 0 so the two workflows + # pick different tokens whenever the pool has at least 2 + START=$(( (DOY - 1 + ROTATION_SLOT * ((N + 1) / 2)) % N )) + for o in $(seq 0 $((K-1))); do + i=$(( (START + o) % N )) + echo "name_$o=${NAMES[$i]}" >> "$GITHUB_OUTPUT" + done + env: + NAMES_JSON: ${{ vars.GH_AW_COPILOT_TOKEN_NAMES }} + ROTATION_SLOT: "0" + - name: Pick first live token name + id: pick + run: | + set -euo pipefail + live() { + [ -n "$1" ] && [ "$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $1" https://api.github.com/user || echo 000)" = "200" ] + } + for pair in "$NAME_0|$CAND_0" "$NAME_1|$CAND_1" "$NAME_2|$CAND_2"; do + nm="${pair%%|*}"; tok="${pair#*|}" + if [ -z "$tok" ]; then continue; fi + echo "::add-mask::$tok" + if live "$tok"; then + echo "name=$nm" >> "$GITHUB_OUTPUT" + echo "Selected rotated token '$nm'" + exit 0 + fi + done + # empty name makes the agent job fall back to the base COPILOT_GITHUB_TOKEN secret + [ -n "$BASE" ] && echo "::add-mask::$BASE" + echo "name=" >> "$GITHUB_OUTPUT" + if live "$BASE"; then echo "Falling back to base COPILOT_GITHUB_TOKEN"; else + echo "WARNING: no live Copilot token (rotated or base)" >&2; fi + env: + BASE: ${{ secrets.COPILOT_GITHUB_TOKEN }} + CAND_0: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_0)] }} + CAND_1: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_1)] }} + CAND_2: ${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_2)] }} + NAME_0: ${{ steps.names.outputs.name_0 }} + NAME_1: ${{ steps.names.outputs.name_1 }} + NAME_2: ${{ steps.names.outputs.name_2 }} + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/weekly-repo-status" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: "gpt-5.6-luna" + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "weekly-repo-status" + GH_AW_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-status.md@main" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/main/workflows/repo-status.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Weekly Repo Status" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/weekly-repo-status.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"labels\":[\"report\",\"weekly-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore diff --git a/.github/workflows/weekly-repo-status.md b/.github/workflows/weekly-repo-status.md new file mode 100644 index 000000000000..b469b59e9a71 --- /dev/null +++ b/.github/workflows/weekly-repo-status.md @@ -0,0 +1,141 @@ +--- +description: | + This workflow creates weekly repo status reports. It gathers recent repository + activity (issues, PRs, discussions, releases, code changes) and generates + engaging GitHub issues with productivity insights, community highlights, + and project recommendations. + +on: + # 12:00 UTC every Sunday. Fixed cron (not the "weekly" fuzzy shorthand) + # because that scatters the run time to spread load. + schedule: "0 12 * * 0" + workflow_dispatch: + +permissions: + contents: read + issues: read + pull-requests: read + +network: defaults + +# gpt-5.6-luna: cheapest lightweight model ($0.20/$1.20 per 1M tokens) — plenty +# for this formulaic weekly report (see GitHub Copilot models-and-pricing docs) +engine: + id: copilot + model: gpt-5.6-luna + +# Rotates the Copilot token across volunteer PATs, see .github/COPILOT_TOKENS.md. +# Strict mode forbids reading secrets in the agent job, so this job picks today's +# token and outputs its alias only; the agent job resolves the secret itself. +# After `gh aw compile`, run `bash .github/scripts/post-compile.sh` to re-wire the +# agent job to this output. +jobs: + pick_copilot_token: + runs-on: ubuntu-latest + outputs: + name: ${{ steps.pick.outputs.name }} + steps: + - name: Compute candidate names by date + id: names + env: + NAMES_JSON: "${{ vars.GH_AW_COPILOT_TOKEN_NAMES }}" + ROTATION_SLOT: "0" + run: | + set -euo pipefail + NAMES=() + if [ -n "${NAMES_JSON:-}" ]; then + mapfile -t NAMES < <(printf '%s' "$NAMES_JSON" | jq -r '.[]') + fi + N=${#NAMES[@]} + K=3 # today's pick plus 2 fallbacks in case it's dead + if [ "$N" -eq 0 ]; then + for o in $(seq 0 $((K-1))); do echo "name_$o=" >> "$GITHUB_OUTPUT"; done + echo "GH_AW_COPILOT_TOKEN_NAMES is empty -> agent will use base COPILOT_GITHUB_TOKEN" + exit 0 + fi + DOY=$(date -u +%-j) + # slot 1 starts half the pool away from slot 0 so the two workflows + # pick different tokens whenever the pool has at least 2 + START=$(( (DOY - 1 + ROTATION_SLOT * ((N + 1) / 2)) % N )) + for o in $(seq 0 $((K-1))); do + i=$(( (START + o) % N )) + echo "name_$o=${NAMES[$i]}" >> "$GITHUB_OUTPUT" + done + - name: Pick first live token name + id: pick + env: + NAME_0: "${{ steps.names.outputs.name_0 }}" + NAME_1: "${{ steps.names.outputs.name_1 }}" + NAME_2: "${{ steps.names.outputs.name_2 }}" + CAND_0: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_0)] }}" + CAND_1: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_1)] }}" + CAND_2: "${{ secrets[format('COPILOT_GITHUB_TOKEN_{0}', steps.names.outputs.name_2)] }}" + BASE: "${{ secrets.COPILOT_GITHUB_TOKEN }}" + run: | + set -euo pipefail + live() { + [ -n "$1" ] && [ "$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $1" https://api.github.com/user || echo 000)" = "200" ] + } + for pair in "$NAME_0|$CAND_0" "$NAME_1|$CAND_1" "$NAME_2|$CAND_2"; do + nm="${pair%%|*}"; tok="${pair#*|}" + if [ -z "$tok" ]; then continue; fi + echo "::add-mask::$tok" + if live "$tok"; then + echo "name=$nm" >> "$GITHUB_OUTPUT" + echo "Selected rotated token '$nm'" + exit 0 + fi + done + # empty name makes the agent job fall back to the base COPILOT_GITHUB_TOKEN secret + [ -n "$BASE" ] && echo "::add-mask::$BASE" + echo "name=" >> "$GITHUB_OUTPUT" + if live "$BASE"; then echo "Falling back to base COPILOT_GITHUB_TOKEN"; else + echo "WARNING: no live Copilot token (rotated or base)" >&2; fi + +tools: + github: + # If in a public repo, setting `lockdown: false` allows + # reading issues, pull requests and comments from 3rd-parties + # If in a private repo this has no particular effect. + lockdown: false + min-integrity: none # This workflow is allowed to examine and comment on any issues + +safe-outputs: + mentions: false + allowed-github-references: [] + # Don't open tracking issues when the agentic run itself fails or is unhealthy + report-failure-as-issue: false + missing-tool: + create-issue: false + report-incomplete: + create-issue: false + create-issue: + title-prefix: "[repo-status] " + labels: [report, weekly-status] + close-older-issues: true +source: githubnext/agentics/workflows/repo-status.md@main +--- + +# Weekly Repo Status + +Create an upbeat weekly status report for the repo as a GitHub issue. + +## What to include + +- Recent repository activity (issues, PRs, discussions, releases, code changes) +- Progress tracking, goal reminders and highlights +- Project status and recommendations +- Actionable next steps for maintainers + +## Style + +- Be positive, encouraging, and helpful 🌟 +- Use emojis moderately for engagement +- Keep it concise - adjust length based on actual activity + +## Process + +1. Gather recent activity from the repository +2. Study the repository, its issues and its pull requests +3. Create a new GitHub issue with your findings and insights diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49829caf125e..a7b564b0f373 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,26 +26,26 @@ repos: - id: identity - id: check-hooks-apply - repo: https://github.com/thlorenz/doctoc.git - rev: v2.2.0 + rev: v2.3.0 hooks: - id: doctoc name: Add TOC for Markdown files files: ^CONTRIBUTING\.md$|^INSTALL\.md$|^README\.md$ - repo: https://github.com/oxipng/oxipng - rev: v9.1.5 + rev: v10.1.0 hooks: - id: oxipng name: run oxipng description: optimize PNG images with lossless compression args: ['-o', '4', '--strip', 'safe', '--alpha'] - repo: https://github.com/gitleaks/gitleaks - rev: v8.27.2 + rev: v8.30.0 hooks: - id: gitleaks name: run gitleaks description: detect hardcoded secrets - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 + rev: v1.5.6 hooks: - id: chmod name: set file permissions @@ -71,7 +71,7 @@ repos: - --license-filepath - .github/workflows/license-templates/LICENSE.txt - --fuzzy-match-generates-todo - exclude: ^(CHANGES|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)\.md$|^ui/docs/(full|smoke)-test-plan\.template\.md$ + exclude: ^(CHANGES|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)\.md$|^ui/docs/(full|smoke)-test-plan\.template\.md$|^\.github/workflows/.*\.md$|^\.github/aw/.*\.md$ - id: insert-license name: add license for all properties files description: automatically adds a licence header to all properties files that don't have a license header @@ -120,6 +120,7 @@ repos: - --license-filepath - .github/workflows/license-templates/LICENSE.txt - --fuzzy-match-generates-todo + exclude: ^\.github/workflows/.*\.lock\.yml$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: @@ -150,7 +151,7 @@ repos: ^server/src/test/resources/certs/rsa_self_signed\.key$| ^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$| ^systemvm/agent/certs/localhost\.key$| - ^systemvm/agent/certs/realhostip\.key$| + ^systemvm/agent/certs/systemvm\.key$| ^test/integration/smoke/test_ssl_offloading\.py$ - id: end-of-file-fixer exclude: \.vhd$|\.svg$ @@ -161,33 +162,31 @@ repos: - id: forbid-submodules - id: mixed-line-ending - id: trailing-whitespace - files: ^(LICENSE|NOTICE)$|\.(bat|cfg|cs|css|gitignore|header|in|install|java|md|properties|py|rb|rc|sh|sql|te|template|txt|ucls|vue|xml|xsl|yaml|yml)$|^cloud-cli/bindir/cloud-tool$|^debian/changelog$ + files: ^(LICENSE|NOTICE)$|README$|\.(bat|cfg|config|cs|css|erb|gitignore|header|in|install|java|md|properties|py|rb|rc|sh|sql|svg|te|template|txt|ucls|vue|xml|xsl|yaml|yml)$|^cloud-cli/bindir/cloud-tool$|^debian/changelog$ args: [--markdown-linebreak-ext=md] exclude: ^services/console-proxy/rdpconsole/src/test/doc/freerdp-debug-log\.txt$ - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 + rev: v2.4.2 hooks: - id: codespell name: run codespell description: Check spelling with codespell - args: [--ignore-words=.github/linters/codespell.txt] - exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$|^server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java$|^test/integration/smoke/test_ssl_offloading.py$ - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 args: [--config, .github/linters/.flake8] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.45.0 + rev: v0.48.0 hooks: - id: markdownlint name: run markdownlint description: check Markdown files with markdownlint args: [--config=.github/linters/.markdown-lint.yml] types: [markdown] - files: \.(md|mdown|markdown)$ + files: \.md$ - repo: https://github.com/adrienverge/yamllint - rev: v1.37.1 + rev: v1.38.0 hooks: - id: yamllint name: run yamllint @@ -195,4 +194,4 @@ repos: args: [--config-file=.github/linters/.yamllint.yml] types: [yaml] files: \.ya?ml$ - exclude: ^.*k8s-.*\.ya?ml$ + exclude: ^.*k8s-.*\.ya?ml$|^.github/workflows/.*\.lock\.ya?ml$ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..4469efa2f494 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ + + +# Agent Guide for Apache CloudStack + +This file is read by automated agents (security scanners, code +analyzers, AI assistants) operating on this repository. + +## Security + +Security model: [SECURITY.md](./SECURITY.md) + +Agents that scan this repository should consult `SECURITY.md` and the +threat model it links before reporting issues. + +The project-wide security threat model is linked from `SECURITY.md`. diff --git a/README.md b/README.md index a5aacb49f6b5..53516503aac4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ [![Simulator CI](https://github.com/apache/cloudstack/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/apache/cloudstack/actions/workflows/ci.yml) [![UI Build](https://github.com/apache/cloudstack/actions/workflows/ui.yml/badge.svg?branch=main)](https://github.com/apache/cloudstack/actions/workflows/ui.yml) +[![Good First Issues](https://img.shields.io/badge/good%20first%20issues-94D5DA)](https://github.com/apache/cloudstack/issues?q=is%3Aissue%20state%3Aopen%20label%3Agood-first-issue) +[![CloudStack Documentation Status](https://app.readthedocs.org/projects/cloudstack-documentation/badge/?version=latest)](https://app.readthedocs.org/projects/cloudstack-documentation/builds/) +[![CloudStack Website Status](https://img.shields.io/website?url=https%3A%2F%2Fcloudstack.apache.org%2F +)](https://cloudstack.apache.org/) + [![Apache CloudStack](tools/logo/apache_cloudstack.png)](https://cloudstack.apache.org/) @@ -203,14 +208,14 @@ Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Sec The following provides more details on the included cryptographic software: * CloudStack makes use of JaSypt cryptographic libraries. -* CloudStack has a system requirement of MySQL, and uses native database encryption functionality. +* CloudStack requires a MySQL-compatible database (MariaDB or MySQL), and uses native database encryption functionality. * CloudStack makes use of the Bouncy Castle general-purpose encryption library. * CloudStack can optionally interact with and control OpenSwan-based VPNs. * CloudStack has a dependency on and makes use of JSch - a java SSH2 implementation. ## Star History -[![Apache CloudStack Star History](https://api.star-history.com/svg?repos=apache/cloudstack&type=Date)](https://www.star-history.com/#apache/cloudstack&Date) +[![Apache CloudStack Star History](https://star-history.dera.page/svg?repos=apache/cloudstack&type=Date)](https://star-history.dera.page/#apache/cloudstack&Date) ## Contributors diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..018f2fa8cb86 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ + + +# Security Policy + +## Reporting a Vulnerability + +`apache/cloudstack` follows the [Apache Software Foundation security process](https://www.apache.org/security/). Please report suspected +vulnerabilities privately to `security@apache.org`; do not open public GitHub issues or pull requests for security reports. + +For more details, see https://cloudstack.apache.org/security.html. + +## Threat Model + +What the project treats as in scope and out of scope, the security +properties it provides and disclaims, the adversary model, and how +findings are triaged are documented in the project-wide threat model: +[THREAT_MODEL.md](THREAT_MODEL.md). diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md new file mode 100644 index 000000000000..4659b937dedb --- /dev/null +++ b/THREAT_MODEL.md @@ -0,0 +1,1148 @@ + + +# Apache CloudStack Security Threat Model (draft) + +> **Document scope and PMC structural decision.** The CloudStack PMC owns +> five repositories: `apache/cloudstack` (the management server, agent, and +> systemvm), plus four satellite clients — `apache/cloudstack-cloudmonkey` +> (CLI), `apache/cloudstack-go` (Go SDK), `apache/cloudstack-terraform-provider`, +> `apache/cloudstack-kubernetes-provider`. This document models +> `apache/cloudstack` as the canonical threat model; the four satellite +> models are short *deltas* that inherit §3 / §4 / §7 from this +> document and add only what each satellite uniquely introduces (`§4 B1` +> reachability, the credential file shape, the wrapper-of-SDK contract, +> etc.). The deltas live at `/tmp/claude/cloudstack--threat-model-draft.md`. +> The satellite clients' interfaces point **inward** at the management-server +> API; some satellites additionally expose **outward** interfaces that are +> designed to be safe to expose *(maintainer: DaanHoogland)*. +> An umbrella model was rejected because the satellites are uniformly thin +> "HMAC-SHA1-signing HTTP client" wrappers — a single document either +> drowns them in CloudStack-server content or, worse, drowns the +> CloudStack-server content in satellite caveats. Each satellite is small +> enough that a 1–2 page delta works. + +## §1 Header + +- **Project:** Apache CloudStack (`apache/cloudstack`) — IaaS orchestration + platform. This document does **not** cover the four satellite repos, which + carry their own delta models. +- **Commit:** `7308dad1` (HEAD of `main` at draft time). +- **Date:** 2026-05-29. +- **Authors:** ASF Security team draft, awaiting CloudStack PMC review. +- **Status:** Draft — under maintainer review. +- **Version binding:** This document describes the model as of the commit + above. A vulnerability report against CloudStack release *N* (currently + the 4.20.x line) should be triaged against the model as it stood at *N*'s + release tag, not against HEAD. +- **Reporting:** vulnerabilities that fall under §8 (claimed properties) + should be reported per the project's published policy + (`security@apache.org` per `README.md` and + `https://cloudstack.apache.org/security.html`); reports that fall under + §3 (out of scope), §9 (properties not provided), or §11a (known + non-findings) will be closed by CloudStack triagers citing this document. +- **Provenance legend** — + *(documented)* = paraphrased from an in-repo source or the project website + with citation; *(maintainer)* = stated by a CloudStack PMC member in + response to this draft; *(inferred)* = synthesized by the producer from + code structure or domain knowledge, awaiting PMC ratification (every + *(inferred)* tag has a matching §14 question). +- **Draft confidence (provenance-tag tally):** 51 *(documented)* / 42 + *(maintainer)* / 38 *(inferred)*. Eleven formerly-open questions (Q1, + Q2, Q4, Q5, Q12 — including the highest-leverage Root-CA strictness + default — plus Q8, Q9, Q10, Q17, Q18, Q19 from the 2026-06-08 review) + were resolved by the CloudStack PMC review (DaanHoogland, vishesh92) and + their tags promoted from *(inferred)* to *(maintainer)*. + +**About the project.** Apache CloudStack is an open-source Infrastructure-as-a- +Service (IaaS) orchestration platform *(documented: `README.md`, +`https://cloudstack.apache.org/`)*. It deploys and manages large fleets of +virtual machines across multiple hypervisors (KVM, VMware, XenServer/XCP-ng, +Hyper-V, baremetal-bridge, OVM) and over object/block/file storage +(NFS, Ceph/RBD, iSCSI, SMB, primary-storage plugins, S3-compatible secondary +storage). A central **management server** (Java/Tomcat-style servlets, +backed by MariaDB/MySQL) exposes a signed REST/JSON API to admins, end +users, and integrations; runs system VMs (Secondary Storage VM, Console +Proxy VM, virtual router); and orchestrates a fleet of **agents** running +on each hypervisor host. Authorization is RBAC + multi-tenant +domain/account/project hierarchy. The deployment shape is "operator-run +private/public cloud control plane", not a hosted-as-a-service appliance. + +## §2 Scope and intended use + +### Intended use + +- A multi-tenant IaaS control plane deployed by an operator inside a + controlled datacenter or cloud, exposing compute / storage / network + orchestration to authenticated end users via APIs which return responses in JSON or XML and a Vue.js Web + UI, with separately authenticated administrators *(documented: `README.md`, + `INSTALL.md`)*. +- Used both for service-provider public clouds and for on-premises private + clouds; the trust model is the same in both *(documented: `README.md`)*. + +### Deployment shape + +CloudStack is **not** an in-process library, **not** a single-binary +appliance, and **not** a hosted SaaS. It is a distributed control plane: +one or more management-server instances — **a single management-server +instance for smaller clouds, or a cluster behind a load balancer for +larger deployments** *(maintainer: DaanHoogland)* — a MariaDB/MySQL +database, one usage server, an optional +SecondaryStorageVM/ConsoleProxyVM/VirtualRouter set of system VMs, and a +per-hypervisor-host `cloudstack-agent` (for KVM/baremetal) or +out-of-process resource bridges (for VMware / XenServer / XCP-ng / Hyper-V). +The operator owns the surrounding L2/L3 network (the **management network**, +the **public network**, the **guest network**, the **storage network**) +and the physical hosts. The threat model is therefore that of a +distributed service (single-instance or clustered), not a library +*(maintainer: DaanHoogland — confirms the distributed control-plane shape; +single-instance is also a supported topology)*. + +### Caller roles + +| Role | Trust level | Notes | +| --- | --- | --- | +| **End-user API client / Web UI user** | untrusted but authenticated | Identity verified via Apache CloudStack-native (password + HMAC-SHA1 signed request), LDAP, SAML2, OAuth2, or pluggable `APIAuthenticator` *(documented: `plugins/user-authenticators/{ldap,saml2,oauth2,...}`, `server/src/main/java/com/cloud/api/ApiServer.java` `verifyRequest`)*. | +| **Domain / Project admin** | partial trust within their domain | Bounded by RBAC (`plugins/acl/{static,dynamic,project}-role-based`) and the domain hierarchy; can manage users / VMs / networks within a domain. | +| **Root admin** | trusted control plane | Global RBAC role; can change global configuration, upload templates/ISOs, run privileged orchestration. | +| **Operator / cluster admin** | trusted | OS-level access to management-server hosts, the MariaDB database, the keystore, and the agent hosts. Sets `agent.properties`, manages `cloudstack-agent` packages, manages the JCEKS keystore used by the agent for TLS *(documented: `agent/conf/agent.properties`, `framework/security/.../KeystoreManager.java`)*. | +| **Hypervisor agent (cloudstack-agent on KVM/baremetal)** | trusted-once-enrolled peer | Mutually authenticated via X.509 client cert signed by the management server's Root CA *(documented: `framework/ca/`, `plugins/ca/root-ca/`, `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`)*. | +| **System VM (SSVM / CPVM / VR)** | trusted-once-enrolled peer | Same X.509 enrolment shape as the agent; carries the agent binary inside *(maintainer: confirmed — same trust tier as agents, not a separate tier)*. | +| **Hypervisor host (the underlying KVM/VMware/etc.)** | trusted by virtue of operator-controlled provisioning | CloudStack expects to drive the hypervisor via libvirt / VMware vSphere SDK / XenAPI as a privileged user *(documented: `plugins/hypervisors/kvm/`, `plugins/hypervisors/vmware/`, `plugins/hypervisors/xenserver/`)*. | +| **Hypervisor-managed guest VM (end-user workload)** | **untrusted** | A guest VM is an attacker's workload; the model defends against it. | +| **Reverse proxy / load balancer in front of management server** | trusted *(if `proxy.header.verify=true`)* | When the operator enables forward-header processing, only requests whose `Remote_Addr` ∈ `proxy.cidr` have their `proxy.header.names` header honoured *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress`; setting names maintainer: vishesh92)*. | +| **Underlying storage (primary / secondary)** | trusted by virtue of operator-granted credentials | CloudStack reads/writes via NFS / RBD / iSCSI / S3 with operator-supplied credentials *(documented: primary/secondary storage plugins under `plugins/storage/`)*. | +| **External integrations (Tungsten, NSX, Netscaler, Palo Alto, …)** | trusted control-plane peers | Operator-configured; CloudStack assumes truthful responses *(inferred — §14 Q3)*. | + +### Component-family table + +| Management server JSON and XML APIs | `server/src/main/java/com/cloud/api/ApiServlet.java`, HTTP on `:8080` (API + UI), optional HTTPS on `:8443` when `https.enable=true`; user API path `:8080/client/api` *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java`, `client/conf/server.properties.in`)* | network (TCP, optionally TLS) | **yes** | +| Management server cluster RPC (peer-to-peer) | NIO + TLS between management-server replicas, `:9090` *(documented: `framework/cluster/`, `utils/.../nio/`)* | network | **yes** (peer auth via Root CA) | +| Management server → agent RPC | NIO + TLS on `:8250` (default `agent.properties`) *(documented: `agent/conf/agent.properties` line 47, `utils/.../nio/NioServer.java`)* | network | **yes** (mutually authenticated via Root CA) | +| `cloudstack-agent` (KVM/baremetal) | reverse-connects to management server, runs commands via libvirt / hypervisor SDK *(documented: `agent/`, `plugins/hypervisors/kvm/`)* | network + hypervisor + OS | **yes** | +| System VMs — SecondaryStorageVM, ConsoleProxyVM, Virtual Router | shipped images under `systemvm/`; agent binaries inside them *(documented: `systemvm/`)* | network (storage / public / guest) | **yes** | +| Console proxy data path | browser ↔ ConsoleProxyVM ↔ hypervisor VNC/SPICE socket; signed token issued by management server *(documented: `server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java`, `server/src/main/java/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java`)* | network | **yes** | +| Secondary-storage HTTP (templates, ISO downloads, snapshot copies) | download links are UUID-named symlinks served by an Apache httpd, with **no auth on the link**; the UUID format prevents enumeration and the symlinks are removed after a period *(maintainer: vishesh92, DaanHoogland)* | network | **yes** | +| Hypervisor plugins (`plugins/hypervisors/{kvm,vmware,xenserver,hyperv,ovm,ovm3,baremetal,ucs,simulator}`) | invoked by agent or by management server *(documented: `plugins/hypervisors/`)* | hypervisor APIs | **yes** for the call shape; **out-of-model** for the upstream hypervisor's own bugs | +| Network plugins (`plugins/network-elements/{netscaler,nsx,palo-alto,tungsten,nicira-nvp,...}`) | management server outbound | external SDN/firewall APIs | **yes** for credential handling and request construction; **out-of-model** for the external endpoint | +| Storage plugins (`plugins/storage/{volume,image,object}`) | management server / agent | NFS, RBD, iSCSI, S3 endpoints | **yes** for credential handling; **out-of-model** for the storage endpoint | +| User authenticator plugins (`plugins/user-authenticators/{md5,sha256salted,pbkdf2,plain-text,ldap,saml2,oauth2}`) | management server | LDAP / SAML2 IdP / OAuth2 IdP | **yes** for the local code; **out-of-model** for the IdP | +| RootCA provider (`plugins/ca/root-ca/`) | self-signed CA generated by management server at first boot, issues certs to agents *(documented: `plugins/ca/root-ca/.../RootCAProvider.java`)* | none directly | **yes** | +| Two-factor authenticators (`plugins/user-two-factor-authenticators/{static-pin,totp}`) | management server | none | **yes** | +| Backup providers (`plugins/backup/`) | management server outbound | external backup endpoints | **yes** for credential handling | +| Quota / metrics / DRS / HA planners | internal | none | **yes** as orchestration only; not a security boundary | +| Database layer (MariaDB/MySQL, Jasypt-encrypted secrets) | management server | network to DB | **yes** for credential handling; DB itself is trusted *(documented: `README.md` "Notice of Cryptographic Software" — JaSypt, native DB encryption)* | +| `cloud-cli`, `tools/marvin`, `test/`, `developer/`, `quickcloud/` | integration / test tooling | varies | **out of model** *(§3)* | +| `systemvm/agent/noVNC` (a vendored fork of `github.com/novnc/novnc` with CloudStack-specific changes on top *(maintainer: vishesh92)*), `…/vendor/pako`, other vendored JS / shell scripts | vendored upstream | n/a | in-model only at the wrapper boundary; upstream bugs go upstream. No automated vendored-dependency update procedure exists today (dependabot does not produce viable PRs); the PMC would prefer to have one *(maintainer: DaanHoogland)* | + +## §3 Out of scope (explicit non-goals) + +CloudStack is not, and does not aim to be, the following — reports +requiring any of these will be closed with the cited disposition: + +1. **A defender against the operator.** Anyone with `root` on a + management-server host, `root` on a hypervisor host, raw MariaDB + credentials, the JCEKS keystore + `security.encryption.key` / + `security.encryption.iv` *(documented: + `framework/security/.../KeysManager.java`)*, or the Root CA private key + already has unbounded power. "The operator misconfigured X" is not a + vulnerability *(inferred — §14 Q6)*. → `OUT-OF-MODEL: + adversary-not-in-scope`. +2. **A defender against a malicious external service the operator + configured.** A hostile LDAP server, SAML IdP, OAuth IdP, Tungsten / + NSX / Netscaler controller, S3 endpoint, Ceph cluster, or backup + provider is treated as a trusted control-plane peer. If the report + requires that peer to be hostile, it is out of model *(inferred — + §14 Q3)*. → `OUT-OF-MODEL: trusted-input`. +3. **A defender against the hypervisor.** CloudStack drives KVM / VMware / + XenServer / XCP-ng / Hyper-V via their own admin APIs. A hypervisor + bug that allows guest escape, a vSphere SDK vulnerability, a libvirt + privilege escalation — all are upstream to the hypervisor project, not + to CloudStack *(inferred — §14 Q7)*. → `OUT-OF-MODEL: + unsupported-component` (upstream pointer). +4. **An isolation boundary between an authorized administrator's API + call and the management server process.** Root admin can change global + configuration, upload templates and scripts to system VMs, register + arbitrary network/storage plugins, and run `runCustomAction`-style + commands. A new way for a root admin to do something they are already + authorized to do is not a vulnerability *(maintainer: vishesh92 — §14 Q8)*. → + `OUT-OF-MODEL: equivalent-harm`. +5. **A defender against a guest VM doing things the hypervisor allows it + to do.** A guest VM consuming CPU, memory, or disk up to its allocated + limit, sending arbitrary IP traffic within its assigned VLAN / VXLAN / + security group, or exploiting another VM via the hypervisor's own + shared surfaces (sidechannel, RowHammer, GPU passthrough leak) is out + of model. CloudStack is responsible only for the orchestration that + *places* the guest, not for hypervisor-level isolation *(maintainer: + vishesh92 — §14 Q9; the in-model case is CloudStack applying + wrong/insecure hypervisor settings — Daan to confirm boundary)*. → `OUT-OF-MODEL: adversary-not-in-scope` for the + side-channel case, `BY-DESIGN: property-disclaimed` for the + resource-limit case. +6. **A sandbox for templates, ISO images, or user-data scripts.** A + user-uploaded template (via `registerTemplate`) is run by the + hypervisor with the privileges the system grants. cloud-init / + user-data / metadata is passed through to the guest; CloudStack does + not parse or sanitize its semantics *(documented: kubernetes-service + plugin `userdata` references; maintainer: vishesh92 — §14 Q10, end-user guest customization)*. → + `BY-DESIGN: property-disclaimed`. +7. **Code that ships but is not part of the supported product:** + `tools/marvin/`, `test/`, `developer/`, `quickcloud/`, `cloud-cli/`, + `tools/devcloud4/`, `tools/devcloud-kvm/`, `tools/appliance/`, + `tools/checkstyle/`, `tools/transifex/`, `services/`-side simulators, + `simulator` hypervisor plugin, and IDE / build helpers under `tools/`. + *(inferred — §14 Q11)*. → `OUT-OF-MODEL: unsupported-component`. +8. **Bundled / vendored upstream libraries** — JaSypt, Bouncy Castle, + JSch, OpenSwan, noVNC + `pako`, MariaDB Connector/J, Spring, + Apache Commons, log4j, etc. *(documented: `README.md` Cryptographic + Software notice)*. `systemvm/agent/noVNC` is specifically a **vendored + fork of `github.com/novnc/novnc`** carrying CloudStack-specific changes + *(maintainer: vishesh92)*. Where CloudStack vendors source, the vendored + code is modeled at the wrapper boundary; vulnerabilities intrinsic to the + upstream project should be reported upstream. There is currently **no + automated procedure** to pull upstream fixes into the vendored copies + (dependabot has not produced viable PRs); the PMC would prefer to + establish one *(maintainer: DaanHoogland)*. + → `OUT-OF-MODEL: unsupported-component` (with an upstream pointer). +9. **The four satellite repos** (`apache/cloudstack-cloudmonkey`, + `apache/cloudstack-go`, `apache/cloudstack-terraform-provider`, + `apache/cloudstack-kubernetes-provider`) — covered by their own delta + threat models which inherit §3 / §4 / §7 from this document. +10. **The CloudStack documentation site, Confluence wiki, downloads + mirrors, Docker Hub images outside `apache/cloudstack-*`, gem / + npm / PyPI packages with similar names, and other non-product + surfaces.** Out of scope. + +## §4 Trust boundaries and data flow + +CloudStack has at least nine distinct trust transitions; a finding is +in-model only when it cleanly maps to one of them. + +| # | Transition | Authentication | Authorization | +| --- | --- | --- | --- | +| B1 | API client → management server JSON API (`:8080`/`:8443`) | per-user API key + HMAC-SHA1 signature over query string, or session login + 2FA *(documented: `server/src/main/java/com/cloud/api/ApiServer.java` `verifyRequest`)*; signature version 3 has expiration enforcement *(documented: same file line ~1053)* | RBAC (dynamic-role-based / static-role-based / project-role-based) on the called API command name + domain/account ownership of named resources | +| B2 | Web UI → management server (`:8080`) | same as B1 plus session cookie | same as B1 | +| B3 | Browser → ConsoleProxyVM → hypervisor VNC socket | signed token issued by management server, embedded in URL; encrypted with `ConsoleProxyPasswordBasedEncryptor` *(documented: `server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java`, `ConsoleProxyPasswordBasedEncryptor.java`)* | implicit (signed-token possession) | +| B4 | Management server ↔ management server (cluster peers) | NIO + TLS, Root CA-issued certs *(documented: `framework/cluster/`, `framework/ca/`)* | peer-trust by valid cert | +| B5 | Management server → `cloudstack-agent` (KVM/baremetal) | NIO + TLS on `:8250`; agent uses X.509 client cert issued by Root CA on first connect; cert provisioning is the `SetupKeyStoreCommand` shape *(documented: `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`, `framework/ca/.../CAService.java`, `plugins/ca/root-ca/.../RootCAProvider.java`)*; trust strictness governed by `ca.plugin.root.auth.strictness` (**default `true` for new setups; `false` only on upgrade from pre-Aug-2017 versions** — see §5a) and `ca.plugin.root.allow.expired.cert` | peer-trust by valid cert | +| B6 | Management server → external services (LDAP / SAML2 / OAuth2 IdP, NSX, Netscaler, Tungsten, S3, backup providers) | per-provider (service account, OAuth token, etc.) | external-service-side | +| B7 | Agent → hypervisor (libvirt / vSphere SDK / XenAPI) | local Unix socket (libvirt) or operator-supplied SDK credentials | hypervisor-side | +| B8 | Management server / agent → primary/secondary storage (NFS, RBD, iSCSI, S3) | OS-level (NFS), Ceph cephx, iSCSI CHAP, IAM key / static credential (S3) | storage-side | +| B9 | Operator → management server config (`db.properties`, `server.properties`, JCEKS keystore, global config table) | filesystem permissions on the host + DB access | OS-level + DB-level | + +### Reachability preconditions per family + +For each family in §2, a finding is in-model only if it is reachable as +follows: + +- **Management server JSON API**: reachable from an *unauthenticated* network + peer who can reach `:8080` / `:8443`. Findings that require an + authenticated peer collapse to "authenticated user with RBAC privilege + X", and must additionally either clear RBAC for the harmful command or + bypass it. +- **Web UI**: same shape as the JSON API; the Vue.js SPA is a presentation + layer over the API. +- **Cluster RPC (B4)**: reachable from a peer that has cleared the Root CA + trust check. A flat "cluster RPC has no auth" finding is `OUT-OF-MODEL: + adversary-not-in-scope` because the model *requires* the Root CA to be + enrolled across peers; a *cleartext*/un-certed cluster RPC finding is + gated by `ca.plugin.root.auth.strictness`, which defaults to `true` on + new setups (see §5a). +- **Management ↔ agent (B5)**: reachable from a peer that presents a + Root-CA-signed certificate the management server accepts. By default on + new setups `ca.plugin.root.auth.strictness = true`, so the management + server **does require** a client certificate from the connecting agent + *(maintainer: vishesh92 — + `https://github.com/apache/cloudstack/pull/2239`)*. The value remains + `false` only when upgrading from versions released before Aug 2017 that + predate the setting; that upgrade case is documented in the upgrade + instructions and is therefore not a concern *(maintainer: DaanHoogland)* + *(documented: `plugins/ca/root-ca/.../RootCAProvider.java`, + `RootCACustomTrustManager.java`)*. +- **Console proxy (B3)**: reachable by anyone who holds a valid signed + token. The token is the entire authorization gate. +- **Agent → hypervisor (B7)**: reachable only on the agent host, by code + the agent runs. +- **External integrations (B6)**: reachable from the management server's + outbound posture; a hostile external service is `OUT-OF-MODEL: + trusted-input` (§3 item 2). + +## §5 Assumptions about the environment + +- **Operating system (management server / usage server)**: RHEL 8/9/10, + CentOS 8/9, Rocky 9/10, Ubuntu 22.04/24.04, SUSE 15, openSUSE Leap 15; + Java 17 (`README.md`, `INSTALL.md`, `packaging/{el8,el9,el10,debian,suse15}`). +- **Operating system (agent)**: same family on KVM/baremetal hosts; + agent ships as `cloudstack-agent` package *(documented: `debian/`, + `packaging/`)*. +- **Database**: MariaDB or MySQL-compatible, accessible from each + management-server instance; CloudStack uses native DB encryption + + JaSypt for application-level secrets *(documented: `README.md` + "Notice of Cryptographic Software")*. +- **Cryptography**: JaSypt (application-secret encryption), Bouncy Castle + (general-purpose crypto, X.509 issuance in the Root CA provider), JSch + (SSH client to system VMs), OpenSwan (optional VPN endpoint termination) + *(documented: `README.md` Cryptographic Software notice)*. +- **Network**: operator-controlled L2/L3 with at least the management + network, public network, guest network, and storage network as logical + fabrics *(documented: CloudStack admin documentation; inferred — + §14 Q13)*. The management network is the trusted control-plane + network; the guest network carries untrusted guest VM traffic. +- **Time**: signature version 3 enforces an `expires` parameter on signed + API requests *(documented: `ApiServer.java` line ~1054)*; this assumes + loosely-synchronized clocks between client and management server + *(inferred — §14 Q14)*. +- **Filesystem**: the JCEKS keystore, `db.properties`, `server.properties`, + and Root CA private key are stored under `/etc/cloudstack/management/` + with OS-level permissions restricted to the `cloudstack` user + *(inferred — §14 Q15)*. +- **Hypervisor**: each supported hypervisor is assumed to provide its own + guest isolation (memory, vCPU, disk, network) and to expose a stable + admin API (libvirt for KVM, vSphere SDK for VMware, XenAPI for + XenServer/XCP-ng, WinRM/Hyper-V API for Hyper-V). +- **What CloudStack does to its host** (negative claims, awaiting + maintainer ratification): + - **does** open listening sockets on documented ports + (`:8080`/`:8443`/`:8250`/`:8096`/`:9090`/console-proxy ports) *(documented)*; + - **does** maintain MariaDB connections from the management server; + - **does** issue X.509 certificates from its self-signed Root CA *(documented: + `plugins/ca/root-ca/.../RootCAProvider.java`)*; + - **does** spawn child processes from the agent (`Script` invocations + against `/usr/share/cloudstack-common/scripts/`) *(documented: + `agent/src/main/java/com/cloud/agent/Agent.java` `keystoreSetupSetupPath`, + `keystoreCertImportScriptPath`)*; + - **does** write logs under operator-configured locations; + - **does** read a documented set of environment variables and the + `db.properties` file at startup *(inferred — §14 Q16)*; + - **does** install signal handlers / shutdown hooks only as + Tomcat/Jetty servlet container default *(inferred — §14 Q16)*. + +## §5a Build-time and configuration variants + +CloudStack ships as a family of packages: 'cloudstack-agent', 'cloudstack-baremetal-agent', 'cloudstack-common', 'cloudstack-integration-tests', 'cloudstack-management', 'cloudstack-marvin', 'cloudstack-mysql-ha', 'cloudstack-ui', 'cloudstack-usage' +*(documented: `debian/`, `packaging/`)*. A sizable number of runtime +configuration knobs materially change the security envelope. The +security-relevant subset: + +| Knob | Default | Maintainer stance | Effect | +| --- | --- | --- | --- | +| `ca.plugin.root.auth.strictness` | **`true` for new setups; `false` only on upgrade from pre-Aug-2017 versions** *(maintainer: vishesh92 — `https://github.com/apache/cloudstack/pull/2239`)* | New setups are strict by default; the `false`-on-upgrade case is called out in the upgrade instructions and is therefore not a concern *(maintainer: DaanHoogland)* | When `false`, the management server's `RootCACustomTrustManager` does **not** require a client certificate from a peer attempting to connect on `:8250` (agent port) or cluster ports. A peer without a cert is allowed in. | +| `ca.plugin.root.allow.expired.cert` | **`true`** *(documented: `RootCAProvider.java`)* | operational default to survive cert-rotation lag *(maintainer: paired with the strictness ruling above)* | When `true`, an expired client cert is accepted during SSL handshake. | +| `ca.plugin.root.issuer.dn` | `CN=ca.cloudstack.apache.org` *(documented: same file)* | configured at first management-server boot | Subject DN of the auto-generated self-signed Root CA. | +| `proxy.header.verify` | `false` by default *(maintainer: vishesh92 — §14 Q17)* | When on, the operator must restrict `proxy.cidr` to the trusted reverse-proxy CIDR | When set, `ApiServlet.getClientAddress` honours proxy-set forward headers *only* for source IPs in `proxy.cidr` *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress`; setting name maintainer: vishesh92)*. | +| `proxy.header.names` | list of header names; semantics: names to check for allowed IP addresses from a proxy-set header *(maintainer: vishesh92)* | list of header names to consult for the allowed client address when set by a proxy | Names the request header(s) carrying the proxy-set client IP. | +| `proxy.cidr` | unset *(maintainer: vishesh92 — §14 Q17; headers honoured only when `Remote_Addr` ∈ this list)* | required when `proxy.header.verify` is on | List of CIDRs for which `proxy.header.names` headers are honoured when the connecting `Remote_Addr` is in this list *(semantics maintainer: vishesh92)*. | +| `enable.user.2fa` / `mandate.user.2fa` | both default `false`; domain-configurable *(maintainer: vishesh92 — §14 Q18)* | `enable.user.2fa` turns 2FA on; `mandate.user.2fa` makes it mandatory (only when `enable.user.2fa` is true) — a deployment choice, not a §10 violation when off | When on, users must complete static-pin or TOTP 2FA after login. | +| `security.encryption.key`, `security.encryption.iv` | auto-generated at first boot *(documented: `framework/security/.../KeysManager.java`)* | trusted secret | Base64-encoded JaSypt master key + IV used to encrypt application-level secrets in the DB. | +| `user.password.encoders.order` | `PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT` *(maintainer: vishesh92)* | first encoder in the order is used to hash new passwords; the list also defines the verification fall-through order | Governs how user passwords are stored and which encoders are accepted on verify. | +| `user.password.encoders.exclude` | `MD5,LDAP,PLAINTEXT` *(maintainer: vishesh92)* | excluded encoders are not used to (re)hash passwords | Excludes weak/legacy encoders from being chosen, even though they remain in the order list for verifying already-stored hashes. | +| `enforce.post.requests.and.timestamps` | per `isPostRequestsAndTimestampsEnforced` *(documented: `ApiServer.java`; setting name maintainer: vishesh92)* | bounds `expires` to a maximum future offset | Prevents an attacker who steals a signed URL with a 10-year expiration from using it forever. | +| `integration.api.port` (`:8096`) | typically disabled *(inferred — §14 Q20)* | When non-zero, exposes an *unauthenticated* admin API for integration testing | An open integration port is a complete RBAC bypass on the management server. | +| Hypervisor enablement (which `plugins/hypervisors/*` are installed and configured) | per zone | operator-driven | An unused hypervisor plugin still ships but is not connected to any host. | +| Hostname / SAN of management-server cert (`ca.framework.cert.management.custom.san`) | unset *(maintainer: vishesh92)* | when set, included in the auto-generated cert SAN | governs which hostnames clients can use to reach the management server. | +| SAML2 / OAuth2 enablement (`plugins/user-authenticators/{saml2,oauth2}`) | off *(inferred — §14 Q19)* | turning on adds an external IdP trust dependency | adds B6 transitions. | +| LDAP enablement (`plugins/user-authenticators/ldap`) | off *(inferred — §14 Q19)* | turning on adds an external LDAP trust dependency | adds B6 transitions. | + +**The Root-CA strictness default (resolved).** Earlier drafts treated +`ca.plugin.root.auth.strictness = false` as the shipped default and the +single highest-leverage open question. The PMC has clarified that **new +setups default to `true`** — the management server *does* require a +Root-CA-signed client cert on `:8250` and the cluster ports — and the +value is `false` **only** when upgrading from versions released before +Aug 2017 that predate the setting *(maintainer: vishesh92 — +`https://github.com/apache/cloudstack/pull/2239`)*. That upgrade case is +documented in the upgrade instructions, so a leftover `false` after such +an upgrade is an operator-hardening/upgrade-hygiene item, not a shipped +insecure default *(maintainer: DaanHoogland)*. A report against an open +`:8250` accepting an un-certed peer on a **new** install is therefore +`MODEL-GAP`/`VALID` (strictness should be on), whereas the same on an +**upgraded** pre-2017 install is `OUT-OF-MODEL: non-default-build` +(documented upgrade step not applied). `ca.plugin.root.allow.expired.cert` +remains `true` as an operational concession to cert-rotation lag. + +## §6 Assumptions about inputs + +### Per-endpoint trust table (network surfaces) + +| Surface / route | Parameter | Attacker-controllable? | Caller must enforce | +| --- | --- | --- | --- | +| Management server `:8080`/`:8443` JSON API | command name + params | **yes** | nothing — CloudStack parses, authenticates (B1), applies RBAC, dispatches | +| Management server `:8080`/`:8443` JSON API | `signature` parameter | **yes** | HMAC-SHA1 verified *constant-time* against expected signature *(documented: `ApiServer.java` line 1137 `ConstantTimeComparator.compareStrings`)* | +| Management server `:8080`/`:8443` JSON API | `expires` parameter (sig v3) | **yes** | rejected if past, or beyond the `enforce.post.requests.and.timestamps` ceiling *(documented: same file; setting name maintainer: vishesh92)* | +| Management server `:8080`/`:8443` JSON API | proxy-set forward headers (`proxy.header.names`) | **yes** if `proxy.header.verify=true` | honoured **only** if the connecting `Remote_Addr` ∈ `proxy.cidr` *(documented: `ApiServlet.java` `getClientAddress`; setting names maintainer: vishesh92)* | +| Management server `:8080`/`:8443` Web UI | session cookie | **yes** | session-fixation / invalidation handled via `invalidateHttpSession` on auth failure *(documented: `ApiServlet.java` line 418)* | +| Integration API `:8096` (if enabled) | command name + params | **yes** | **no signature check** — integration port is unauthenticated by design | +| Management ↔ agent `:8250` | NIO Thrift-like payload | **only by a peer that has cleared B5 trust** | client cert via `RootCACustomTrustManager` | +| Management ↔ cluster peer | NIO payload | **only by a peer that has cleared B4 trust** | client cert via `RootCACustomTrustManager` | +| Console proxy URL | encrypted token (containing VM identity + endpoint + duration) | **yes** | token MUST decrypt + verify with `ConsoleProxyPasswordBasedEncryptor` keys *(documented: `ConsoleProxyPasswordBasedEncryptor.java`)* | +| Secondary-storage HTTP download URL | UUID-named symlink path | **yes** | **no auth on the download link**; the UUID format is the anti-enumeration control and the symlink is removed after a period — timed availability of the download token is the mitigation *(maintainer: vishesh92, DaanHoogland)* | +| Template / ISO upload | URL of remote source | **yes** within RBAC | upload-gated by `registerTemplate` RBAC; bytes are then served to hypervisors as image data | +| User-data / metadata service (`169.254.169.254` from inside guests) | guest-controlled bytes (the request) | **yes from the guest**, but the service is reached *from the guest* and serves only that guest's data | guest-VM-side isolation by virtual router | +| Hypervisor agent log / event stream | bytes from hypervisor | trusted operator surface | none — assumed truthful | +| LDAP / SAML / OAuth response (B6) | bytes from IdP | trusted | LDAP queries treat returned attributes as authoritative | +| Storage response (B8) | bytes / metadata from storage | trusted | bytes are object content; envelope is control-plane | + +### Size / shape / rate + +- CloudStack does not document a maximum signed-API request size; assumed + to be servlet-container default (Jetty / Tomcat) *(inferred — §14 Q21)*. +- API rate limiting is per-account via the global config knobs `api.throttling.*` + *(inferred — §14 Q22)*; an attacker with a valid API key can be rate- + limited at the application layer. +- Template / ISO upload size is bounded by storage capacity and per-account + resource limits *(inferred — §14 Q22)*; pathological compressed-image + inputs (e.g. extremely compressible QCOW2 with sparse holes that expand + to TB on extraction) are robustness concerns *(inferred — §14 Q23)*. +- Cluster-peer and agent RPC payload sizes: no documented application-layer + cap; NIO framing applies *(inferred — §14 Q21)*. + +## §7 Adversary model + +### Actors + +| Actor | In scope? | Capabilities granted | +| --- | --- | --- | +| Unauthenticated network peer reaching `:8080`/`:8443` | **yes** | TCP to the listening ports; may attempt authentication; may attempt to violate the protocol pre-auth | +| Unauthenticated peer reaching `:8250` (agent port) | **only if** `ca.plugin.root.auth.strictness = false`, which on new setups it is **not** (default `true`); `false` arises only on un-remediated pre-Aug-2017 upgrades (§5a) | TCP to the listening port; may attempt to connect as a peer without presenting a cert | +| Unauthenticated peer reaching `:8096` (integration port) | **yes** *if* the port is open (typically not in production) | full unauthenticated admin API | +| Authenticated end user with limited RBAC role | **yes** | call APIs their role permits; manage VMs/networks/storage in their domain/account/project | +| Authenticated end user with broad RBAC role | partial | only RBAC-envelope escapes are in scope | +| Authenticated domain admin | **yes** | full management within their domain; cross-domain leakage is in scope | +| Authenticated root admin | **out of scope** — see §3 item 4 | unbounded by design | +| Co-tenant (different account in same domain or different domain on same CloudStack) | **yes** | cross-tenant leakage (VM ID guessing, network bleed, storage bleed, template visibility) is in scope | +| Guest VM workload | **partial** | hypervisor-mediated; out-of-scope for hypervisor isolation bugs (§3 item 5), in-scope for the orchestration that placed the VM (security-group rule application, VLAN tagging, public IP routing) | +| Browser holding a valid console-proxy URL | **yes** | the URL is a bearer credential; scope of harm is one VM's console for the URL's lifetime | +| Operator | **out of scope** | see §3 item 1 | +| Hostile hypervisor | **out of scope** | see §3 item 3 | +| Hostile LDAP / SAML / OAuth IdP, hostile NSX/Netscaler/Tungsten, hostile S3 endpoint | **out of scope** | see §3 item 2 | +| Reverse proxy that should be trusted but is not in `proxy.cidr` | **out of scope** | its forward headers are not honoured | +| Local process on the management-server host running as a different UID | **partial** *(inferred — §14 Q24)* | same-host attackers with non-cloudstack UID can reach `:8080` unless host firewalling forbids; CloudStack does not defend against same-host `root` | +| Side-channel observer (cache timing, network timing, hypervisor side channels) | **out of scope** *(inferred — §14 Q25)* | n/a | +| Quantum adversary | **out of scope** | n/a | + +### Authenticated-but-Byzantine peer (distributed-systems threshold) + +CloudStack is **not** a Byzantine-fault-tolerant system. A compromised +management-server cluster peer with a valid Root-CA-issued cert can +schedule arbitrary work onto the agent fleet, read any guest's data, and +hand out console-proxy tokens. The cluster trusts its own membership +*(inferred — §14 Q26)*. Likewise, a compromised agent host can serve +malicious data on the management network and produce wrong status. → +reports requiring a Byzantine internal peer are `OUT-OF-MODEL: +adversary-not-in-scope`. + +## §8 Security properties the project provides + +For each property: condition, violation symptom, severity tier, provenance. + +### P1 — Authentication of API clients via signed request + +- **Condition**: a request carries `apiKey` + `signature` (and, for + signature version 3, an `expires` parameter not in the past) + *(documented: `ApiServer.java` `verifyRequest`)*; the signature is + HMAC-SHA1 of the canonical parameter string under the per-user + secret key, base64-encoded, lowercased, URL-decoded, and compared + to the computed value using `ConstantTimeComparator.compareStrings` + *(documented: same file line 1137)*. +- **Violation symptom**: a request executes API commands without a + valid `apiKey`+`signature` pair (and without a valid session + cookie / SAML / OAuth / LDAP login). +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P2 — Session authentication via password + optional 2FA + +- **Condition**: user logs in via the `login` API; 2FA is verified after + password if enabled for the user / domain *(documented: `ApiServlet.java` + lines 360–582)*. +- **Violation symptom**: a session is created without a valid password, or + 2FA enforcement is bypassed for a user where it is mandated. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P3 — Constant-time signature comparison + +- **Condition**: applies to the API signature check. +- **Violation symptom**: timing-side-channel measurement of signature + comparison reveals the expected signature byte-by-byte. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented: `ApiServer.java` line 1137)* + +### P4 — Authorization via RBAC + domain/account/project hierarchy + +- **Condition**: the authenticated principal calls an API command, and the + command name is permitted for their role *(documented: `plugins/acl/{static,dynamic,project}-role-based`)*; + resources named in the request belong to the principal's domain/account/project + or to a child within the principal's scope. +- **Violation symptom**: a non-root principal successfully executes an + API command not licensed for their role, or operates on a resource + outside their domain/account/project scope. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P5 — Mutual TLS on management ↔ agent, management ↔ cluster peer, *when configured* + +- **Condition**: `ca.plugin.root.auth.strictness = true` — **the default + on new setups** *(maintainer: vishesh92 — + `https://github.com/apache/cloudstack/pull/2239`)*. Pre-Aug-2017 + upgrades may leave it `false` until the documented upgrade step is + applied *(maintainer: DaanHoogland)*. `ca.plugin.root.allow.expired.cert` + remains `true` (cert-rotation concession), so the property covers + *peer-cert presence and Root-CA chain*, not cert freshness. +- **Violation symptom**: a peer without a Root-CA-issued cert successfully + completes a session on `:8250` or the cluster port on a setup where + strictness is on. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented; default resolved by maintainer.)* + +### P6 — Reverse-proxy IP-trust gating for forward headers + +- **Condition**: `proxy.header.verify` on (default `false`) *(maintainer: + vishesh92 — §14 Q17)*; + only requests whose `Remote_Addr` falls in `proxy.cidr` have their + `proxy.header.names` forward header(s) consulted *(documented: + `ApiServlet.java` `getClientAddress` `NetUtils.isIpInCidrList`; setting + names maintainer: vishesh92)*. +- **Violation symptom**: a request from a source IP **outside** + `proxy.cidr` succeeds with an attacker-supplied forward header + taking effect. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P7 — Console-proxy token confidentiality and integrity + +- **Condition**: tokens are encrypted under the + `ConsoleProxyPasswordBasedEncryptor` keys *(documented: + `ConsoleProxyPasswordBasedEncryptor.java`)*; a token includes the VM + identity, the hypervisor endpoint, and a duration / expiry. +- **Violation symptom**: a third party with no console-access RBAC + privilege forges or decrypts a token to gain console access; or a token + remains valid past its declared expiry. +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P8 — Application-secret encryption at rest in the DB via JaSypt + +- **Condition**: `security.encryption.key` + `security.encryption.iv` are + initialised at first boot and kept under filesystem ACLs + *(documented: `framework/security/.../KeysManager.java`, + `README.md` Cryptographic Software notice)*. +- **Violation symptom**: an attacker with read access to the DB but not + to the encryption key file recovers plaintext for secrets the model + claims are encrypted (typically: external service passwords, account + API secret keys when stored encrypted). +- **Severity**: **security-critical**, `VALID` per §13. +- *(documented)* + +### P9 — Memory safety on well-formed inputs across documented surfaces (JVM-bounded) + +- **Condition**: input matches the documented protocol on B1–B5; the JVM + is conformant; native code is invoked only via documented hypervisor + SDKs (libvirt / vSphere / XenAPI). CloudStack presumes **no limitation + on implementation language** — ocaml, python and bash run on hypervisors + and go is used on the management server (the set may grow); the + memory-safety claims here hold for the **JVM components**, to which the + JVM-conformance condition applies *(maintainer: DaanHoogland — §14 Q27)*. +- **Violation symptom**: heap corruption, OOM-via-input-size attack on a + surface where the input source is `:8080` / `:8443` / B5; JVM-side + crashes from a request a normally-RBAC'd user could send. +- **Severity**: **security-critical** when reachable from network input; + **`VALID-HARDENING`** when reachable only by a writer who already + controls the bytes (§3 item 5). +- *(maintainer: DaanHoogland — §14 Q27)* + +### P10 — Bounded RBAC scope of cross-domain visibility (`SHOW`-equivalent listing) + +- **Condition**: `list*` API commands filter responses to the principal's + domain/account/project scope per `plugins/acl/` policy. +- **Violation symptom**: a `list*` response leaks resource IDs / names / + metadata for resources outside the principal's RBAC scope. +- **Severity**: **security-critical** for resources whose existence is + itself confidential (typically: customer VM names, custom template + names); `VALID` per §13. +- *(inferred — §14 Q28)* + +## §9 Security properties the project does *not* provide + +State each plainly so a triager can route an inbound report to the matching +disclaimer. + +- **No defence against the operator.** Anyone with root on a + management-server host, the JCEKS keystore + `security.encryption.key`, + the Root CA private key, or the MariaDB credentials wins. See §3 item 1 + *(inferred — §14 Q6)*. +- **No defence against a malicious external service the operator + configured.** A hostile LDAP/SAML/OAuth IdP, NSX controller, Tungsten, + Netscaler, S3 endpoint, or backup provider is trusted. See §3 item 2. +- **No defence against the hypervisor.** Guest VM escape via libvirt, + vSphere, XenAPI, Hyper-V is upstream. See §3 item 3. +- **No isolation between a root admin's API call and the management-server + process.** Root admin can register arbitrary plugins, upload arbitrary + templates, run `runCustomAction`. See §3 item 4 *(inferred — §14 Q8)*. +- **No sandbox for guest VM workloads beyond what the hypervisor provides.** + Side-channel leaks between co-tenant VMs (cache, branch, memory bus, + shared GPU) are out of scope. See §3 item 5 *(inferred — §14 Q9)*. +- **No sandbox for user-data / templates / ISOs.** Templates run as their + own OS image with their own cloud-init; CloudStack does not parse or + reject user-data semantics. See §3 item 6 *(inferred — §14 Q10)*. +- **No defence against decompression / decoding bombs in uploaded + templates / ISOs.** A pathological QCOW2 / RAW image can consume + arbitrary CPU / disk on extraction; per-account resource limits are the + bound *(inferred — §14 Q23)*. +- **No defence against intra-cluster Byzantine failure.** A compromised + cluster peer with a valid Root-CA-issued cert can read any data the + cluster can read; see §7 *(inferred — §14 Q26)*. Likewise a compromised + agent host. +- **No data-at-rest encryption beyond JaSypt for selected DB columns + + whatever storage layers provide.** Guest volumes are encrypted only if + the primary-storage plugin supports it (Ceph RBD encryption, LUKS at + hypervisor layer) and the operator has configured it *(inferred — + §14 Q29)*. +- **No defence against side-channel observation** of API request timing, + agent RPC timing, or memory access patterns *(inferred — §14 Q25)*. +- **No application-layer constant-time comparison of anything other than + the API signature.** Login password comparison, session cookie + comparison, console-token comparison — not documented constant-time + *(inferred — §14 Q30)*. +- **No defender stance against an attacker on the same Linux host running + as a non-`cloudstack` UID** — CloudStack defends only across the + network surface; same-host attackers with shell access on the + management-server host already have many paths to win *(inferred — + §14 Q24)*. +- **No supported posture for the integration API port (`:8096`).** When + open, it is an unauthenticated admin surface; closing it is the + operator's job *(inferred — §14 Q20)*. + +### False-friend properties (call out separately) + +- **The Root CA is self-signed and auto-generated** — it is *not* a + publicly-trusted CA. Browsers and external clients require manual trust + bootstrap. The Root CA private key resides on the management server; a + compromised management server compromises the entire agent fleet's + trust. +- **`ca.plugin.root.auth.strictness = false` is not "TLS off" — it is + "client cert not required"** *(documented: `RootCAProvider.java`)*. TLS + on the wire is still there; what is missing is the peer-cert check. + Note the value is `true` on new setups *(maintainer: vishesh92)*; a + scanner that flags "client cert not requested" is only correct on an + un-remediated pre-Aug-2017 upgrade, and even then it identifies a + documented upgrade step, not a transport-encryption bug. +- **`ca.plugin.root.allow.expired.cert = true` is the operational default + to survive cert-rotation lag** but is not a security boundary. +- **The HMAC-SHA1 signature is request-integrity over the URL, not + request encryption.** Transport encryption is TLS; if the operator + serves the API over `http://`, the signature still validates but the + whole request (including the secret-derived signature) is visible to + the network. +- **The console-proxy URL is a bearer credential.** Anyone who sees the + URL (in logs, in a proxy, in a shoulder-surf) holds the console for + the URL's lifetime. +- **`list*` filtering is a per-call authorization view, not an + information-flow channel.** Existence of a resource that the principal + cannot see may leak through error messages, async-job status, event + logs, or by-ID lookup probing *(inferred — §14 Q28)*. +- **The integration API port is not a "trusted" port in the sense of + Kerberos `auth-int` — it is *no authentication at all***. The name + invites confusion. +- **JaSypt-encrypted DB columns are *(documented)* protected against a + DB-only read.** They are *not* protected against an attacker who + obtains both the DB and the encryption-key file. + +### Well-known attack classes the project does not defend against + +- **Cross-tenant VM-ID guessing / template-name enumeration**: §10 misuse, + not engine breakage. +- **Decompression / decoding bombs in uploaded templates and ISOs**. +- **Hypervisor side-channel attacks between co-tenant VMs**. +- **Confused-deputy between RBAC role and resource ownership** — e.g. a + domain admin's role permits a command, but the resource named is in a + child domain they should not touch *(inferred — §14 Q28)*. +- **Time-of-check-to-time-of-use** between RBAC check at API entry and + the actual orchestration on the agent fleet — policy revocations + mid-job are not retroactively enforced *(inferred — §14 Q31)*. + +## §10 Downstream responsibilities + +The operator deploying CloudStack in production **must**: + +1. Keep `ca.plugin.root.auth.strictness = true` (the default on new + setups). When **upgrading from a pre-Aug-2017 version**, follow the + documented upgrade step to turn strictness on — otherwise agent and + cluster-peer ports accept peers without a cert *(maintainer: vishesh92, + DaanHoogland — `https://github.com/apache/cloudstack/pull/2239`)*. + Consider tightening `ca.plugin.root.allow.expired.cert` (default `true`) + once cert rotation is reliable. +2. Restrict the management network at L2/L3 so that `:8250` (agent), + `:9090` (cluster), and the MariaDB port are reachable only from the + intended peers *(inferred — §14 Q13)*. +3. Restrict the integration API port `:8096` — either disable it entirely + or limit it to a localhost/management subnet *(inferred — §14 Q20)*. +4. Terminate TLS for the JSON API and Web UI on `:8443` (not `:8080`); if + `:8080` is exposed at all, only behind a TLS-terminating reverse + proxy *(inferred — §14 Q32)*. +5. When using a reverse proxy, set `proxy.header.verify = true`, + `proxy.header.names` to the forward header(s) the proxy sets, *and* + `proxy.cidr` to the proxy's CIDR — leaving `proxy.cidr` unset/empty + means the header is ignored (safe-default per P6), but a misconfigured + wide CIDR is a trust-bypass *(setting names maintainer: vishesh92)*. +6. Protect the `security.encryption.key` / `security.encryption.iv` + files, the JaSypt-encrypted DB, the Root CA private key, and the + `cloudstack-management` Unix user's home directory at OS level. +7. Keep the password-encoder configuration at safe defaults: + `user.password.encoders.order` defaults to + `PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT` (so PBKDF2 is used to hash + new passwords) and `user.password.encoders.exclude` defaults to + `MD5,LDAP,PLAINTEXT` (so the weak encoders are not chosen for hashing, + only retained for verifying already-stored hashes) *(maintainer: + vishesh92)*. Do not remove `MD5`/`PLAINTEXT` from the exclude list in + production — the supported greenfield encoder set is + `PBKDF2,SHA256SALT,SAML2` *(maintainer: vishesh92 — §14 Q19)*. +8. Enable 2FA (`totp` or `static-pin`) for administrators and ideally for + all users — 2FA on/off is a deployment choice via `enable.user.2fa` + and `mandate.user.2fa` (both default `false`) *(maintainer: vishesh92 — + §14 Q18)*. +9. Rotate per-user API secret keys on personnel change and on suspected + compromise. +10. Treat user-uploaded templates and ISOs as crossing a trust boundary — + scan / quarantine before allowing into the supported-template set. +11. Apply per-account resource limits (vCPU / RAM / volume size / image + size) to bound decompression-bomb and orchestration-DoS attacks. +12. Configure storage-layer encryption (Ceph RBD encryption, LUKS at KVM, + vSphere VM Encryption, etc.) if data-at-rest encryption is required. +13. Secure each `cloudstack-agent` host: `cloudstack` Unix user, agent + keystore under `/etc/cloudstack/agent/`, root account, libvirt / + vSphere admin credentials. +14. Restrict console-proxy URLs: do not log them, do not embed them in + public responses, set a short token lifetime. +15. Audit API call logs (via the event-bus plugin) for anomalous patterns. + +## §11 Known misuse patterns + +- **Leaving `:8250` open to the world with `ca.plugin.root.auth.strictness=false` + on an upgraded pre-Aug-2017 cluster.** New setups default to `true`; + the `false` value only survives an upgrade where the documented step + was skipped *(maintainer: vishesh92, DaanHoogland)*. In that state any + peer can connect as an agent — an upgrade-hygiene gap, dispositioned + `OUT-OF-MODEL: non-default-build` (documented upgrade step not applied). +- **Exposing `:8096` (integration API) publicly.** Anyone reaching the + port executes admin API commands without auth. +- **Exposing `:8080` (HTTP JSON API) publicly without a TLS-terminating + reverse proxy.** Signed-request integrity holds, but the API secret- + key-derived signature is visible to any wire observer; replay within + the `expires` window is trivial. +- **Setting `proxy.header.verify=true` with `proxy.cidr` wider than + the actual reverse-proxy CIDR.** An attacker outside the proxy can + spoof a `proxy.header.names` header and claim any IP address for audit + logs and authentication-IP checks *(setting names maintainer: vishesh92)*. +- **Removing `MD5`/`PLAINTEXT` from `user.password.encoders.exclude` (or + reordering them to the front of `user.password.encoders.order`) in + production.** The encoders ship for verifying legacy hashes; promoting + them to hash new passwords stores weakly-protected credentials + *(maintainer: vishesh92 — §14 Q19; the supported greenfield encoder set is `PBKDF2,SHA256SALT,SAML2`)*. +- **Granting domain admin to too many users.** A domain admin can manage + all accounts within the domain — including reading guest console URLs. +- **Embedding console-proxy URLs in screenshots, ticketing systems, or + chat.** Tokens are bearer credentials. +- **Reusing `security.encryption.key` across environments of different + trust levels.** A staging-env leak becomes a production-env decrypt + primitive *(inferred — §14 Q33)*. +- **Leaving `ca.plugin.root.auth.strictness=false` after a pre-Aug-2017 + upgrade in a multi-management-server deployment.** A peer can join the + cluster without a cert until the documented upgrade step flips it to the + new-setup default of `true` *(maintainer: vishesh92, DaanHoogland)*. +- **Uploading large or pathological templates and relying on hypervisor + to enforce size.** Per-account resource limits, not the engine, are the + enforcement. + +## §11a Known non-findings (recurring false positives) + +This section is the highest-leverage input for automated agentic security +scans. Each entry: tool symptom, why it is safe under the model, the § +that licenses the call. + +- **"Management ↔ agent port `:8250` accepts no client cert" reported + against a setup with `ca.plugin.root.auth.strictness=false`.** New setups + default to `true` and **do** require a Root-CA-signed client cert + *(maintainer: vishesh92 — `https://github.com/apache/cloudstack/pull/2239`)*. + The value is `false` only on an upgrade from a pre-Aug-2017 version that + predates the setting, and the upgrade instructions document turning it on + *(maintainer: DaanHoogland)*. → On a new install: `KNOWN-NON-FINDING` + (strictness is on). On an upgraded install with the step skipped: + `OUT-OF-MODEL: non-default-build` (documented upgrade step not applied). +- **"Integration port `:8096` is unauthenticated."** The port is + unauthenticated by design; operator responsibility per §10 to close / + bind to localhost. → `OUT-OF-MODEL: non-default-build` once the PMC + confirms. +- **"HMAC-SHA1 signature uses SHA1."** SHA1-HMAC is **not** broken for + HMAC use; collision attacks on SHA1 do not extend to HMAC-SHA1 + *(documented: cryptographic literature; CloudStack uses + `Mac.getInstance("HmacSHA1")` — `ApiServer.java` line 1130)*. → `KNOWN-NON-FINDING`. +- **"Constant-time string compare for the signature."** Already done — + `ConstantTimeComparator.compareStrings` per `ApiServer.java` line 1137. + → `KNOWN-NON-FINDING` (a finding flagging this is wrong). +- **"Root CA private key is on the management server."** By design — the + management server *is* the CA. → `BY-DESIGN: property-disclaimed`. +- **"Self-signed Root CA cert."** By design — the CA is generated at + first boot per `RootCAProvider.java`. Browsers will warn until the + operator bootstraps trust. → `BY-DESIGN: property-disclaimed`. +- **"Expired agent cert is accepted (`ca.plugin.root.allow.expired.cert=true`)."** + Documented default — an operational concession to cert-rotation lag, paired + with the strictness default *(maintainer: vishesh92, DaanHoogland)*. → + `VALID-HARDENING` at most; tightening it is an operator choice per §10. +- **"Hardcoded password / keytab in `tools/marvin/`, `test/`, `developer/`, + `quickcloud/`."** These directories are unsupported components per §3 + item 7. → `OUT-OF-MODEL: unsupported-component`. +- **"User-data / template contents execute arbitrary code in the guest + VM."** Templates are run as their own OS by the hypervisor; cloud-init + / user-data is intentionally a code-execution channel into the guest. + → `BY-DESIGN: property-disclaimed` per §9. +- **"Root admin can change global config / register plugins / upload + arbitrary templates."** Documented and intentional. → `BY-DESIGN: + property-disclaimed` per §9 / §3 item 4. +- **"DoS via expensive list call on a large CloudStack."** Pagination is + present; further bounds are admission-control / quota. → `BY-DESIGN: + property-disclaimed` per §9. +- **"Decompression bomb in an uploaded QCOW2 / template."** Per-account + resource limits are the bound. → `VALID-HARDENING` at most, unless the + decompression reaches §8 P9 memory-safety violations. +- **"Vendored Bouncy Castle / JaSypt / noVNC / `pako` has CVE-X."** Report + upstream; `systemvm/agent/noVNC` is a vendored fork of + `github.com/novnc/novnc` with CloudStack changes, and there is no + automated sync procedure today *(maintainer: vishesh92, DaanHoogland)*. → + `OUT-OF-MODEL: unsupported-component` (upstream pointer); a + CloudStack-introduced change *to* the fork is in-model. +- **"Secondary-storage download URL has no authentication / can be replayed."** + By design: download links are UUID-named symlinks served by an Apache + httpd with no auth on the link; the UUID format defeats enumeration and + the symlink is removed after a period, so timed availability is the + mitigation *(maintainer: vishesh92, DaanHoogland)*. → `BY-DESIGN: + property-disclaimed` for the no-auth aspect; a link that is *not* removed + after its window, or a guessable (non-UUID) name, is `VALID-HARDENING`. +- **"A proxy-set forward header is honoured without authentication."** + Honoured only if (a) `proxy.header.verify=true`, (b) the header is one of + `proxy.header.names`, *and* (c) the connecting `Remote_Addr` ∈ + `proxy.cidr` *(setting names maintainer: vishesh92)*. → `KNOWN-NON-FINDING`. +- **"Session-fixation: a session ID is reusable after failed login."** + `invalidateHttpSession` is called on each auth failure path per + `ApiServlet.java`. → `KNOWN-NON-FINDING` (verify the symptom; if + reproducible, escalate to `MODEL-GAP`). + +## §12 Conditions that would change this model + +Revise this document when any of the following lands: + +- A new authentication mechanism on a client-facing surface (e.g. + mTLS-as-API-auth on the JSON API, WebAuthn, OIDC). +- A new RBAC backend beyond the three included ACL plugins (e.g. OPA + integration, policy-engine integration). +- A new data-at-rest encryption story at the CloudStack layer (currently + delegated; see §9). +- A change in the default of any §5a flag, *especially* + `ca.plugin.root.auth.strictness` and `ca.plugin.root.allow.expired.cert`. +- Removal or change of the legacy `md5` / `plain-text` user-authenticator + plugins. +- A change in the signing algorithm or signature scheme on the JSON API + (e.g. SHA1 → SHA256 by default). +- A new hypervisor or system VM that adds a new trust boundary. +- A change in the extension mechanisms implemented by CloudStack + *(maintainer: DaanHoogland — §14 Q36)*. +- A new external-data surface (a new SDN controller integration, a new + storage provider, a new backup provider). +- A vulnerability report that cannot be cleanly routed to one of the §13 + dispositions: that is evidence the model is incomplete. + +## §13 Triage dispositions + +A report against `apache/cloudstack` receives exactly one of the +following: + +| Disposition | Meaning | Licensed by | +| --- | --- | --- | +| `VALID` | Violates a §8 property via an in-scope §7 adversary using an in-scope §6 input. | §8, §6, §7 | +| `VALID-HARDENING` | No §8 property violated, but a §11 misuse pattern can be made harder to fall into by code change. Fixed at maintainer discretion, typically no CVE. | §11 | +| `OUT-OF-MODEL: trusted-input` | Requires attacker control of a §6 parameter the model marks trusted (e.g. operator-supplied config flag, hostile LDAP/SAML/NSX/etc.). | §6 | +| `OUT-OF-MODEL: adversary-not-in-scope` | Requires a §7 actor the model excludes (operator, hostile hypervisor, hostile external IdP / SDN, Byzantine peer, side-channel observer, same-host non-`cloudstack` `root`). | §7 | +| `OUT-OF-MODEL: unsupported-component` | Lands in `tools/marvin/`, `test/`, `developer/`, `quickcloud/`, vendored upstream code, `simulator` hypervisor, etc. | §3 items 7–8 | +| `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a flag that is not the new-setup default (e.g. `ca.plugin.root.auth.strictness=false` surviving an un-remediated pre-Aug-2017 upgrade, integration port `:8096` open). | §5a | +| `OUT-OF-MODEL: equivalent-harm` | An actor already-authorized under the model can cause the same harm via a documented path (root admin doing root-admin things, RBAC-licensed user using their RBAC-licensed commands). | §3 items 4, 5 | +| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project explicitly does not provide (template sandboxing, side-channel resistance, hypervisor isolation, etc.). | §9 | +| `KNOWN-NON-FINDING` | Matches a §11a recurring false positive. | §11a | +| `MODEL-GAP` | Cannot be cleanly routed to any of the above — triggers §12 model revision. | §12 | + +## §14 Open questions for the maintainers + +Every *(inferred)* tag in the body maps to one of these. Proposed answers +are inline; please confirm, correct, or strike. + +### Wave 1 — scope, intended use, the two big insecure defaults + +**Q1.** ~~The model assumes CloudStack is "a clustered distributed +control plane deployed inside an operator-controlled datacenter +network", not a single-host appliance or a hosted SaaS. Confirm?~~ +**RESOLVED** *(maintainer: DaanHoogland)* — distributed control plane; +**both** a single management-server instance (smaller clouds) and a +clustered deployment are supported topologies. Folded into §2. + +**Q2.** ~~Are the SecondaryStorageVM, ConsoleProxyVM, and Virtual Router +treated as trusted-once-enrolled peers, or do they get their own trust +tier?~~ **RESOLVED** *(maintainer)* — **yes**, same trust tier as agents, +not a separate tier. Folded into §2 caller-roles. + +**Q3.** ~~Are external integrations (LDAP, SAML2 IdP, OAuth2 IdP, NSX +controller, Netscaler, Tungsten, S3-compatible storage, backup +providers) modeled as trusted control-plane peers?~~ **RESOLVED** +*(maintainer: DaanHoogland — yes)* — trusted control-plane peers; this +licenses §3 item 2 and §11a trusted-input dispositions. *(maps to §2, §3, §11a)* + +**Q4.** ~~SecondaryStorageVM HTTP download surface — is the URL token +per-template ACL-checked, or is the SSVM URL itself a bearer credential?~~ +**RESOLVED** *(maintainer: vishesh92, DaanHoogland)* — download links are +UUID-named symlinks served by an Apache httpd with **no auth on the link**; +the UUID format defeats enumeration and the symlink is removed after a +period (timed availability is the mitigation). The PMC noted this should +be re-tested/confirmed in code. Folded into §6, §11a. *(Daan also asked +why static code analysis did not surface this — a note for the scan +agent, not a model gap.)* + +**Q5.** ~~Vendored upstream code under `systemvm/agent/noVNC` and bundled +JaSypt / Bouncy Castle / JSch — is the policy "report upstream; we pick up +fixes on next sync"?~~ **RESOLVED** *(maintainer: vishesh92, DaanHoogland)* +— `systemvm/agent/noVNC` is a **vendored fork of `github.com/novnc/novnc`** +with CloudStack changes; vendored bugs go upstream. There is **no automated +update procedure today** (dependabot has not produced viable PRs); the PMC +would prefer to establish one. Folded into §3 item 8, §11a. + +**Q6.** ~~Is "an operator with `root` on a management-server host, the +JCEKS keystore + encryption keys, the Root CA private key, or MariaDB +credentials" out of scope?~~ **RESOLVED** *(maintainer: DaanHoogland — yes)* +— `OUT-OF-MODEL: adversary-not-in-scope`. *(maps to §3 item 1, §9)* + +**Q7.** ~~Hypervisor bugs (libvirt / vSphere SDK / XenAPI / Hyper-V API / +KVM/QEMU itself) — out of scope, report upstream?~~ **RESOLVED** +*(maintainer: DaanHoogland — yes, out of scope; report upstream)*. *(maps to §3 item 3)* + +### Wave 2 — the two big insecure defaults + +**Q12.** ~~**Highest-leverage question in the model.** Are +`ca.plugin.root.auth.strictness` and `ca.plugin.root.allow.expired.cert` +shipped insecure-by-default?~~ **RESOLVED** *(maintainer: vishesh92, +DaanHoogland — `https://github.com/apache/cloudstack/pull/2239`)*: + +- `ca.plugin.root.auth.strictness` defaults to **`true` on new setups** — + the management server **does** require a Root-CA-signed client cert on + `:8250` and the cluster ports. It is `false` **only** after upgrading + from a version released before Aug 2017 that predates the setting; the + upgrade instructions document turning it on, so a leftover `false` is an + upgrade-hygiene gap, not a shipped insecure default. +- `ca.plugin.root.allow.expired.cert` defaults to `true` as an operational + concession to cert-rotation lag. + +This resolution reshaped §3 item 1, §5a, §7 (the un-certed peer row), +§8 P5, §9 false-friends, §10, §11, §11a, and §13. The earlier +"assumes operator must flip per §10" framing is withdrawn. + +### Wave 3 — adjacent insecure defaults and admin-only surfaces + +**Q8.** ~~Is "a root admin with full RBAC role causes harm Y via a +documented path Z" out of scope (proposed: **yes**, `OUT-OF-MODEL: +equivalent-harm`)? In particular: `runCustomAction`, template upload, +plugin registration, global config change, system-VM patching, system-VM +console access.~~ **RESOLVED** *(maintainer: vishesh92)* — yes; a root +admin generally has direct access to most of these resources anyway → +`OUT-OF-MODEL: equivalent-harm`. *(maps to §3 item 4, §9)* + +**Q9.** ~~Guest VM workloads — confirm that hypervisor-mediated side +channels and resource-exhaustion-within-allocation are out of scope, and +that the in-scope orchestration concerns are limited to "did CloudStack +place the VM in the right VLAN / apply the right security group / route +the right IP" (proposed)?~~ **RESOLVED** *(maintainer: vishesh92)* — yes; +side channels + resource-exhaustion-within-allocation are out of scope. +The one in-model case: CloudStack applying a wrong/insecure setting while +launching or managing the guest (CloudStack must use correct/secure +hypervisor settings). *(DaanHoogland to confirm the boundary.)* *(maps to +§3 item 5, §7, §9)* + +**Q10.** ~~Templates / ISOs / user-data — confirm that there is no +sandboxing of user-supplied OS images, and that user-data is intentionally +a code-execution channel into the guest (proposed)?~~ **RESOLVED** +*(maintainer: vishesh92)* — yes; userdata is the end user customizing +their own guest OS (tenant-controlled data inside their own boundary), not +a CloudStack-side injection surface. *(maps to §3 item 6, §9)* + +**Q11.** Confirm the unsupported-component list: `tools/marvin/`, +`test/`, `developer/`, `quickcloud/`, `cloud-cli/`, +`tools/{devcloud4,devcloud-kvm,appliance,checkstyle,transifex,bugs-wiki,...}`, +`simulator` hypervisor plugin. Anything to add or remove? **RESOLVED** *(maintainer: DaanHoogland)* — exclude `simulator` and `tools/appliance` explicitly (out of scope for now; a future security-purpose tooling effort may revisit). *(maps to §3 item 7)* + +**Q17.** Forward-header gating — the **setting names are confirmed** +*(maintainer: vishesh92)*: `proxy.header.verify` (the on/off gate), +`proxy.header.names` (header names to consult), and `proxy.cidr` (CIDRs of +the `Remote_Addr` values for which those headers are honoured). **RESOLVED** *(maintainer: vishesh92)* — `proxy.header.verify` is +**`false` by default**; only when the connecting `Remote_Addr` ∈ +`proxy.cidr` does CloudStack read the client IP from `proxy.header.names`. +*(maps to §5a, §6, §10)* + +**Q18.** ~~2FA — proposed: off by default, operator turns it on per +domain / per user via `enable.2fa.*`. Confirm; and is "2FA disabled in +production" a §10 violation or a deployment choice?~~ **RESOLVED** +*(maintainer: vishesh92)* — deployment choice, not a §10 violation. Two +domain-configurable global settings: `enable.user.2fa` (default `false`; +whether 2FA is enabled) and `mandate.user.2fa` (default `false`; whether +2FA is mandatory — applies only when `enable.user.2fa` is true). *(maps to +§5a, §10)* + +**Q19.** User-authenticator plugins — encoder selection is governed by +`user.password.encoders.order` (default +`PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT`) and +`user.password.encoders.exclude` (default `MD5,LDAP,PLAINTEXT`), so PBKDF2 +is the effective hashing default and `MD5`/`PLAINTEXT` are retained only +for verifying legacy hashes *(maintainer: vishesh92)*. **RESOLVED** *(maintainer: vishesh92)* — a +report against `md5`/`plain-text` hashing *new* passwords in a greenfield +install is `OUT-OF-MODEL: non-default-build`: the default +`user.password.encoders.exclude` (`MD5,LDAP,PLAINTEXT`) removes them from +the effective set, so the supported greenfield encoders are +`PBKDF2,SHA256SALT,SAML2`. *(maps to §5a, §10, §11)* + +**Q20.** Integration API port `:8096` — proposed: closed (port-zero) by +default in production packaging, open only when explicitly configured; +when open, it is unauthenticated by design. A report of "integration +port allows admin commands without auth" is `OUT-OF-MODEL: +non-default-build` *if* the operator opened it, else `VALID`. Confirm the default. **RESOLVED** *(maintainer: DaanHoogland)* — default is `0` (disabled); `8096` is set only in test configurations. *(maps to §5a, §10, §11a)* + +### Wave 4 — environment, distributed model, false-friends + +**Q13.** Network-fabric assumptions — proposed: at least four logical +networks (management, public, guest, storage), with the management +network as the trusted control plane. Is that the canonical model, or +do you support more compressed topologies (single-fabric) in production? **RESOLVED** *(maintainer: DaanHoogland)* — there are four logical networks (management, public, guest, storage); each may have multiple instances across topologies (e.g. multiple zones) and may be combined within physical networks, but all four logical types must be present for a functional system. *(maps to §5, §10)* + +**Q14.** Clock-skew assumption for signature v3 `expires` enforcement — +proposed: operator's responsibility to keep client + management-server clocks roughly in sync. Confirm. **RESOLVED** *(maintainer: DaanHoogland)* — confirmed; operator responsibility (PMC to add to the security model page). *(maps to §5)* + +**Q15.** Confirm the filesystem-permissions inventory for sensitive +files: JCEKS keystore, Root CA private key, JaSypt key + IV, +`db.properties`. Who owns them, what mode? **RESOLVED** *(maintainer: vishesh92)* — not a CSV inventory of every file in a running system; only the four sensitive artifacts named here (JCEKS keystore, Root CA private key, JaSypt key + IV, `db.properties`). Ownership is `root` (user) / `cloud` (group), mode read+write for the owner and read for the `cloud` group (i.e. `0640`, `root:cloud`). *(maps to §5, §10)* + +**Q16.** Confirm the "what CloudStack does not do to its host" inventory +in §5: no child processes besides agent `Script` invocations / system +VM provisioning; signal-handlers via servlet container default; +environment-variable consumption confined to documented set. Anything to add? **RESOLVED** *(maintainer: DaanHoogland)* — confirmed; nothing to add. *(maps to §5)* + +**Q21.** API request size cap and cluster/agent RPC payload size cap — +are these explicitly bounded, or "whatever Jetty / NIO defaults give"? **RESOLVED** *(maintainer: DaanHoogland)* — the UI server sets an explicit cap, `org.apache.cloudstack.ServerDaemon.DEFAULT_REQUEST_CONTENT_SIZE = 1048576` (1 MiB); for other components the sizes are capped by the upstream components used. *(maps to §6, §9)* + +**Q22.** `api.throttling.*` and per-account resource limits — proposed: +these are the entire DoS-protection surface, with no engine-level guard. Confirm. **RESOLVED** *(maintainer: DaanHoogland)* — confirmed; enforced at the API access check, and `api.throttling.enabled` is **`false` by default**. *(maps to §6, §9, §10)* + +**Q23.** Decompression behaviour on uploaded QCOW2 / RAW / OVA — proposed: +no engine-side cap; per-account storage limits + hypervisor limits are the bound. Confirm. **RESOLVED** *(maintainer: DaanHoogland)* — correct. *(maps to §6, §9)* + +**Q24.** Same-host non-`cloudstack` UID — proposed: game-over, no defence +claimed. Confirm. **RESOLVED** *(maintainer: DaanHoogland, vishesh92)* — same-host non-`cloudstack` UID is game-over (no defence claimed; an operator at that level already controls the host, §3). On the related host-registration question: re-adding a host with the same IP **updates the existing host record** rather than creating a spoofed peer, and is gated by root-admin/operator access plus the keys/certs required to connect to the management server — adding a host directly without those fails (refinement tracked in apache/cloudstack#13182). So this is not an unauthenticated identity-spoof path. *(maps to §7, §9)* + +**Q25.** Side-channel observers (CPU cache timing, branch-predictor / speculative-execution channels e.g. Spectre-class, hypervisor-shared microarchitectural channels) — out of scope (proposed). **RESOLVED** *(maintainer: DaanHoogland)* — agreed, out of scope. *("branch" = branch-predictor / speculative-execution side channels — clarified by producer.)* *(maps to §7, §9)* + +**Q26.** Byzantine-internal-peer threshold — confirm CloudStack makes no +BFT claim, so any compromised cluster peer or agent with a valid +Root-CA-issued cert is unbounded (proposed). **RESOLVED** *(maintainer: DaanHoogland)* — agreed; no BFT claim. (A quorum-style mitigation would only be meaningful in larger clusters, not single/dual-node — possible future feature proposal.) *(maps to §7, §9)* + +**Q27.** §8 P9 memory-safety — JVM-bounded; is the reachability +boundary correctly "in-model for the JSON API + B5 input; out-of-model +for native hypervisor SDK bugs that surface as `Throwable`"? **RESOLVED** *(maintainer: DaanHoogland)* — the reachability boundary is right, but **§8 P9 must not imply CloudStack is Java-only** — no implementation-language limitation is presumed (ocaml, python, bash run on hypervisors; go is used on the management server; the set may grow). The memory-safety claims hold for the JVM components only. *(reflected in §8 P9.)* *(maps to §8 P9, §9)* + +**Q28.** §8 P10 listing-scope — confirm the §10 invariant "`list*` +responses are scoped to the principal's domain/account/project". And: +is information leak via error messages / async-job status / event log an in-model concern, or accepted? **RESOLVED** *(maintainer: DaanHoogland)* — in-model: regular system logs (e.g. log4j) are exempt, but other than those, information leaks (via error messages, async-job status, event log) are a concern. *(maps to §8 P10, §9, §11)* + +**Q29.** Data-at-rest encryption — confirm CloudStack delegates entirely +to storage layer / hypervisor (LUKS, Ceph encryption, vSphere VM +Encryption); no CloudStack-layer encryption of guest volumes. **RESOLVED** *(maintainer: DaanHoogland, vishesh92)* — correct; delegated entirely to the storage layer / hypervisor. *(maps to §9)* + +**Q30.** Constant-time comparison — confirm that *only* the API +signature path uses `ConstantTimeComparator`. Login password compare, +session cookie compare, console-token compare — none documented +constant-time. Is that intentional? **RESOLVED** *(maintainer: DaanHoogland)* — not intentional — the absence of constant-time comparison on the login-password / session-cookie / console-token paths is a lack of feature (hardening opportunity), not a by-design decision. *(maps to §8, §9)* + +**Q31.** Time-of-check-to-time-of-use between RBAC check at API entry +and orchestration on agent fleet — confirm mid-job RBAC revocation is +**not** retroactively enforced (proposed). **RESOLVED** *(maintainer: DaanHoogland)* — agreed/confirmed. *(maps to §9)* + +**Q32.** TLS posture on `:8080` vs `:8443` — confirm production deploys +behind TLS on `:8443` or behind a TLS-terminating reverse proxy; a bare +`:8080` HTTP API is dev-only. **RESOLVED** *(maintainer: DaanHoogland)* — confirmed. *(maps to §5a, §10)* + +**Q33.** `security.encryption.key` reuse across environments — confirm +that reusing the JaSypt key + IV across staging and production is a +documented misuse. **RESOLVED** *(maintainer: DaanHoogland)* — indeed — confirmed misuse. *(maps to §11)* + +### Wave 5 — meta + +**Q34.** Should this document live at `docs/threat-model.md` in +`apache/cloudstack`, or as a page on `cloudstack.apache.org/security/`? +Or both, with one canonical and the other linked? **RESOLVED** *(maintainer: DaanHoogland, vishesh92)* — both: this document is the source of truth, and `cloudstack.apache.org/security` carries an excerpt plus a link to it. *(meta)* + +**Q35.** Is there an existing CloudStack threat-model document +(Confluence, internal, or a `[SECURITY]`-tagged dev@ thread) that this +should reconcile against rather than supersede? **RESOLVED** *(maintainer: DaanHoogland)* — `cloudstack.apache.org/security/` is the only existing security model today; this document becomes its source of truth, with the page linking to it. *(meta — §3.1a of the rubric)* + +**Q36.** What kind of change should trigger a revision (proposed list in +§12 — confirm or correct)? **RESOLVED** *(maintainer: DaanHoogland)* — confirmed, plus add: a change in the extension mechanisms implemented by CloudStack (now reflected in §12). *(meta, §12)* + +**Q37.** §11a is the highest-leverage section for the scan agent's +suppression list. The current draft has 15 patterns; could the PMC +populate §11a from recurring "not a vuln" closures on the +`security@apache.org` ↔ CloudStack triage queue and on +`https://cloudstack.apache.org/security.html`? Concrete asks: 3–5 +patterns the PMC sees recur in inbound reports (e.g. "SSL bare on +`:8080` in a dev cluster", "agent port open without strictness flipped", +"`md5` authenticator left enabled after upgrade", "console URL appears +in support ticket"). *(meta — §11a)* + +**Q38.** Confirm the structural decision to keep the four satellite repos +as separate delta models (`cloudstack-go-threat-model-draft.md`, +`cloudstack-cloudmonkey-threat-model-draft.md`, +`cloudstack-terraform-provider-threat-model-draft.md`, +`cloudstack-kubernetes-provider-threat-model-draft.md`) inheriting §3 +/ §4 / §7 from this document. **RESOLVED** *(maintainer: DaanHoogland)* — confirmed; the satellites are not the system core (the core runs without them, they cannot run without the core), and there is an added hierarchy — `cloudstack-go` is a dependency of the other three. *(meta, §3 item 9)* + +--- + +## Appendix: SECURITY.md → §x back-map + +CloudStack does not currently ship an in-repo `SECURITY.md`; the `README.md` +section "Reporting Security Vulnerabilities" points to +`https://cloudstack.apache.org/security.html` as the canonical disclosure +landing page. The de facto security-policy artifacts are scattered: + +| Source | Claim | Lands in | +| --- | --- | --- | +| `README.md` "Reporting Security Vulnerabilities" | report to `security@apache.org`; canonical page at `cloudstack.apache.org/security.html` | §1 reporting cross-reference | +| `README.md` "Notice of Cryptographic Software" | JaSypt, Bouncy Castle, JSch, OpenSwan, MySQL native encryption | §5 cryptography assumption, §8 P8 | +| `agent/conf/agent.properties` (`host`, `port`, `ssl.handshake.timeout`, …) | agent ↔ management server transport on `:8250` | §2 component table, §4 B5 | +| `server/src/main/java/com/cloud/api/ApiServer.java` `verifyRequest` (lines ~980–1156) | HMAC-SHA1 signature + `expires` enforcement (`enforce.post.requests.and.timestamps`) + constant-time compare | §8 P1, §8 P3, §5a "enforce.post.requests.and.timestamps" row, §11a "SHA1 / constant-time" entries | +| `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress` (lines 700–725) | forward-header gating by `proxy.cidr` / `proxy.header.names` when `proxy.header.verify=true` | §8 P6, §5a "proxy.header.verify" row | +| `server/src/main/java/com/cloud/api/ApiServlet.java` 2FA path (lines 360–582) | password + 2FA flow | §8 P2 | +| `framework/ca/.../CAService.java`, `plugins/ca/root-ca/.../RootCAProvider.java` | Root CA generated at first boot; agent enrolment via `SetupKeyStoreCommand` | §4 B5, §8 P5, §5a strictness/allow-expired rows | +| `plugins/ca/root-ca/.../RootCACustomTrustManager.java` | `authStrictness` and `allowExpiredCertificate` semantics | §5a, §8 P5 | +| `plugins/acl/{static,dynamic,project}-role-based` | RBAC backends | §8 P4 | +| `plugins/user-authenticators/{md5,sha256salted,pbkdf2,plain-text,ldap,saml2,oauth2}` | pluggable user auth; selection via `user.password.encoders.order` / `user.password.encoders.exclude` | §2 caller-roles row, §5a "user.password.encoders.*" rows, §10 item 7 | +| `plugins/user-two-factor-authenticators/{static-pin,totp}` | 2FA backends | §5a "enable.user.2fa / mandate.user.2fa", §10 item 8 | +| `framework/security/.../KeysManager.java`, `KeystoreManager.java` | `security.encryption.key`, `security.encryption.iv` (Hidden), application-secret JaSypt encryption | §8 P8, §5a, §10 item 6 | +| `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore` (lines ~793–916) | agent receives Root CA-signed cert via `SetupKeyStoreCommand` and imports it | §4 B5, §8 P5 | +| `server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java`, `ConsoleProxyPasswordBasedEncryptor.java` | signed encrypted console-proxy URL token | §4 B3, §8 P7 | +| `https://cloudstack.apache.org/security.html` (website) | canonical disclosure landing page | §1 reporting cross-reference (note: not accessible from the producer's network at draft time; verify content with PMC) | diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index 0dc5b8211e0d..4e36eff4d75f 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -78,6 +78,14 @@ zone=default # Generated with "uuidgen". local.storage.uuid= +# Enable TLS for image server transfers. The keys are read from: +# cert file = /etc/cloudstack/agent/cloud.crt +# key file = /etc/cloudstack/agent/cloud.key +image.server.tls.enabled=true + +# The Address for the network interface that the image server listens on. If not specified, it will listen on the Management network. +#image.server.listen.address= + # Location for KVM virtual router scripts. # The path defined in this property is relative to the directory "/usr/share/cloudstack-common/". domr.scripts.dir=scripts/network/domr/kvm @@ -308,7 +316,7 @@ iscsi.session.cleanup.enabled=false #vm.migrate.domain.retrieve.timeout=10 # This parameter specifies if the host must be rebooted when something goes wrong with the heartbeat. -#reboot.host.and.alert.management.on.heartbeat.timeout=true +#reboot.host.and.alert.management.on.heartbeat.timeout=false # Enables manually setting CPU's topology on KVM's VM. #enable.manually.setting.cpu.topology.on.kvm.vm=true @@ -364,6 +372,14 @@ iscsi.session.cleanup.enabled=false # to the directory "/usr/share/cloudstack-common/". #network.scripts.dir=scripts/vm/network/vnet +# Sets the VXLAN networking mode used, either 'multicast' (default) or 'evpn'. +# The different modes lead to different scripts being executed by the Agent. +# multicast: modifyvxlan.sh +# evpn: modifyvxlan-evpn.sh +# Existing environments using VXLAN can safely switch to the 'evpn' mode as this +# will not break existing functionality. +#network.vxlan.mode=multicast + # Defines the location for storage scripts. # The path defined in this property is relative. # To locate the script, ACS first tries to concatenate @@ -457,3 +473,30 @@ iscsi.session.cleanup.enabled=false # Instance conversion VIRT_V2V_TMPDIR env var #convert.instance.env.virtv2v.tmpdir= + +# Time, in seconds, to wait before retrying to rebase during the incremental snapshot process. +# incremental.snapshot.retry.rebase.wait=60 + +# Path to the VDDK library directory for VMware to KVM conversion via VDDK, +# passed to virt-v2v as -io vddk-libdir= +#vddk.lib.dir= + +# Ordered VDDK transport preference for VMware to KVM conversion via VDDK, passed as +# -io vddk-transports= to virt-v2v. Example: nbd:nbdssl +#vddk.transports= + +# Optional vCenter SHA1 thumbprint for VMware to KVM conversion via VDDK, passed as +# -io vddk-thumbprint=. If unset, CloudStack computes it on the KVM host via openssl. +#vddk.thumbprint= + +# Timeout (in seconds) for QCOW2 delta merge operations, mainly used for classic volume snapshots, disk-only VM snapshots on file-based storage, and the KBOSS plugin. +# If a value of 0 or less is informed, the default will be used. +# qcow2.delta.merge.timeout=259200 + +# Maximum number of backup validation jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many validations as possible will be done at +# the same time. +# backup.validation.max.concurrent.operations.per.host= + +# Maximum number of backup compression jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many compressions as possible will be +# done at the same time. +# backup.compression.max.concurrent.operations.per.host= diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 3364f9708cf5..e4775188d0ca 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -123,6 +123,20 @@ public class AgentProperties{ */ public static final Property LOCAL_STORAGE_PATH = new Property<>("local.storage.path", "/var/lib/libvirt/images/"); + /** + * Enables TLS on the KVM image server transfer endpoint.
+ * Data type: Boolean.
+ * Default value: true + */ + public static final Property IMAGE_SERVER_TLS_ENABLED = new Property<>("image.server.tls.enabled", true); + + /** + * The IP address that the KVM image server listens on.
+ * Data type: String.
+ * Default value: null + */ + public static final Property IMAGE_SERVER_LISTEN_ADDRESS = new Property<>("image.server.listen.address", null, String.class); + /** * Directory where Qemu sockets are placed.
* These sockets are for the Qemu Guest Agent and SSVM provisioning.
@@ -156,7 +170,8 @@ public class AgentProperties{ public static final Property CMDS_TIMEOUT = new Property<>("cmds.timeout", 7200); /** - * The timeout (in seconds) for the snapshot merge operation, mainly used for classic volume snapshots and disk-only VM snapshots on file-based storage.
+ * The timeout (in seconds) for QCOW2 delta merge operations, mainly used for classic volume snapshots, disk-only VM snapshots on file-based storage, and the KBOSS plugin. + * If a value of 0 or less is informed, the default will be used.
* This configuration is only considered if libvirt.events.enabled is also true.
* Data type: Integer.
* Default value: 259200 @@ -304,6 +319,15 @@ public class AgentProperties{ */ public static final Property NETWORK_BRIDGE_TYPE = new Property<>("network.bridge.type", "native"); + /** + * Sets the VXLAN networking mode used by the BridgeVifDriver.
+ * Possible values: multicast | evpn
+ * When set to evpn, the driver will use modifyvxlan-evpn.sh instead of modifyvxlan.sh.
+ * Data type: String.
+ * Default value: multicast + */ + public static final Property NETWORK_VXLAN_MODE = new Property<>("network.vxlan.mode", "multicast"); + /** * Sets the driver used to plug and unplug NICs from the bridges.
* A sensible default value will be selected based on the network.bridge.type but can be overridden here.
@@ -593,10 +617,10 @@ public class AgentProperties{ /** * This parameter specifies if the host must be rebooted when something goes wrong with the heartbeat.
* Data type: Boolean.
- * Default value: true + * Default value: false */ public static final Property REBOOT_HOST_AND_ALERT_MANAGEMENT_ON_HEARTBEAT_TIMEOUT - = new Property<>("reboot.host.and.alert.management.on.heartbeat.timeout", true); + = new Property<>("reboot.host.and.alert.management.on.heartbeat.timeout", false); /** * Enables manually setting CPU's topology on KVM's VM.
@@ -808,6 +832,30 @@ public Property getWorkers() { */ public static final Property CONVERT_ENV_VIRTV2V_TMPDIR = new Property<>("convert.instance.env.virtv2v.tmpdir", null, String.class); + /** + * Path to the VDDK library directory on the KVM conversion host, used when converting VMs from VMware to KVM via VDDK. + * This directory is passed to virt-v2v as -io vddk-libdir=<path>. + * Data type: String.
+ * Default value: null + */ + public static final Property VDDK_LIB_DIR = new Property<>("vddk.lib.dir", null, String.class); + + /** + * Ordered list of VDDK transports for virt-v2v, passed as -io vddk-transports=<value>. + * Example: nbd:nbdssl. + * Data type: String.
+ * Default value: null + */ + public static final Property VDDK_TRANSPORTS = new Property<>("vddk.transports", null, String.class); + + /** + * vCenter TLS certificate thumbprint used by virt-v2v VDDK mode, passed as -io vddk-thumbprint=<value>. + * If unset, the KVM host computes it at runtime from the vCenter endpoint. + * Data type: String.
+ * Default value: null + */ + public static final Property VDDK_THUMBPRINT = new Property<>("vddk.thumbprint", null, String.class); + /** * BGP controll CIDR * Data type: String.
@@ -885,6 +933,38 @@ public Property getWorkers() { */ public static final Property CREATE_FULL_CLONE = new Property<>("create.full.clone", false); + /** + * Time, in seconds, to wait before retrying to rebase during the incremental snapshot process. + * */ + public static final Property INCREMENTAL_SNAPSHOT_RETRY_REBASE_WAIT = new Property<>("incremental.snapshot.retry.rebase.wait", 60); + + /** + * When set to true, executes modifymacip.sh (resolved via the + * network scripts directory) on VM NIC plug (VM start) and unplug (VM stop) to manage static + * ARP/NDP entries and host routes for VM interfaces.
+ * The script is invoked with:
+ *   add: -o add -b <bridge> -m <mac> [-4 <ipv4>] [-6 <ipv6>]
+ *   delete: -o delete -b <bridge> -m <mac>
+ * A bundled reference implementation is available at + * scripts/vm/network/vnet/modifymacip.sh.
+ * Set to false or leave unset to disable this feature.
+ * Data type: Boolean.
+ * Default value: false + */ + public static final Property VM_NETWORK_MACIP_STATIC = new Property<>("vm.network.macip.static", false, Boolean.class); + + + /** + * Maximum number of backup validation jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many validations as possible will be done at + * the same time. + */ + public static final Property BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST = new Property<>("backup.validation.max.concurrent.operations.per.host", null, Integer.class); + + /** + * Maximum number of backup compression jobs that can be executed at the same time. Values lower than 0 remove the limit, meaning that as many compressions as possible will be + * done at the same time. + */ + public static final Property BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST = new Property<>("backup.compression.max.concurrent.operations.per.host", null, Integer.class); public static class Property { private String name; diff --git a/api/pom.xml b/api/pom.xml index c80c35593451..4cdb57b6414c 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -71,6 +71,11 @@ cloud-framework-direct-download ${project.version} + + org.apache.cloudstack + cloud-framework-kms + ${project.version} + diff --git a/api/src/main/java/com/cloud/agent/api/to/BucketTO.java b/api/src/main/java/com/cloud/agent/api/to/BucketTO.java index f7e4bfea80fb..fd8237998a74 100644 --- a/api/src/main/java/com/cloud/agent/api/to/BucketTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/BucketTO.java @@ -26,10 +26,13 @@ public final class BucketTO { private String secretKey; + private long accountId; + public BucketTO(Bucket bucket) { this.name = bucket.getName(); this.accessKey = bucket.getAccessKey(); this.secretKey = bucket.getSecretKey(); + this.accountId = bucket.getAccountId(); } public BucketTO(String name) { @@ -47,4 +50,8 @@ public String getAccessKey() { public String getSecretKey() { return this.secretKey; } + + public long getAccountId() { + return this.accountId; + } } diff --git a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java index 26294cfbb223..76a75e03ba55 100644 --- a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java +++ b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java @@ -19,5 +19,5 @@ package com.cloud.agent.api.to; public enum DataObjectType { - VOLUME, SNAPSHOT, TEMPLATE, ARCHIVE + VOLUME, SNAPSHOT, TEMPLATE, ARCHIVE, BACKUP } diff --git a/api/src/main/java/com/cloud/agent/api/to/NicTO.java b/api/src/main/java/com/cloud/agent/api/to/NicTO.java index ca95fcfd6790..2ed7d9f9a201 100644 --- a/api/src/main/java/com/cloud/agent/api/to/NicTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/NicTO.java @@ -33,6 +33,7 @@ public class NicTO extends NetworkTO { boolean dpdkEnabled; Integer mtu; Long networkId; + boolean enabled; String networkSegmentName; @@ -154,4 +155,12 @@ public String getNetworkSegmentName() { public void setNetworkSegmentName(String networkSegmentName) { this.networkSegmentName = networkSegmentName; } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } } diff --git a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java index 18737c584b34..7daeb9649177 100644 --- a/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/RemoteInstanceTO.java @@ -36,13 +36,17 @@ public class RemoteInstanceTO implements Serializable { private String vcenterPassword; private String vcenterHost; private String datacenterName; + private String clusterName; + private String hostName; public RemoteInstanceTO() { } - public RemoteInstanceTO(String instanceName) { + public RemoteInstanceTO(String instanceName, String clusterName, String hostName) { this.hypervisorType = Hypervisor.HypervisorType.VMware; this.instanceName = instanceName; + this.clusterName = clusterName; + this.hostName = hostName; } public RemoteInstanceTO(String instanceName, String instancePath, String vcenterHost, String vcenterUsername, String vcenterPassword, String datacenterName) { @@ -55,6 +59,12 @@ public RemoteInstanceTO(String instanceName, String instancePath, String vcenter this.datacenterName = datacenterName; } + public RemoteInstanceTO(String instanceName, String instancePath, String vcenterHost, String vcenterUsername, String vcenterPassword, String datacenterName, String clusterName, String hostName) { + this(instanceName, instancePath, vcenterHost, vcenterUsername, vcenterPassword, datacenterName); + this.clusterName = clusterName; + this.hostName = hostName; + } + public Hypervisor.HypervisorType getHypervisorType() { return this.hypervisorType; } @@ -82,4 +92,12 @@ public String getVcenterHost() { public String getDatacenterName() { return datacenterName; } + + public String getClusterName() { + return clusterName; + } + + public String getHostName() { + return hostName; + } } diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index e26cc1e9f029..9af6c731fd24 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -51,6 +51,7 @@ public class VirtualMachineTO { private long minRam; private long maxRam; + private long requestedRam; private String hostName; private String arch; private String os; @@ -207,15 +208,20 @@ public long getMinRam() { return minRam; } - public void setRam(long minRam, long maxRam) { + public void setRam(long minRam, long maxRam, long requestedRam) { this.minRam = minRam; this.maxRam = maxRam; + this.requestedRam = requestedRam; } public long getMaxRam() { return maxRam; } + public long getRequestedRam() { + return requestedRam; + } + public String getHostName() { return hostName; } diff --git a/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java b/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java index 604720aaa290..5d028d31d5b6 100644 --- a/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java +++ b/api/src/main/java/com/cloud/agent/manager/allocator/HostAllocator.java @@ -22,19 +22,11 @@ import com.cloud.deploy.DeploymentPlanner.ExcludeList; import com.cloud.host.Host; import com.cloud.host.Host.Type; -import com.cloud.offering.ServiceOffering; import com.cloud.utils.component.Adapter; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; public interface HostAllocator extends Adapter { - /** - * @param UserVm vm - * @param ServiceOffering offering - **/ - boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering); - /** * Determines which physical hosts are suitable to * allocate the guest virtual machines on @@ -49,31 +41,6 @@ public interface HostAllocator extends Adapter { public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo); - /** - * Determines which physical hosts are suitable to allocate the guest - * virtual machines on - * - * Allocators must set any other hosts not considered for allocation in the - * ExcludeList avoid. Thus the avoid set and the list of hosts suitable, - * together must cover the entire host set in the cluster. - * - * @param VirtualMachineProfile - * vmProfile - * @param DeploymentPlan - * plan - * @param GuestType - * type - * @param ExcludeList - * avoid - * @param int returnUpTo (use -1 to return all possible hosts) - * @param boolean considerReservedCapacity (default should be true, set to - * false if host capacity calculation should not look at reserved - * capacity) - * @return List List of hosts that are suitable for VM allocation - **/ - - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity); - /** * Determines which physical hosts are suitable to allocate the guest * virtual machines on diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 438283136d2c..729f72b23ca2 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -24,15 +24,18 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; +import org.apache.cloudstack.api.command.admin.network.CloneNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd; -import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.DeleteGuestNetworkIpv6PrefixCmd; import org.apache.cloudstack.api.command.admin.network.DeleteManagementNetworkIpRangeCmd; import org.apache.cloudstack.api.command.admin.network.DeleteNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6PrefixesCmd; +import org.apache.cloudstack.api.command.admin.network.NetworkOfferingBaseCmd; import org.apache.cloudstack.api.command.admin.network.UpdateNetworkOfferingCmd; import org.apache.cloudstack.api.command.admin.network.UpdatePodManagementNetworkIpRangeCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; @@ -105,6 +108,33 @@ public interface ConfigurationService { */ ServiceOffering createServiceOffering(CreateServiceOfferingCmd cmd); + /** + * Clones a service offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created service offering cloned from source, null otherwise + */ + ServiceOffering cloneServiceOffering(CloneServiceOfferingCmd cmd); + + /** + * Clones a disk offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created disk offering cloned from source, null otherwise + */ + DiskOffering cloneDiskOffering(CloneDiskOfferingCmd cmd); + + /** + * Clones a network offering with optional parameter overrides + * + * @param cmd + * the command object that specifies the source offering ID and optional parameter overrides + * @return the newly created network offering cloned from source, null otherwise + */ + NetworkOffering cloneNetworkOffering(CloneNetworkOfferingCmd cmd); + /** * Updates a service offering * @@ -282,7 +312,7 @@ Vlan updateVlanAndPublicIpRange(UpdateVlanIpRangeCmd cmd) throws ConcurrentOpera boolean releasePublicIpRange(ReleasePublicIpRangeCmd cmd); - NetworkOffering createNetworkOffering(CreateNetworkOfferingCmd cmd); + NetworkOffering createNetworkOffering(NetworkOfferingBaseCmd cmd); NetworkOffering updateNetworkOffering(UpdateNetworkOfferingCmd cmd); diff --git a/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java b/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java index 8f7e773070f0..22d796d4a775 100644 --- a/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java +++ b/api/src/main/java/com/cloud/deploy/DeploymentPlanner.java @@ -70,7 +70,7 @@ public interface DeploymentPlanner extends Adapter { boolean canHandle(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid); public enum AllocationAlgorithm { - random, firstfit, userdispersing; + random, firstfit, userdispersing, firstfitleastconsumed; } public enum PlannerResourceUsage { diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 889e821a0905..f7d13343d469 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -30,12 +30,17 @@ import org.apache.cloudstack.backup.BackupRepositoryService; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionCustomAction; import org.apache.cloudstack.gpu.GpuCard; import org.apache.cloudstack.gpu.GpuDevice; import org.apache.cloudstack.gpu.VgpuProfile; import org.apache.cloudstack.ha.HAConfig; +import org.apache.cloudstack.kms.HSMProfile; +import org.apache.cloudstack.kms.KMSKey; import org.apache.cloudstack.network.BgpPeer; import org.apache.cloudstack.network.Ipv4GuestSubnetNetworkMap; import org.apache.cloudstack.quota.QuotaTariff; @@ -43,7 +48,7 @@ import org.apache.cloudstack.storage.object.ObjectStore; import org.apache.cloudstack.storage.sharedfs.SharedFS; import org.apache.cloudstack.usage.Usage; -import org.apache.cloudstack.vm.schedule.VMSchedule; +import org.apache.cloudstack.schedule.ResourceSchedule; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterGuestIpv6Prefix; @@ -125,17 +130,18 @@ public class EventTypes { public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE"; public static final String EVENT_VM_RECOVER = "VM.RECOVER"; - // VM Schedule - public static final String EVENT_VM_SCHEDULE_CREATE = "VM.SCHEDULE.CREATE"; - public static final String EVENT_VM_SCHEDULE_UPDATE = "VM.SCHEDULE.UPDATE"; - public static final String EVENT_VM_SCHEDULE_DELETE = "VM.SCHEDULE.DELETE"; - + // VM Schedule action-execution events (fired when a scheduled action runs). public static final String EVENT_VM_SCHEDULE_START = "VM.SCHEDULE.START"; public static final String EVENT_VM_SCHEDULE_STOP = "VM.SCHEDULE.STOP"; public static final String EVENT_VM_SCHEDULE_REBOOT = "VM.SCHEDULE.REBOOT"; public static final String EVENT_VM_SCHEDULE_FORCE_STOP = "VM.SCHEDULE.FORCE.STOP"; public static final String EVENT_VM_SCHEDULE_FORCE_REBOOT = "VM.SCHEDULE.FORCE.REBOOT"; + // Generic Resource Schedule CRUD events (apply to all resource types). + public static final String EVENT_SCHEDULE_CREATE = "SCHEDULE.CREATE"; + public static final String EVENT_SCHEDULE_UPDATE = "SCHEDULE.UPDATE"; + public static final String EVENT_SCHEDULE_DELETE = "SCHEDULE.DELETE"; + // Domain Router public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE"; public static final String EVENT_ROUTER_DESTROY = "ROUTER.DESTROY"; @@ -271,6 +277,20 @@ public class EventTypes { public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE"; public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION"; + // KMS (Key Management Service) events + public static final String EVENT_KMS_KEY_WRAP = "KMS.KEY.WRAP"; + public static final String EVENT_KMS_KEY_UNWRAP = "KMS.KEY.UNWRAP"; + public static final String EVENT_KMS_KEY_CREATE = "KMS.KEY.CREATE"; + public static final String EVENT_KMS_KEY_UPDATE = "KMS.KEY.UPDATE"; + public static final String EVENT_KMS_KEY_ROTATE = "KMS.KEY.ROTATE"; + public static final String EVENT_KMS_KEY_DELETE = "KMS.KEY.DELETE"; + public static final String EVENT_VOLUME_MIGRATE_TO_KMS = "VOLUME.MIGRATE.TO.KMS"; + + // HSM Profile events + public static final String EVENT_HSM_PROFILE_CREATE = "HSM.PROFILE.CREATE"; + public static final String EVENT_HSM_PROFILE_UPDATE = "HSM.PROFILE.UPDATE"; + public static final String EVENT_HSM_PROFILE_DELETE = "HSM.PROFILE.DELETE"; + // Account events public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE"; public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE"; @@ -298,8 +318,9 @@ public class EventTypes { public static final String EVENT_REGISTER_CNI_CONFIG = "REGISTER.CNI.CONFIG"; public static final String EVENT_DELETE_CNI_CONFIG = "DELETE.CNI.CONFIG"; - //register for user API and secret keys + //user API and secret keys public static final String EVENT_REGISTER_FOR_SECRET_API_KEY = "REGISTER.USER.KEY"; + public static final String EVENT_DELETE_SECRET_API_KEY = "DELETE.USER.KEY"; public static final String API_KEY_ACCESS_UPDATE = "API.KEY.ACCESS.UPDATE"; // Template Events @@ -374,11 +395,13 @@ public class EventTypes { // Service Offerings public static final String EVENT_SERVICE_OFFERING_CREATE = "SERVICE.OFFERING.CREATE"; + public static final String EVENT_SERVICE_OFFERING_CLONE = "SERVICE.OFFERING.CLONE"; public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT"; public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE"; // Disk Offerings public static final String EVENT_DISK_OFFERING_CREATE = "DISK.OFFERING.CREATE"; + public static final String EVENT_DISK_OFFERING_CLONE = "DISK.OFFERING.CLONE"; public static final String EVENT_DISK_OFFERING_EDIT = "DISK.OFFERING.EDIT"; public static final String EVENT_DISK_OFFERING_DELETE = "DISK.OFFERING.DELETE"; @@ -399,6 +422,7 @@ public class EventTypes { // Network offerings public static final String EVENT_NETWORK_OFFERING_CREATE = "NETWORK.OFFERING.CREATE"; + public static final String EVENT_NETWORK_OFFERING_CLONE = "NETWORK.OFFERING.CLONE"; public static final String EVENT_NETWORK_OFFERING_ASSIGN = "NETWORK.OFFERING.ASSIGN"; public static final String EVENT_NETWORK_OFFERING_EDIT = "NETWORK.OFFERING.EDIT"; public static final String EVENT_NETWORK_OFFERING_REMOVE = "NETWORK.OFFERING.REMOVE"; @@ -598,6 +622,7 @@ public class EventTypes { // VPC offerings public static final String EVENT_VPC_OFFERING_CREATE = "VPC.OFFERING.CREATE"; + public static final String EVENT_VPC_OFFERING_CLONE = "VPC.OFFERING.CLONE"; public static final String EVENT_VPC_OFFERING_UPDATE = "VPC.OFFERING.UPDATE"; public static final String EVENT_VPC_OFFERING_DELETE = "VPC.OFFERING.DELETE"; @@ -630,6 +655,7 @@ public class EventTypes { // Backup and Recovery events public static final String EVENT_VM_BACKUP_IMPORT_OFFERING = "BACKUP.IMPORT.OFFERING"; + public static final String EVENT_VM_BACKUP_OFFERING_CLONE = "BACKUP.OFFERING.CLONE"; public static final String EVENT_VM_BACKUP_OFFERING_ASSIGN = "BACKUP.OFFERING.ASSIGN"; public static final String EVENT_VM_BACKUP_OFFERING_REMOVE = "BACKUP.OFFERING.REMOVE"; public static final String EVENT_VM_BACKUP_CREATE = "BACKUP.CREATE"; @@ -642,6 +668,7 @@ public class EventTypes { public static final String EVENT_VM_BACKUP_USAGE_METRIC = "BACKUP.USAGE.METRIC"; public static final String EVENT_VM_BACKUP_EDIT = "BACKUP.OFFERING.EDIT"; public static final String EVENT_VM_CREATE_FROM_BACKUP = "VM.CREATE.FROM.BACKUP"; + public static final String EVENT_SCREENSHOT_DOWNLOAD = "BACKUP.VALIDATION.SCREENSHOT.DOWNLOAD"; // external network device events public static final String EVENT_EXTERNAL_NVP_CONTROLLER_ADD = "PHYSICAL.NVPCONTROLLER.ADD"; @@ -670,6 +697,7 @@ public class EventTypes { public static final String EVENT_AUTOSCALEVMGROUP_DISABLE = "AUTOSCALEVMGROUP.DISABLE"; public static final String EVENT_AUTOSCALEVMGROUP_SCALEDOWN = "AUTOSCALEVMGROUP.SCALEDOWN"; public static final String EVENT_AUTOSCALEVMGROUP_SCALEUP = "AUTOSCALEVMGROUP.SCALEUP"; + public static final String EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE = "AUTOSCALEVMGROUP.SCHEDULE.UPDATE"; public static final String EVENT_BAREMETAL_DHCP_SERVER_ADD = "PHYSICAL.DHCP.ADD"; public static final String EVENT_BAREMETAL_DHCP_SERVER_DELETE = "PHYSICAL.DHCP.DELETE"; @@ -848,6 +876,7 @@ public class EventTypes { public static final String EVENT_EXTENSION_DELETE = "EXTENSION.DELETE"; public static final String EVENT_EXTENSION_RESOURCE_REGISTER = "EXTENSION.RESOURCE.REGISTER"; public static final String EVENT_EXTENSION_RESOURCE_UNREGISTER = "EXTENSION.RESOURCE.UNREGISTER"; + public static final String EVENT_EXTENSION_RESOURCE_UPDATE = "EXTENSION.RESOURCE.UPDATE"; public static final String EVENT_EXTENSION_CUSTOM_ACTION_ADD = "EXTENSION.CUSTOM.ACTION.ADD"; public static final String EVENT_EXTENSION_CUSTOM_ACTION_UPDATE = "EXTENSION.CUSTOM.ACTION.UPDATE"; public static final String EVENT_EXTENSION_CUSTOM_ACTION_DELETE = "EXTENSION.CUSTOM.ACTION.DELETE"; @@ -859,6 +888,17 @@ public class EventTypes { public static final String EVENT_BACKUP_REPOSITORY_ADD = "BACKUP.REPOSITORY.ADD"; public static final String EVENT_BACKUP_REPOSITORY_UPDATE = "BACKUP.REPOSITORY.UPDATE"; + // DNS Framework Events + public static final String EVENT_DNS_SERVER_ADD = "DNS.SERVER.ADD"; + public static final String EVENT_DNS_SERVER_UPDATE = "DNS.SERVER.UPDATE"; + public static final String EVENT_DNS_SERVER_DELETE = "DNS.SERVER.DELETE"; + public static final String EVENT_DNS_ZONE_CREATE = "DNS.ZONE.CREATE"; + public static final String EVENT_DNS_ZONE_UPDATE = "DNS.ZONE.UPDATE"; + public static final String EVENT_DNS_ZONE_DELETE = "DNS.ZONE.DELETE"; + public static final String EVENT_DNS_RECORD_CREATE = "DNS.RECORD.CREATE"; + public static final String EVENT_DNS_RECORD_DELETE = "DNS.RECORD.DELETE"; + public static final String EVENT_DNS_NAME_COLLISION = "DNS.NAME.COLLISION"; + static { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -882,15 +922,18 @@ public class EventTypes { entityEventDetails.put(EVENT_VM_IMPORT, VirtualMachine.class); entityEventDetails.put(EVENT_VM_UNMANAGE, VirtualMachine.class); - // VMSchedule - entityEventDetails.put(EVENT_VM_SCHEDULE_CREATE, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_DELETE, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_UPDATE, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_START, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_STOP, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, VMSchedule.class); - entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, VMSchedule.class); + // VMSchedule action-execution events + entityEventDetails.put(EVENT_VM_SCHEDULE_START, ResourceSchedule.class); + entityEventDetails.put(EVENT_VM_SCHEDULE_STOP, ResourceSchedule.class); + entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, ResourceSchedule.class); + entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, ResourceSchedule.class); + entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, ResourceSchedule.class); + entityEventDetails.put(EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE, ResourceSchedule.class); + + // Generic Resource Schedule + entityEventDetails.put(EVENT_SCHEDULE_CREATE, ResourceSchedule.class); + entityEventDetails.put(EVENT_SCHEDULE_UPDATE, ResourceSchedule.class); + entityEventDetails.put(EVENT_SCHEDULE_DELETE, ResourceSchedule.class); entityEventDetails.put(EVENT_ROUTER_CREATE, VirtualRouter.class); entityEventDetails.put(EVENT_ROUTER_DESTROY, VirtualRouter.class); @@ -1009,6 +1052,20 @@ public class EventTypes { entityEventDetails.put(EVENT_VOLUME_RECOVER, Volume.class); entityEventDetails.put(EVENT_VOLUME_CHANGE_DISK_OFFERING, Volume.class); + // KMS Key Events + entityEventDetails.put(EVENT_KMS_KEY_CREATE, KMSKey.class); + entityEventDetails.put(EVENT_KMS_KEY_UPDATE, KMSKey.class); + entityEventDetails.put(EVENT_KMS_KEY_UNWRAP, KMSKey.class); + entityEventDetails.put(EVENT_KMS_KEY_WRAP, KMSKey.class); + entityEventDetails.put(EVENT_KMS_KEY_DELETE, KMSKey.class); + entityEventDetails.put(EVENT_KMS_KEY_ROTATE, KMSKey.class); + entityEventDetails.put(EVENT_VOLUME_MIGRATE_TO_KMS, KMSKey.class); + + // HSM Profile Events + entityEventDetails.put(EVENT_HSM_PROFILE_CREATE, HSMProfile.class); + entityEventDetails.put(EVENT_HSM_PROFILE_UPDATE, HSMProfile.class); + entityEventDetails.put(EVENT_HSM_PROFILE_DELETE, HSMProfile.class); + // Domains entityEventDetails.put(EVENT_DOMAIN_CREATE, Domain.class); entityEventDetails.put(EVENT_DOMAIN_DELETE, Domain.class); @@ -1045,11 +1102,13 @@ public class EventTypes { // Service Offerings entityEventDetails.put(EVENT_SERVICE_OFFERING_CREATE, ServiceOffering.class); + entityEventDetails.put(EVENT_SERVICE_OFFERING_CLONE, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_EDIT, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_DELETE, ServiceOffering.class); // Disk Offerings entityEventDetails.put(EVENT_DISK_OFFERING_CREATE, DiskOffering.class); + entityEventDetails.put(EVENT_DISK_OFFERING_CLONE, DiskOffering.class); entityEventDetails.put(EVENT_DISK_OFFERING_EDIT, DiskOffering.class); entityEventDetails.put(EVENT_DISK_OFFERING_DELETE, DiskOffering.class); @@ -1070,6 +1129,7 @@ public class EventTypes { // Network offerings entityEventDetails.put(EVENT_NETWORK_OFFERING_CREATE, NetworkOffering.class); + entityEventDetails.put(EVENT_NETWORK_OFFERING_CLONE, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_ASSIGN, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_EDIT, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_REMOVE, NetworkOffering.class); @@ -1397,6 +1457,17 @@ public class EventTypes { // Backup Repository entityEventDetails.put(EVENT_BACKUP_REPOSITORY_ADD, BackupRepositoryService.class); entityEventDetails.put(EVENT_BACKUP_REPOSITORY_UPDATE, BackupRepositoryService.class); + + // DNS Framework Events + entityEventDetails.put(EVENT_DNS_SERVER_ADD, DnsServer.class); + entityEventDetails.put(EVENT_DNS_SERVER_UPDATE, DnsServer.class); + entityEventDetails.put(EVENT_DNS_SERVER_DELETE, DnsServer.class); + entityEventDetails.put(EVENT_DNS_ZONE_CREATE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_ZONE_UPDATE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_ZONE_DELETE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_RECORD_CREATE, DnsRecord.class); + entityEventDetails.put(EVENT_DNS_RECORD_DELETE, DnsRecord.class); + } public static boolean isNetworkEvent(String eventType) { diff --git a/api/src/main/java/com/cloud/ha/Investigator.java b/api/src/main/java/com/cloud/ha/Investigator.java index 88d802a1ce44..00371d395f5a 100644 --- a/api/src/main/java/com/cloud/ha/Investigator.java +++ b/api/src/main/java/com/cloud/ha/Investigator.java @@ -26,17 +26,19 @@ public interface Investigator extends Adapter { * Returns if the vm is still alive. * * @param vm to work on. + * @return true if vm is alive, otherwise false */ - public boolean isVmAlive(VirtualMachine vm, Host host) throws UnknownVM; + boolean isVmAlive(VirtualMachine vm, Host host) throws UnknownVM; - public Status isAgentAlive(Host agent); + /** + * Returns the agent status of the host. + * + * @param host + * @return status of the host agent + */ + Status getHostAgentStatus(Host host); class UnknownVM extends Exception { - - /** - * - */ private static final long serialVersionUID = 1L; - }; } diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index 07a0dfce0419..c110e4ca94e1 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -57,8 +57,17 @@ public static String[] toStrings(Host.Type... types) { String HOST_UEFI_ENABLE = "host.uefi.enable"; String HOST_VOLUME_ENCRYPTION = "host.volume.encryption"; String HOST_INSTANCE_CONVERSION = "host.instance.conversion"; + String HOST_VDDK_SUPPORT = "host.vddk.support"; + String HOST_VDDK_LIB_DIR = "vddk.lib.dir"; + String HOST_VDDK_VERSION = "host.vddk.version"; String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; + String HOST_SSH_PORT = "host.ssh.port"; + String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count"; + String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; + String GUEST_OS_RULE = "guest.os.rule"; + + int DEFAULT_SSH_PORT = 22; /** * @return name of the machine. diff --git a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java index 0c821b4e36c0..67db19b7cc54 100644 --- a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java +++ b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java @@ -20,6 +20,7 @@ import java.util.Map; import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupProvider; import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.agent.api.Command; @@ -94,10 +95,10 @@ public interface HypervisorGuru extends Adapter { Map getClusterSettings(long vmId); VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, - String vmInternalName, Backup backup) throws Exception; + String vmInternalName, Backup backup, BackupProvider backupProvider) throws Exception; boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, - VirtualMachine vm, long poolId, Backup backup) throws Exception; + VirtualMachine vm, long poolId, Backup backup, BackupProvider backupProvider) throws Exception; /** * Will generate commands to migrate a vm to a pool. For now this will only work for stopped VMs on Vmware. * diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java index ce905b293ff3..80f6a6045c72 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesCluster.java @@ -98,7 +98,7 @@ enum State { s_fsm.addTransition(State.Running, Event.ScaleDownRequested, State.Scaling); s_fsm.addTransition(State.Stopped, Event.ScaleUpRequested, State.ScalingStoppedCluster); s_fsm.addTransition(State.Scaling, Event.OperationSucceeded, State.Running); - s_fsm.addTransition(State.Scaling, Event.OperationFailed, State.Alert); + s_fsm.addTransition(State.Scaling, Event.OperationFailed, State.Running); s_fsm.addTransition(State.ScalingStoppedCluster, Event.OperationSucceeded, State.Stopped); s_fsm.addTransition(State.ScalingStoppedCluster, Event.OperationFailed, State.Alert); diff --git a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java index 37b8907b454a..5a6eaa3f7b9a 100644 --- a/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java +++ b/api/src/main/java/com/cloud/kubernetes/cluster/KubernetesServiceHelper.java @@ -18,6 +18,7 @@ import org.apache.cloudstack.acl.ControlledEntity; +import java.util.List; import java.util.Map; import com.cloud.user.Account; @@ -33,8 +34,10 @@ enum KubernetesClusterNodeType { ControlledEntity findByUuid(String uuid); ControlledEntity findByVmId(long vmId); void checkVmCanBeDestroyed(UserVm userVm); + void checkVmAffinityGroupsCanBeUpdated(UserVm userVm); boolean isValidNodeType(String nodeType); Map getServiceOfferingNodeTypeMap(Map> serviceOfferingNodeTypeMap); Map getTemplateNodeTypeMap(Map> templateNodeTypeMap); + Map> getAffinityGroupNodeTypeMap(Map> affinityGroupNodeTypeMap); void cleanupForAccount(Account account); } diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java index 0846306f70f9..2f0bcdd5ef9a 100644 --- a/api/src/main/java/com/cloud/network/Network.java +++ b/api/src/main/java/com/cloud/network/Network.java @@ -116,6 +116,7 @@ class Service { public static final Service NetworkACL = new Service("NetworkACL", Capability.SupportedProtocols); public static final Service Connectivity = new Service("Connectivity", Capability.DistributedRouter, Capability.RegionLevelVpc, Capability.StretchedL2Subnet, Capability.NoVlan, Capability.PublicAccess); + public static final Service CustomAction = new Service("CustomAction"); private final String name; private final Capability[] caps; @@ -207,6 +208,7 @@ public static class Provider { public static final Provider Nsx = new Provider("Nsx", false); public static final Provider Netris = new Provider("Netris", false); + public static final Provider NetworkExtension = new Provider("NetworkExtension", false, true); private final String name; private final boolean isExternal; @@ -250,11 +252,47 @@ public static Provider getProvider(String providerName) { return null; } + /** Private constructor for transient (non-registered) providers. */ + private Provider(String name) { + this.name = name; + this.isExternal = false; + this.needCleanupOnShutdown = true; + // intentionally NOT added to supportedProviders + } + + /** + * Creates a transient (non-registered) {@link Provider} with the given name. + * + *

The new instance is not added to {@code supportedProviders}, so it + * will never be returned by {@link #getProvider(String)} and will not pollute the + * global provider registry. Use this for dynamic / extension-backed providers + * whose names are only known at runtime (e.g. NetworkOrchestrator extensions).

+ * + * @param name the provider name (typically the extension name) + * @return a transient {@link Provider} instance with the given name + */ + public static Provider createTransientProvider(String name) { + return new Provider(name); + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .append("name", name) .toString(); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Provider)) return false; + Provider provider = (Provider) obj; + return this.name.equals(provider.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } } public static class Capability { @@ -510,4 +548,6 @@ public void setIp6Address(String ip6Address) { Integer getPrivateMtu(); Integer getNetworkCidrSize(); + + boolean getKeepMacAddressOnPublicNic(); } diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java index c212e6319eb4..7e1a07ebeb69 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -187,6 +187,8 @@ public interface NetworkModel { boolean canElementEnableIndividualServices(Provider provider); + boolean canElementEnableIndividualServicesByName(String providerName); + boolean areServicesSupportedInNetwork(long networkId, Service... services); boolean isNetworkSystem(Network network); @@ -237,6 +239,18 @@ public interface NetworkModel { String getDefaultGuestTrafficLabel(long dcId, HypervisorType vmware); + /** + * Resolves a provider name to a {@link Provider} instance. + * For known static providers, delegates to {@link Provider#getProvider(String)}. + * For dynamically-registered NetworkOrchestrator extension providers whose names + * are not in the static registry, returns a transient {@link Provider} with the + * given name so callers can still dispatch correctly. + * + * @param providerName the provider name from {@code ntwk_service_map} or similar + * @return a {@link Provider} instance, or {@code null} if not resolvable + */ + Provider resolveProvider(String providerName); + /** * @param providerName * @return diff --git a/api/src/main/java/com/cloud/network/NetworkProfile.java b/api/src/main/java/com/cloud/network/NetworkProfile.java index 2e8efb489308..d690344a0e38 100644 --- a/api/src/main/java/com/cloud/network/NetworkProfile.java +++ b/api/src/main/java/com/cloud/network/NetworkProfile.java @@ -385,6 +385,11 @@ public Integer getNetworkCidrSize() { return networkCidrSize; } + @Override + public boolean getKeepMacAddressOnPublicNic() { + return true; + } + @Override public String toString() { return String.format("NetworkProfile %s", diff --git a/api/src/main/java/com/cloud/network/NetworkRuleApplier.java b/api/src/main/java/com/cloud/network/NetworkRuleApplier.java index b9942e71eb26..69b712bc6ca2 100644 --- a/api/src/main/java/com/cloud/network/NetworkRuleApplier.java +++ b/api/src/main/java/com/cloud/network/NetworkRuleApplier.java @@ -21,8 +21,13 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.rules.FirewallRule; +import com.cloud.network.vpc.Vpc; public interface NetworkRuleApplier { - public boolean applyRules(Network network, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException; + default boolean applyRules(Network network, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException { + return applyRules(network, null, purpose, rules); + } + + boolean applyRules(Network network, Vpc vpc, FirewallRule.Purpose purpose, List rules) throws ResourceUnavailableException; } diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index 742206c7e3bf..c32bb711c0f2 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -232,7 +232,7 @@ Network createPrivateNetwork(String networkName, String displayText, long physic /** * Requests an IP address for the guest NIC */ - NicSecondaryIp allocateSecondaryGuestIP(long nicId, IpAddresses requestedIpPair) throws InsufficientAddressCapacityException; + NicSecondaryIp allocateSecondaryGuestIP(long nicId, IpAddresses requestedIpPair, String description) throws InsufficientAddressCapacityException; boolean releaseSecondaryIpFromNic(long ipAddressId); @@ -279,4 +279,6 @@ Network createPrivateNetwork(String networkName, String displayText, long physic IpAddresses getIpAddressesFromIps(String ipAddress, String ip6Address, String macAddress); String getNicVlanValueForExternalVm(NicTO nic); + + Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId); } diff --git a/api/src/main/java/com/cloud/network/Networks.java b/api/src/main/java/com/cloud/network/Networks.java index 5f767686dc97..61a1c820723f 100644 --- a/api/src/main/java/com/cloud/network/Networks.java +++ b/api/src/main/java/com/cloud/network/Networks.java @@ -81,7 +81,11 @@ public String getValueFrom(URI uri) { return uri == null ? null : uri.getAuthority(); } }, - Vswitch("vs", String.class), LinkLocal(null, null), Vnet("vnet", Long.class), Storage("storage", Integer.class), Lswitch("lswitch", String.class) { + Vswitch("vs", String.class), + LinkLocal(null, null), + Vnet("vnet", Long.class), + Storage("storage", Integer.class), + Lswitch("lswitch", String.class) { @Override public URI toUri(T value) { try { @@ -99,7 +103,8 @@ public String getValueFrom(URI uri) { return uri == null ? null : uri.getSchemeSpecificPart(); } }, - Mido("mido", String.class), Pvlan("pvlan", String.class), + Mido("mido", String.class), + Pvlan("pvlan", String.class), Vxlan("vxlan", Long.class) { @Override public URI toUri(T value) { diff --git a/api/src/main/java/com/cloud/network/element/DnsServiceProvider.java b/api/src/main/java/com/cloud/network/element/DnsServiceProvider.java index 7abce537221c..2942e76965a7 100644 --- a/api/src/main/java/com/cloud/network/element/DnsServiceProvider.java +++ b/api/src/main/java/com/cloud/network/element/DnsServiceProvider.java @@ -33,4 +33,6 @@ boolean configDnsSupportForSubnet(Network network, NicProfile nic, VirtualMachin throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException; boolean removeDnsSupportForSubnet(Network network) throws ResourceUnavailableException; + + default boolean removeDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vmProfile) throws ResourceUnavailableException { return true; } } diff --git a/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java b/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java index c091142d9353..6b0f932e8c22 100644 --- a/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java +++ b/api/src/main/java/com/cloud/network/element/FirewallServiceProvider.java @@ -21,14 +21,20 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; import com.cloud.network.rules.FirewallRule; +import com.cloud.network.vpc.Vpc; public interface FirewallServiceProvider extends NetworkElement { /** - * Apply rules - * @param network - * @param rules - * @return - * @throws ResourceUnavailableException + * Apply firewall rules in a network context. */ - boolean applyFWRules(Network network, List rules) throws ResourceUnavailableException; + default boolean applyFWRules(Network network, List rules) throws ResourceUnavailableException { + return false; + } + + /** + * Apply firewall rules in a VPC context. + */ + default boolean applyFWRulesInVPC(Vpc vpc, List rules) throws ResourceUnavailableException { + return false; + } } diff --git a/api/src/main/java/com/cloud/network/element/NetworkElement.java b/api/src/main/java/com/cloud/network/element/NetworkElement.java index cb0fc2fca981..67be7b9ba2e2 100644 --- a/api/src/main/java/com/cloud/network/element/NetworkElement.java +++ b/api/src/main/java/com/cloud/network/element/NetworkElement.java @@ -146,4 +146,8 @@ boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, Reser * @return true/false */ boolean verifyServicesCombination(Set services); + + default boolean rollingRestartSupported() { + return true; + } } diff --git a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java index 0bf06be15d87..b7fe3b26761c 100644 --- a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java +++ b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java @@ -108,7 +108,7 @@ LoadBalancer createPublicLoadBalancerRule(String xId, String name, String descri /** * Assign a virtual machine or list of virtual machines, or Map of to a load balancer. */ - boolean assignToLoadBalancer(long lbRuleId, List vmIds, Map> vmIdIpMap, boolean isAutoScaleVM); + boolean assignToLoadBalancer(long lbRuleId, List vmIds, Map> vmIdIpMap, Map vmIdNetworkMap, boolean isAutoScaleVM); boolean assignSSLCertToLoadBalancerRule(Long lbRuleId, String certName, String publicCert, String privateKey); diff --git a/api/src/main/java/com/cloud/network/rules/FirewallRule.java b/api/src/main/java/com/cloud/network/rules/FirewallRule.java index 369c6aa57eb8..38ba009163ce 100644 --- a/api/src/main/java/com/cloud/network/rules/FirewallRule.java +++ b/api/src/main/java/com/cloud/network/rules/FirewallRule.java @@ -69,7 +69,9 @@ enum TrafficType { State getState(); - long getNetworkId(); + Long getNetworkId(); + + Long getVpcId(); Long getSourceIpAddressId(); diff --git a/api/src/main/java/com/cloud/network/vpc/Vpc.java b/api/src/main/java/com/cloud/network/vpc/Vpc.java index b94089d2d433..a0686e2bf7d0 100644 --- a/api/src/main/java/com/cloud/network/vpc/Vpc.java +++ b/api/src/main/java/com/cloud/network/vpc/Vpc.java @@ -107,4 +107,6 @@ public enum State { String getIp6Dns2(); boolean useRouterIpAsResolver(); + + boolean getKeepMacAddressOnPublicNic(); } diff --git a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java index 17f49bb36521..f84602232159 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java @@ -84,4 +84,6 @@ public enum State { NetworkOffering.RoutingMode getRoutingMode(); Boolean isSpecifyAsNumber(); + + boolean isConserveMode(); } diff --git a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java index 97b95339ecf3..891cfb02d9df 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.apache.cloudstack.api.command.admin.vpc.CloneVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.CreateVPCOfferingCmd; import org.apache.cloudstack.api.command.admin.vpc.UpdateVPCOfferingCmd; import org.apache.cloudstack.api.command.user.vpc.ListVPCOfferingsCmd; @@ -34,12 +35,14 @@ public interface VpcProvisioningService { VpcOffering createVpcOffering(CreateVPCOfferingCmd cmd); + VpcOffering cloneVPCOffering(CloneVPCOfferingCmd cmd); + VpcOffering createVpcOffering(String name, String displayText, List supportedServices, Map> serviceProviders, Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol, Long serviceOfferingId, String externalProvider, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, VpcOffering.State state, - NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber); + NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber, boolean conserveMode); Pair,Integer> listVpcOfferings(ListVPCOfferingsCmd cmd); diff --git a/api/src/main/java/com/cloud/network/vpc/VpcService.java b/api/src/main/java/com/cloud/network/vpc/VpcService.java index c1546609d2b7..3d0ba43263f5 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcService.java @@ -58,7 +58,7 @@ public interface VpcService { */ Vpc createVpc(long zoneId, long vpcOffId, long vpcOwnerId, String vpcName, String displayText, String cidr, String networkDomain, String ip4Dns1, String ip4Dns2, String ip6Dns1, String ip6Dns2, Boolean displayVpc, Integer publicMtu, Integer cidrSize, - Long asNumber, List bgpPeerIds, Boolean useVrIpResolver) throws ResourceAllocationException; + Long asNumber, List bgpPeerIds, Boolean useVrIpResolver, boolean keepMacAddressOnPublicNic) throws ResourceAllocationException; /** * Persists VPC record in the database @@ -104,7 +104,7 @@ Vpc createVpc(long zoneId, long vpcOffId, long vpcOwnerId, String vpcName, Strin * @throws ResourceUnavailableException if during restart some resources may not be available * @throws InsufficientCapacityException if for instance no address space, compute or storage is sufficiently available */ - Vpc updateVpc(long vpcId, String vpcName, String displayText, String customId, Boolean displayVpc, Integer mtu, String sourceNatIp) throws ResourceUnavailableException, InsufficientCapacityException; + Vpc updateVpc(long vpcId, String vpcName, String displayText, String customId, Boolean displayVpc, Integer mtu, String sourceNatIp, Boolean keepMacAddressOnPublicNic) throws ResourceUnavailableException, InsufficientCapacityException; /** * Lists VPC(s) based on the parameters passed to the API call diff --git a/api/src/main/java/com/cloud/offering/DiskOfferingInfo.java b/api/src/main/java/com/cloud/offering/DiskOfferingInfo.java index 12dcf423e34f..197565a1fccb 100644 --- a/api/src/main/java/com/cloud/offering/DiskOfferingInfo.java +++ b/api/src/main/java/com/cloud/offering/DiskOfferingInfo.java @@ -23,6 +23,7 @@ public class DiskOfferingInfo { private Long _size; private Long _minIops; private Long _maxIops; + private Long _kmsKeyId; public DiskOfferingInfo() { } @@ -38,6 +39,14 @@ public DiskOfferingInfo(DiskOffering diskOffering, Long size, Long minIops, Long _maxIops = maxIops; } + public DiskOfferingInfo(DiskOffering diskOffering, Long size, Long minIops, Long maxIops, Long kmsKeyId) { + _diskOffering = diskOffering; + _size = size; + _minIops = minIops; + _maxIops = maxIops; + _kmsKeyId = kmsKeyId; + } + public void setDiskOffering(DiskOffering diskOffering) { _diskOffering = diskOffering; } @@ -69,4 +78,12 @@ public void setMaxIops(Long maxIops) { public Long getMaxIops() { return _maxIops; } + + public void setKmsKeyId(Long kmsKeyId) { + _kmsKeyId = kmsKeyId; + } + + public Long getKmsKeyId() { + return _kmsKeyId; + } } diff --git a/api/src/main/java/com/cloud/projects/ProjectService.java b/api/src/main/java/com/cloud/projects/ProjectService.java index 5080cb5a7812..d11e9ae0446d 100644 --- a/api/src/main/java/com/cloud/projects/ProjectService.java +++ b/api/src/main/java/com/cloud/projects/ProjectService.java @@ -82,7 +82,7 @@ public interface ProjectService { Project updateProject(long id, String name, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException; - boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType); + boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType) throws ResourceAllocationException; boolean deleteAccountFromProject(long projectId, String accountName); @@ -100,6 +100,6 @@ public interface ProjectService { Project findByProjectAccountIdIncludingRemoved(long projectAccountId); - boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole); + boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole) throws ResourceAllocationException; } diff --git a/api/src/main/java/com/cloud/server/ManagementService.java b/api/src/main/java/com/cloud/server/ManagementService.java index 51737546ffad..bcca229c06bc 100644 --- a/api/src/main/java/com/cloud/server/ManagementService.java +++ b/api/src/main/java/com/cloud/server/ManagementService.java @@ -71,7 +71,6 @@ import org.apache.cloudstack.api.command.user.vmgroup.UpdateVMGroupCmd; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.config.ConfigurationGroup; -import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.alert.Alert; import com.cloud.capacity.Capacity; @@ -108,14 +107,6 @@ public interface ManagementService { static final String Name = "management-server"; - ConfigKey JsInterpretationEnabled = new ConfigKey<>("Hidden" - , Boolean.class - , "js.interpretation.enabled" - , "false" - , "Enable/Disable all JavaScript interpretation related functionalities to create or update Javascript rules." - , false - , ConfigKey.Scope.Global); - /** * returns the a map of the names/values in the configuration table * @@ -534,6 +525,4 @@ VirtualMachine upgradeSystemVM(ScaleSystemVMCmd cmd) throws ResourceUnavailableE boolean removeManagementServer(RemoveManagementServerCmd cmd); - void checkJsInterpretationAllowedIfNeededForParameterValue(String paramName, boolean paramValue); - } diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 5b3e97698fda..3511b4e88cb9 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -35,7 +35,8 @@ public static enum ImageFormat { VDI(true, true, false, "vdi"), TAR(false, false, false, "tar"), ZIP(false, false, false, "zip"), - DIR(false, false, false, "dir"); + DIR(false, false, false, "dir"), + PNG(false, false, false, "png"); private final boolean supportThinProvisioning; private final boolean supportSparse; @@ -170,6 +171,7 @@ public static enum StoragePoolType { ISO(false, false, EncryptionSupport.Unsupported), // for iso image LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR CLVM(true, false, EncryptionSupport.Unsupported), + CLVM_NG(true, false, EncryptionSupport.Hypervisor), RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD SharedMountPoint(true, true, EncryptionSupport.Hypervisor), VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage diff --git a/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java b/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java index db702a61f2bc..7d5b2d7c57d7 100644 --- a/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java +++ b/api/src/main/java/com/cloud/storage/VMTemplateStorageResourceAssoc.java @@ -23,9 +23,10 @@ public interface VMTemplateStorageResourceAssoc extends InternalIdentity { public static enum Status { - UNKNOWN, DOWNLOAD_ERROR, NOT_DOWNLOADED, DOWNLOAD_IN_PROGRESS, DOWNLOADED, ABANDONED, UPLOADED, NOT_UPLOADED, UPLOAD_ERROR, UPLOAD_IN_PROGRESS, CREATING, CREATED, BYPASSED + UNKNOWN, DOWNLOAD_ERROR, NOT_DOWNLOADED, DOWNLOAD_IN_PROGRESS, DOWNLOADED, ABANDONED, LIMIT_REACHED, UPLOADED, NOT_UPLOADED, UPLOAD_ERROR, UPLOAD_IN_PROGRESS, CREATING, CREATED, BYPASSED } + List ERROR_DOWNLOAD_STATES = List.of(Status.DOWNLOAD_ERROR, Status.ABANDONED, Status.LIMIT_REACHED, Status.UNKNOWN); List PENDING_DOWNLOAD_STATES = List.of(Status.NOT_DOWNLOADED, Status.DOWNLOAD_IN_PROGRESS); String getInstallPath(); diff --git a/api/src/main/java/com/cloud/storage/Volume.java b/api/src/main/java/com/cloud/storage/Volume.java index c7fbdb0a5445..89298e04587f 100644 --- a/api/src/main/java/com/cloud/storage/Volume.java +++ b/api/src/main/java/com/cloud/storage/Volume.java @@ -60,7 +60,9 @@ enum State { UploadError(false, "Volume upload encountered some error"), UploadAbandoned(false, "Volume upload is abandoned since the upload was never initiated within a specified time"), Attaching(true, "The volume is attaching to a VM from Ready state."), - Restoring(true, "The volume is being restored from backup."); + Restoring(true, "The volume is being restored from backup."), + Consolidating(true, "The volume is being flattened."), + RestoreError(false, "The volume restore encountered an error."); boolean _transitional; @@ -153,6 +155,10 @@ public String getDescription() { s_fsm.addTransition(new StateMachine2.Transition(Destroy, Event.RestoreRequested, Restoring, null)); s_fsm.addTransition(new StateMachine2.Transition(Restoring, Event.RestoreSucceeded, Ready, null)); s_fsm.addTransition(new StateMachine2.Transition(Restoring, Event.RestoreFailed, Ready, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Ready, Event.ConsolidationRequested, Consolidating, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Consolidating, Event.OperationSucceeded, Ready, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(Consolidating, Event.OperationFailed, RestoreError, null)); + s_fsm.addTransition(new StateMachine2.Transition<>(RestoreError, Event.RestoreFailed, RestoreError, null)); } } @@ -179,7 +185,8 @@ enum Event { OperationTimeout, RestoreRequested, RestoreSucceeded, - RestoreFailed; + RestoreFailed, + ConsolidationRequested } /** @@ -275,6 +282,14 @@ enum Event { void setPassphraseId(Long id); + Long getKmsKeyId(); + + void setKmsKeyId(Long id); + + Long getKmsWrappedKeyId(); + + void setKmsWrappedKeyId(Long id); + String getEncryptFormat(); void setEncryptFormat(String encryptFormat); diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 1a9bcc6ee98b..372eb0385618 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import com.cloud.dc.DataCenter; import com.cloud.exception.ResourceAllocationException; import com.cloud.offering.DiskOffering; import com.cloud.user.Account; @@ -70,6 +71,10 @@ public interface VolumeApiService { */ Volume allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationException; + Volume allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Long snapshotId, String name, + Long cmdSize, Boolean displayVolume, Long cmdMinIops, Long cmdMaxIops, String customId, Long kmsKeyId) + throws ResourceAllocationException; + /** * Creates the volume based on the given criteria * @@ -80,6 +85,8 @@ public interface VolumeApiService { */ Volume createVolume(CreateVolumeCmd cmd); + Volume createVolume(long volumeId, Long vmId, Long snapshotId, Long storageId, Boolean display); + /** * Resizes the volume based on the given criteria * @@ -107,7 +114,7 @@ public interface VolumeApiService { Volume attachVolumeToVM(AttachVolumeCmd command); - Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS); + Volume attachVolumeToVM(Long vmId, Long volumeId, Long deviceId, Boolean allowAttachForSharedFS, boolean allowAttachOnRestoring); Volume detachVolumeViaDestroyVM(long vmId, long volumeId); @@ -182,7 +189,7 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId, boolean validateConditionsToReplaceDiskOfferingOfVolume(Volume volume, DiskOffering newDiskOffering, StoragePool destPool); - Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); + Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge, Boolean countDisplayFalseInResourceCount); void destroyVolume(long volumeId); @@ -203,4 +210,6 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId, Pair checkAndRepairVolume(CheckAndRepairVolumeCmd cmd) throws ResourceAllocationException; Long getVolumePhysicalSize(Storage.ImageFormat format, String path, String chainInfo); + + Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone, boolean encryptEnabledOnly); } diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index b92654bfe174..fc450e9179c5 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -21,12 +21,13 @@ import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,6 +36,14 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.command.admin.user.DeleteUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeyRulesCmd; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.backup.BackupOffering; @@ -59,7 +68,8 @@ UserAccount createUserAccount(String userName, String password, String firstName User getSystemUser(); - User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID); + User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, + String accountName, Long domainId, String userUUID, boolean isPasswordChangeRequired); User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID, User.Source source); @@ -78,10 +88,16 @@ User createUser(String userName, String password, String firstName, String lastN Account getActiveAccountById(long accountId); + Account getActiveAccountByUuid(String accountUuid); + Account getAccount(long accountId); + Account getAccountByUuid(String accountUuid); + User getActiveUser(long userId); + User getOneActiveUserForAccount(Account account); + User getUserIncludingRemoved(long userId); boolean isRootAdmin(Long accountId); @@ -96,7 +112,7 @@ User createUser(String userName, String password, String firstName, String lastN void markUserRegistered(long userId); - public String[] createApiKeyAndSecretKey(RegisterUserKeyCmd cmd); + ApiKeyPair createApiKeyAndSecretKey(RegisterUserKeysCmd cmd); public String[] createApiKeyAndSecretKey(final long userId); @@ -124,8 +140,12 @@ User createUser(String userName, String password, String firstName, String lastN void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource); + void validateCallingUserHasAccessToDesiredUser(Long userId); + Long finalizeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly); + Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId); + /** * returns the user account object for a given user id * @param userId user id @@ -133,9 +153,15 @@ User createUser(String userName, String password, String firstName, String lastN */ UserAccount getUserAccountById(Long userId); - public Pair> getKeys(GetUserKeysCmd cmd); + Pair> getKeys(GetUserKeysCmd cmd); + + ListResponse listKeys(ListUserKeysCmd cmd); + + List listKeyRules(ListUserKeyRulesCmd cmd); + + void deleteApiKey(DeleteUserKeysCmd cmd); - public Pair> getKeys(Long userId); + void deleteApiKey(ApiKeyPair id); /** * Lists user two-factor authentication provider plugins @@ -150,4 +176,13 @@ User createUser(String userName, String password, String firstName, String lastN */ UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId); + ApiKeyPair getLatestUserKeyPair(Long userId); + + ApiKeyPair getKeyPairById(Long id); + + ApiKeyPair getKeyPairByApiKey(String apiKey); + + String getAccessingApiKey(BaseCmd cmd); + + List getAllKeypairPermissions(String apiKey); } diff --git a/api/src/main/java/com/cloud/user/ApiKeyPairState.java b/api/src/main/java/com/cloud/user/ApiKeyPairState.java new file mode 100644 index 000000000000..63405c62e320 --- /dev/null +++ b/api/src/main/java/com/cloud/user/ApiKeyPairState.java @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.user; + +public enum ApiKeyPairState { + ENABLED, REMOVED, EXPIRED +} diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 738e593582b4..89128f87829e 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -30,6 +30,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.ServiceOffering; import com.cloud.template.VirtualMachineTemplate; +import org.apache.cloudstack.resourcelimit.Reserver; public interface ResourceLimitService { @@ -191,6 +192,7 @@ public interface ResourceLimitService { */ public void checkResourceLimit(Account account, ResourceCount.ResourceType type, long... count) throws ResourceAllocationException; public void checkResourceLimitWithTag(Account account, ResourceCount.ResourceType type, String tag, long... count) throws ResourceAllocationException; + public void checkResourceLimitWithTag(Account account, Long domainId, boolean considerSystemAccount, ResourceCount.ResourceType type, String tag, long... count) throws ResourceAllocationException; /** * Gets the count of resources for a resource type and account @@ -251,15 +253,15 @@ public interface ResourceLimitService { List getResourceLimitStorageTags(DiskOffering diskOffering); void updateTaggedResourceLimitsAndCountsForAccounts(List responses, String tag); void updateTaggedResourceLimitsAndCountsForDomains(List responses, String tag); - void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException; - + void checkVolumeResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; + List getResourceLimitStorageTagsForResourceCountOperation(Boolean display, DiskOffering diskOffering, Boolean enforceResourceLimitOnDisplayFalse); void checkVolumeResourceLimitForDiskOfferingChange(Account owner, Boolean display, Long currentSize, Long newSize, - DiskOffering currentOffering, DiskOffering newOffering) throws ResourceAllocationException; + DiskOffering currentOffering, DiskOffering newOffering, List reservations) throws ResourceAllocationException; - void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering) throws ResourceAllocationException; + void checkPrimaryStorageResourceLimit(Account owner, Boolean display, Long size, DiskOffering diskOffering, List reservations) throws ResourceAllocationException; void incrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); - void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); + void decrementVolumeResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering, Boolean countDisplayFalseInResourceCount); void updateVmResourceCountForTemplateChange(long accountId, Boolean display, ServiceOffering offering, VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate); @@ -273,25 +275,23 @@ void updateVolumeResourceCountForDiskOfferingChange(long accountId, Boolean disp void incrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); void decrementVolumePrimaryStorageResourceCount(long accountId, Boolean display, Long size, DiskOffering diskOffering); - void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template) throws ResourceAllocationException; - void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); - void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template); + void checkVmResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; + void incrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Boolean countDisplayFalseInResourceLimit); + void decrementVmResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Boolean countDisplayFalseInResourceCount); void checkVmResourceLimitsForServiceOfferingChange(Account owner, Boolean display, Long currentCpu, Long newCpu, - Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template) throws ResourceAllocationException; + Long currentMemory, Long newMemory, ServiceOffering currentOffering, ServiceOffering newOffering, VirtualMachineTemplate template, List reservations) throws ResourceAllocationException; void checkVmResourceLimitsForTemplateChange(Account owner, Boolean display, ServiceOffering offering, - VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate) throws ResourceAllocationException; + VirtualMachineTemplate currentTemplate, VirtualMachineTemplate newTemplate, List reservations) throws ResourceAllocationException; - void checkVmCpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu) throws ResourceAllocationException; void incrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu); void decrementVmCpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long cpu); - void checkVmMemoryResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory) throws ResourceAllocationException; void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); - void checkVmGpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu) throws ResourceAllocationException; void incrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu); void decrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu); + long recalculateDomainResourceCount(final long domainId, final ResourceType type, String tag); } diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index 041b39ad2729..da7245a47980 100644 --- a/api/src/main/java/com/cloud/user/User.java +++ b/api/src/main/java/com/cloud/user/User.java @@ -65,14 +65,6 @@ public enum Source { public void setState(Account.State state); - public String getApiKey(); - - public void setApiKey(String apiKey); - - public String getSecretKey(); - - public void setSecretKey(String secretKey); - public String getTimezone(); public void setTimezone(String timezone); diff --git a/api/src/main/java/com/cloud/user/UserAccount.java b/api/src/main/java/com/cloud/user/UserAccount.java index e6b07fb371eb..5736244e3259 100644 --- a/api/src/main/java/com/cloud/user/UserAccount.java +++ b/api/src/main/java/com/cloud/user/UserAccount.java @@ -39,10 +39,6 @@ public interface UserAccount extends InternalIdentity { String getState(); - String getApiKey(); - - String getSecretKey(); - Date getCreated(); Date getRemoved(); diff --git a/api/src/main/java/com/cloud/vm/DiskProfile.java b/api/src/main/java/com/cloud/vm/DiskProfile.java index 971ebde496e4..766573d4d40b 100644 --- a/api/src/main/java/com/cloud/vm/DiskProfile.java +++ b/api/src/main/java/com/cloud/vm/DiskProfile.java @@ -82,7 +82,7 @@ public DiskProfile(Volume vol, DiskOffering offering, HypervisorType hyperType) null); this.hyperType = hyperType; this.provisioningType = offering.getProvisioningType(); - this.requiresEncryption = offering.getEncrypt() || vol.getPassphraseId() != null; + this.requiresEncryption = offering.getEncrypt() || vol.getPassphraseId() != null || vol.getKmsKeyId() != null; } public DiskProfile(DiskProfile dp) { diff --git a/api/src/main/java/com/cloud/vm/Nic.java b/api/src/main/java/com/cloud/vm/Nic.java index afc44b8d39fa..3722e5769c92 100644 --- a/api/src/main/java/com/cloud/vm/Nic.java +++ b/api/src/main/java/com/cloud/vm/Nic.java @@ -33,6 +33,11 @@ * Nic represents one nic on the VM. */ public interface Nic extends Identity, InternalIdentity { + + interface Topics { + String NIC_LIFECYCLE = "nic.lifecycle"; + } + enum Event { ReservationRequested, ReleaseRequested, CancelRequested, OperationCompleted, OperationFailed, } @@ -162,4 +167,6 @@ public enum ReservationStrategy { String getIPv6Address(); Integer getMtu(); + + boolean isEnabled(); } diff --git a/api/src/main/java/com/cloud/vm/NicProfile.java b/api/src/main/java/com/cloud/vm/NicProfile.java index a0c80ceb1bfb..54a32bbcb181 100644 --- a/api/src/main/java/com/cloud/vm/NicProfile.java +++ b/api/src/main/java/com/cloud/vm/NicProfile.java @@ -52,6 +52,7 @@ public class NicProfile implements InternalIdentity, Serializable { boolean defaultNic; Integer networkRate; boolean isSecurityGroupEnabled; + boolean enabled; Integer orderIndex; @@ -87,6 +88,7 @@ public NicProfile(Nic nic, Network network, URI broadcastUri, URI isolationUri, broadcastType = network.getBroadcastDomainType(); trafficType = network.getTrafficType(); format = nic.getAddressFormat(); + enabled = nic.isEnabled(); iPv4Address = nic.getIPv4Address(); iPv4Netmask = nic.getIPv4Netmask(); @@ -414,6 +416,14 @@ public void setIpv4AllocationRaceCheck(boolean ipv4AllocationRaceCheck) { this.ipv4AllocationRaceCheck = ipv4AllocationRaceCheck; } + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + // // OTHER METHODS // @@ -453,6 +463,6 @@ public String toString() { return String.format("NicProfile %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( this, "id", "uuid", "vmId", "deviceId", - "broadcastUri", "reservationId", "iPv4Address")); + "broadcastType", "broadcastUri", "reservationId", "iPv4Address")); } } diff --git a/api/src/main/java/com/cloud/vm/NicSecondaryIp.java b/api/src/main/java/com/cloud/vm/NicSecondaryIp.java index 2856e0aea756..d25627c1782d 100644 --- a/api/src/main/java/com/cloud/vm/NicSecondaryIp.java +++ b/api/src/main/java/com/cloud/vm/NicSecondaryIp.java @@ -38,6 +38,8 @@ public interface NicSecondaryIp extends ControlledEntity, Identity, InternalIden String getIp6Address(); + String getDescription(); + long getNetworkId(); long getVmId(); diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 01f11b73cd41..5864e91cd7f4 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -40,6 +40,7 @@ import org.apache.cloudstack.api.command.user.vm.StartVMCmd; import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVmNicCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; @@ -74,10 +75,11 @@ public interface UserVmService { * Destroys one virtual machine * * @param cmd the API Command Object containg the parameters to use for this service action + * @param checkExpunge * @throws ConcurrentOperationException * @throws ResourceUnavailableException */ - UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, ConcurrentOperationException; + UserVm destroyVm(DestroyVMCmd cmd, boolean checkExpunge) throws ResourceUnavailableException, ConcurrentOperationException; /** * Destroys one virtual machine @@ -152,6 +154,8 @@ void startVirtualMachineForHA(VirtualMachine vm, Map sshKeyPairs, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, - Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, + Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, Long rootDiskKmsKeyId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** @@ -302,7 +306,7 @@ UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOfferin List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, List dataDiskInfoList, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, Long rootDiskKmsKeyId, String vmType, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** * Creates a User VM in Advanced Zone (Security Group feature is disabled) @@ -374,7 +378,7 @@ UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffe String hostName, String displayName, Long diskOfferingId, Long diskSize, List dataDiskInfoList, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, - Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId, Volume volume, Snapshot snapshot) + Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId, Long rootDiskKmsKeyId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; @@ -524,6 +528,7 @@ UserVm upgradeVirtualMachine(ScaleVMCmd cmd) throws ResourceUnavailableException * @param userId user ID * @param serviceOffering service offering for the imported VM * @param sshPublicKey ssh key for the imported VM + * @param guestOsId guest OS ID for the imported VM (if not passed, then the guest OS of the template will be used) * @param hostName the name for the imported VM * @param hypervisorType hypervisor type for the imported VM * @param customParameters details for the imported VM @@ -533,7 +538,7 @@ UserVm upgradeVirtualMachine(ScaleVMCmd cmd) throws ResourceUnavailableException * @throws InsufficientCapacityException in case of errors */ UserVm importVM(final DataCenter zone, final Host host, final VirtualMachineTemplate template, final String instanceNameInternal, final String displayName, final Account owner, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, - final long accountId, final long userId, final ServiceOffering serviceOffering, final String sshPublicKey, + final long accountId, final long userId, final ServiceOffering serviceOffering, final String sshPublicKey, final Long guestOsId, final String hostName, final HypervisorType hypervisorType, final Map customParameters, final VirtualMachine.PowerState powerState, final LinkedHashMap> networkNicMap) throws InsufficientCapacityException; diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index d244de7115e8..3adcc85d28a1 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -58,7 +58,10 @@ public enum State { Error(false, "VM is in error"), Unknown(false, "VM state is unknown."), Shutdown(false, "VM state is shutdown from inside"), - Restoring(true, "VM is being restored from backup"); + Restoring(true, "VM is being restored from backup"), + BackingUp(true, "VM is being backed up"), + BackupError(false, "VM backup is in an inconsistent state. Operator should analyse the logs and restore the VM"), + RestoreError(false, "VM restore left the VM in an inconsistent state. Operator should analyse the logs and restore the VM"); private final boolean _transitional; String _description; @@ -124,6 +127,9 @@ public static StateMachine2 getStat s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.StopRequested, State.Stopping, null)); s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.AgentReportShutdowned, State.Stopped, Arrays.asList(new Impact[]{Impact.USAGE}))); s_fsm.addTransition(new Transition(State.Expunging, VirtualMachine.Event.OperationFailed, State.Expunging,null)); + // Note: In addition to the Stopped -> Error transition for failed VM creation, + // a VM can also transition from Expunging to Error on OperationFailedToError. + s_fsm.addTransition(new Transition(State.Expunging, VirtualMachine.Event.OperationFailedToError, State.Error, null)); s_fsm.addTransition(new Transition(State.Expunging, VirtualMachine.Event.ExpungeOperation, State.Expunging,null)); s_fsm.addTransition(new Transition(State.Error, VirtualMachine.Event.DestroyRequested, State.Expunging, null)); s_fsm.addTransition(new Transition(State.Error, VirtualMachine.Event.ExpungeOperation, State.Expunging, null)); @@ -131,6 +137,14 @@ public static StateMachine2 getStat s_fsm.addTransition(new Transition(State.Destroyed, Event.RestoringRequested, State.Restoring, null)); s_fsm.addTransition(new Transition(State.Restoring, Event.RestoringSuccess, State.Stopped, null)); s_fsm.addTransition(new Transition(State.Restoring, Event.RestoringFailed, State.Stopped, null)); + s_fsm.addTransition(new Transition<>(State.Running, Event.BackupRequested, State.BackingUp, null)); + s_fsm.addTransition(new Transition<>(State.Stopped, Event.BackupRequested, State.BackingUp, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.BackupSucceededRunning, State.Running, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.BackupSucceededStopped, State.Stopped, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToError, State.BackupError, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToRunning, State.Running, null)); + s_fsm.addTransition(new Transition<>(State.BackingUp, Event.OperationFailedToStopped, State.Stopped, null)); + s_fsm.addTransition(new Transition(State.RestoreError, Event.RestoringFailed, State.RestoreError, null)); s_fsm.addTransition(new Transition(State.Starting, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, Arrays.asList(new Impact[]{Impact.USAGE}))); s_fsm.addTransition(new Transition(State.Stopping, VirtualMachine.Event.FollowAgentPowerOnReport, State.Running, null)); @@ -209,6 +223,8 @@ public enum Event { ExpungeOperation, OperationSucceeded, OperationFailed, + OperationFailedToRunning, + OperationFailedToStopped, OperationFailedToError, OperationRetry, AgentReportShutdowned, @@ -218,6 +234,10 @@ public enum Event { RestoringRequested, RestoringFailed, RestoringSuccess, + BackupRequested, + BackupSucceededStopped, + BackupSucceededRunning, + FinalizedBackupChain, // added for new VMSync logic FollowAgentPowerOnReport, diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index db7665724973..877df55c6d67 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -96,6 +96,7 @@ public interface VmDetailConstants { String CKS_NODE_TYPE = "node"; String OFFERING = "offering"; String TEMPLATE = "template"; + String AFFINITY_GROUP = "affinitygroup"; // VMware to KVM VM migrations specific String VMWARE_TO_KVM_PREFIX = "vmware-to-kvm"; @@ -129,4 +130,20 @@ public interface VmDetailConstants { String EXTERNAL_DETAIL_PREFIX = "External:"; String CLOUDSTACK_VM_DETAILS = "cloudstack.vm.details"; String CLOUDSTACK_VLAN = "cloudstack.vlan"; + + // KVM Checkpoints related + String ACTIVE_CHECKPOINT_ID = "active.checkpoint.id"; + String ACTIVE_CHECKPOINT_CREATE_TIME = "active.checkpoint.create.time"; + String LAST_CHECKPOINT_ID = "last.checkpoint.id"; + String LAST_CHECKPOINT_CREATE_TIME = "last.checkpoint.create.time"; + + // KBOSS specific + String LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS = "linkedVolumesSecondaryStorageUuids"; + String VALIDATION_COMMAND = "backupValidationCommand"; + String VALIDATION_COMMAND_ARGUMENTS = "backupValidationCommandArguments"; + String VALIDATION_COMMAND_EXPECTED_RESULT = "backupValidationCommandExpectedResult"; + String VALIDATION_COMMAND_TIMEOUT = "backupValidationCommandTimeout"; + String VALIDATION_SCREENSHOT_WAIT = "backupValidationScreenshotWait"; + String VALIDATION_BOOT_TIMEOUT = "backupValidationBootTimeout"; + String LAST_KNOWN_STATE = "last_known_state"; } diff --git a/api/src/main/java/com/cloud/vm/VmDiskInfo.java b/api/src/main/java/com/cloud/vm/VmDiskInfo.java index b8779a8d77c6..97683e8397fa 100644 --- a/api/src/main/java/com/cloud/vm/VmDiskInfo.java +++ b/api/src/main/java/com/cloud/vm/VmDiskInfo.java @@ -33,6 +33,11 @@ public VmDiskInfo(DiskOffering diskOffering, Long size, Long minIops, Long maxIo _deviceId = deviceId; } + public VmDiskInfo(DiskOffering diskOffering, Long size, Long minIops, Long maxIops, Long deviceId, Long kmsKeyId) { + super(diskOffering, size, minIops, maxIops, kmsKeyId); + _deviceId = deviceId; + } + public Long getDeviceId() { return _deviceId; } diff --git a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java index 660f64f43ef2..286a3598e4fb 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java +++ b/api/src/main/java/org/apache/cloudstack/acl/APIChecker.java @@ -20,6 +20,7 @@ import com.cloud.user.Account; import com.cloud.user.User; import com.cloud.utils.component.Adapter; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import java.util.List; @@ -31,8 +32,8 @@ public interface APIChecker extends Adapter { // If true, apiChecker has checked the operation // If false, apiChecker is unable to handle the operation or not implemented // On exception, checkAccess failed don't allow - boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException; - boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException; + boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; + boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException; /** * Verifies if the account has permission for the given list of APIs and returns only the allowed ones. * @@ -43,4 +44,5 @@ public interface APIChecker extends Adapter { */ List getApisAllowedToUser(Role role, User user, List apiNames) throws PermissionDeniedException; boolean isEnabled(); + List getImplicitRolePermissions(RoleType roleType); } diff --git a/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java b/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java index 251c6b6d3f9e..f382b1c6964f 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RolePermissionEntity.java @@ -21,7 +21,7 @@ import org.apache.cloudstack.api.InternalIdentity; public interface RolePermissionEntity extends InternalIdentity, Identity { - public enum Permission { + enum Permission { ALLOW, DENY } Rule getRule(); diff --git a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java index f041c8342aec..14e0a608a925 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleService.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleService.java @@ -104,5 +104,26 @@ public interface RoleService { List findAllPermissionsBy(Long roleId); + List findAllRolePermissionsEntityBy(Long roleId, boolean considerImplicitRules); + Permission getRolePermission(String permission); + + int removeRolesIfNeeded(List roles); + + /** + * Checks if the role of the caller account has compatible permissions of the specified role permissions. + * For each permission of the {@param rolePermissionsToAccess}, the role of the caller needs to contain the same permission. + * + * @param rolePermissions the permissions of the caller role. + * @param rolePermissionsToAccess the permissions for the role that the caller role wants to access. + * @return True if the role can be accessed with the given permissions; false otherwise. + */ + boolean roleHasPermission(Map rolePermissions, List rolePermissionsToAccess); + + /** + * Given a list of role permissions, returns a {@link Map} containing the API name as the key and the {@link RolePermissionEntity} for the API as the value. + * + * @param rolePermissions Permissions for the role from role. + */ + Map getRoleRulesAndPermissions(List rolePermissions); } diff --git a/api/src/main/java/org/apache/cloudstack/acl/Rule.java b/api/src/main/java/org/apache/cloudstack/acl/Rule.java index a4ef7773f67b..ad01825a95f1 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/Rule.java +++ b/api/src/main/java/org/apache/cloudstack/acl/Rule.java @@ -25,16 +25,18 @@ public final class Rule { private final String rule; + private final Pattern matchingPattern; private final static Pattern ALLOWED_PATTERN = Pattern.compile("^[a-zA-Z0-9*]+$"); public Rule(final String rule) { validate(rule); this.rule = rule; + matchingPattern = Pattern.compile(rule.toLowerCase().replace("*", "(\\w*\\*?)+")); } public boolean matches(final String commandName) { - return StringUtils.isNotEmpty(commandName) - && commandName.toLowerCase().matches(rule.toLowerCase().replace("*", "\\w*")); + return StringUtils.isNotEmpty(commandName) && + matchingPattern.matcher(commandName.toLowerCase()).matches(); } public String getRuleString() { diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java new file mode 100644 index 000000000000..ecce0ae50824 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPair.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.apikeypair; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface ApiKeyPair extends ControlledEntity, InternalIdentity, Identity { + Long getUserId(); + Date getStartDate(); + Date getEndDate(); + Date getCreated(); + String getDescription(); + String getApiKey(); + String getSecretKey(); + String getName(); + Date getRemoved(); + void setRemoved(Date date); + void validateDate(); + boolean hasEndDatePassed(); +} diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java new file mode 100644 index 000000000000..60b3834cc073 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairPermission.java @@ -0,0 +1,23 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.apikeypair; + +import org.apache.cloudstack.acl.RolePermissionEntity; + +public interface ApiKeyPairPermission extends RolePermissionEntity { + long getApiKeyPairId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java new file mode 100644 index 000000000000..de9c829b17dc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/acl/apikeypair/ApiKeyPairService.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.apikeypair; + +import java.util.List; + +public interface ApiKeyPairService { + List findAllPermissionsByKeyPairId(Long apiKeyPairId, Long roleId); + + ApiKeyPair findByApiKey(String apiKey); + + ApiKeyPair findById(Long id); +} diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java index 018e5f5bab5a..03992c0c1c7c 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityGroupService.java @@ -66,5 +66,4 @@ public interface AffinityGroupService { boolean isAffinityGroupAvailableInDomain(long affinityGroupId, long domainId); - } diff --git a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java index 9995d8039e1f..96ca35f264ca 100644 --- a/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java +++ b/api/src/main/java/org/apache/cloudstack/affinity/AffinityProcessorBase.java @@ -29,6 +29,9 @@ public class AffinityProcessorBase extends AdapterBase implements AffinityGroupProcessor { + public static final String AFFINITY_TYPE_HOST = "host affinity"; + public static final String AFFINITY_TYPE_HOST_ANTI = "host anti-affinity"; + protected String _type; @Override diff --git a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java index fcc87908bd5d..a9c2abc11ce7 100644 --- a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java @@ -83,6 +83,9 @@ private AlertType(short type, String name, boolean isDefault) { public static final AlertType ALERT_TYPE_VPN_GATEWAY_OBSOLETE_PARAMETERS = new AlertType((short)34, "ALERT.S2S.VPN.GATEWAY.OBSOLETE.PARAMETERS", true, true); public static final AlertType ALERT_TYPE_BACKUP_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_BACKUP_STORAGE, "ALERT.STORAGE.BACKUP", true); public static final AlertType ALERT_TYPE_OBJECT_STORAGE = new AlertType(Capacity.CAPACITY_TYPE_OBJECT_STORAGE, "ALERT.STORAGE.OBJECT", true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_FAILED = new AlertType((short)35, "ALERT.BACKUP.VALIDATION.FAILED", true, true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_UNABLE_TO_VALIDATE = new AlertType((short)36, "ALERT.BACKUP.VALIDATION.UNABLE.TO.VALIDATE", true, true); + public static final AlertType ALERT_TYPE_BACKUP_VALIDATION_CLEANUP_FAILED = new AlertType((short)37, "ALERT.BACKUP.VALIDATION.CLEANUP_FAILED", true, true); public short getType() { return type; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java index 4d33ba859a5b..2aa97b65a3d5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -89,7 +89,9 @@ public enum ApiCommandResourceType { KubernetesSupportedVersion(null), SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class), Extension(org.apache.cloudstack.extension.Extension.class), - ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class); + ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class), + KmsKey(org.apache.cloudstack.kms.KMSKey.class), + HsmProfile(org.apache.cloudstack.kms.HSMProfile.class); private final Class clazz; @@ -127,8 +129,8 @@ public String toString() { } public static ApiCommandResourceType fromString(String value) { - if (StringUtils.isNotEmpty(value) && EnumUtils.isValidEnum(ApiCommandResourceType.class, value)) { - return valueOf(value); + if (StringUtils.isNotBlank(value) && EnumUtils.isValidEnumIgnoreCase(ApiCommandResourceType.class, value)) { + return EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, value); } return null; } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 1ab6fba6081f..ac6acdf42516 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -19,6 +19,8 @@ public class ApiConstants { public static final String ACCOUNT = "account"; public static final String ACCOUNTS = "accounts"; + public static final String ACCOUNT_NAME = "accountname"; + public static final String ACCOUNT_STATE_TO_SHOW = "accountstatetoshow"; public static final String ACCOUNT_TYPE = "accounttype"; public static final String ACCOUNT_ID = "accountid"; public static final String ACCOUNT_IDS = "accountids"; @@ -46,6 +48,7 @@ public class ApiConstants { public static final String AS_NUMBER_ID = "asnumberid"; public static final String ASN_RANGE = "asnrange"; public static final String ASN_RANGE_ID = "asnrangeid"; + public static final String API_KEY_FILTER = "apikeyfilter"; public static final String ASYNC_BACKUP = "asyncbackup"; public static final String AUTO_SELECT = "autoselect"; public static final String USER_API_KEY = "userapikey"; @@ -60,12 +63,15 @@ public class ApiConstants { public static final String BACKUP_LIMIT = "backuplimit"; public static final String BACKUP_OFFERING_NAME = "backupofferingname"; public static final String BACKUP_OFFERING_ID = "backupofferingid"; + public static final String BACKUP_OFFERING_DETAILS = "backupofferingdetails"; public static final String BACKUP_STORAGE_AVAILABLE = "backupstorageavailable"; public static final String BACKUP_STORAGE_LIMIT = "backupstoragelimit"; public static final String BACKUP_STORAGE_TOTAL = "backupstoragetotal"; public static final String BACKUP_VM_OFFERING_REMOVED = "vmbackupofferingremoved"; public static final String IS_BACKUP_VM_EXPUNGED = "isbackupvmexpunged"; public static final String BACKUP_TOTAL = "backuptotal"; + public static final String BALANCE = "balance"; + public static final String BALANCES = "balances"; public static final String BASE64_IMAGE = "base64image"; public static final String BGP_PEERS = "bgppeers"; public static final String BGP_PEER_IDS = "bgppeerids"; @@ -74,6 +80,7 @@ public class ApiConstants { public static final String BOOTABLE = "bootable"; public static final String BIND_DN = "binddn"; public static final String BIND_PASSWORD = "bindpass"; + public static final String BLANK_INSTANCE = "blankinstance"; public static final String BUS_ADDRESS = "busaddress"; public static final String BYTES_READ_RATE = "bytesreadrate"; public static final String BYTES_READ_RATE_MAX = "bytesreadratemax"; @@ -154,6 +161,7 @@ public class ApiConstants { public static final String CUSTOM_ID = "customid"; public static final String CUSTOM_ACTION_ID = "customactionid"; public static final String CUSTOM_JOB_ID = "customjobid"; + public static final String CURRENCY = "currency"; public static final String CURRENT_START_IP = "currentstartip"; public static final String CURRENT_END_IP = "currentendip"; public static final String ENCRYPT = "encrypt"; @@ -167,6 +175,7 @@ public class ApiConstants { public static final String DATACENTER_NAME = "datacentername"; public static final String DATADISKS_DETAILS = "datadisksdetails"; public static final String DATADISK_OFFERING_LIST = "datadiskofferinglist"; + public static final String DATE = "date"; public static final String DEFAULT_VALUE = "defaultvalue"; public static final String DELETE_PROTECTION = "deleteprotection"; public static final String DESCRIPTION = "description"; @@ -194,6 +203,7 @@ public class ApiConstants { public static final String UTILIZATION = "utilization"; public static final String DRIVER = "driver"; public static final String ROOT_DISK_SIZE = "rootdisksize"; + public static final String ROOT_DISK_KMS_KEY_ID = "rootdiskkmskeyid"; public static final String DHCP_OPTIONS_NETWORK_LIST = "dhcpoptionsnetworklist"; public static final String DHCP_OPTIONS = "dhcpoptions"; public static final String DHCP_PREFIX = "dhcp:"; @@ -213,6 +223,7 @@ public class ApiConstants { public static final String DOMAIN_PATH = "domainpath"; public static final String DOMAIN_ID = "domainid"; public static final String DOMAIN__ID = "domainId"; + public static final String DUMMY = "dummy"; public static final String DURATION = "duration"; public static final String ELIGIBLE = "eligible"; public static final String EMAIL = "email"; @@ -256,6 +267,7 @@ public class ApiConstants { public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork"; public static final String FOR_SYSTEM_VMS = "forsystemvms"; public static final String FOR_PROVIDER = "forprovider"; + public static final String FROM_CHECKPOINT_ID = "fromcheckpointid"; public static final String FULL_PATH = "fullpath"; public static final String GATEWAY = "gateway"; public static final String IP6_GATEWAY = "ip6gateway"; @@ -275,6 +287,7 @@ public class ApiConstants { public static final String HEALTH = "health"; public static final String HEADERS = "headers"; public static final String HIDE_IP_ADDRESS_USAGE = "hideipaddressusage"; + public static final String HISTORY = "history"; public static final String HOST_ID = "hostid"; public static final String HOST_IDS = "hostids"; public static final String HOST_IP = "hostip"; @@ -328,6 +341,7 @@ public class ApiConstants { public static final String IS_2FA_VERIFIED = "is2faverified"; public static final String IS_2FA_MANDATED = "is2famandated"; + public static final String IS_ACTIVE = "isactive"; public static final String IS_ASYNC = "isasync"; public static final String IP_AVAILABLE = "ipavailable"; public static final String IP_LIMIT = "iplimit"; @@ -356,6 +370,7 @@ public class ApiConstants { public static final String JOB_STATUS = "jobstatus"; public static final String KEEPALIVE_ENABLED = "keepaliveenabled"; public static final String KERNEL_VERSION = "kernelversion"; + public static final String KEYPAIR_ID = "keypairid"; public static final String KEY = "key"; public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; @@ -437,6 +452,7 @@ public class ApiConstants { public static final String MAX_VGPU_PER_PHYSICAL_GPU = "maxvgpuperphysicalgpu"; public static final String GUEST_OS_LIST = "guestoslist"; public static final String GUEST_OS_COUNT = "guestoscount"; + public static final String GUEST_OS_RULE = "guestosrule"; public static final String OS_MAPPING_CHECK_ENABLED = "osmappingcheckenabled"; public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate"; public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled"; @@ -505,6 +521,7 @@ public class ApiConstants { public static final String REPAIR = "repair"; public static final String REPETITION_ALLOWED = "repetitionallowed"; public static final String REQUIRES_HVM = "requireshvm"; + public static final String RESERVED_RESOURCE_DETAILS = "reservedresourcedetails"; public static final String RESOURCES = "resources"; public static final String RESOURCE_COUNT = "resourcecount"; public static final String RESOURCE_NAME = "resourcename"; @@ -518,12 +535,12 @@ public class ApiConstants { public static final String QUALIFIERS = "qualifiers"; public static final String QUERY_FILTER = "queryfilter"; public static final String QUIESCE_VM = "quiescevm"; + public static final String QUICK_RESTORE = "quickrestore"; public static final String SCHEDULE = "schedule"; public static final String SCHEDULE_ID = "scheduleid"; public static final String SCOPE = "scope"; public static final String SEARCH_BASE = "searchbase"; public static final String SECONDARY_IP = "secondaryip"; - public static final String SECRET_KEY = "secretkey"; public static final String SECURITY_GROUP_IDS = "securitygroupids"; public static final String SECURITY_GROUP_NAMES = "securitygroupnames"; public static final String SECURITY_GROUP_NAME = "securitygroupname"; @@ -537,9 +554,11 @@ public class ApiConstants { public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_REMOVED = "showremoved"; + public static final String SHOW_RESOURCES = "showresources"; public static final String SHOW_RESOURCE_ICON = "showicon"; public static final String SHOW_INACTIVE = "showinactive"; public static final String SHOW_UNIQUE = "showunique"; + public static final String SHOW_PERMISSIONS = "showpermissions"; public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; public static final String SINCE = "since"; @@ -555,6 +574,7 @@ public class ApiConstants { public static final String USE_STORAGE_REPLICATION = "usestoragereplication"; public static final String SOURCE_CIDR_LIST = "sourcecidrlist"; + public static final String SOURCE_OFFERING_ID = "sourceofferingid"; public static final String SOURCE_ZONE_ID = "sourcezoneid"; public static final String SSL_VERIFICATION = "sslverification"; public static final String START_ASN = "startasn"; @@ -566,6 +586,8 @@ public class ApiConstants { public static final String STATE = "state"; public static final String STATS = "stats"; public static final String STATUS = "status"; + public static final String COMPRESSION_STATUS = "compressionstatus"; + public static final String VALIDATION_STATUS = "validationstatus"; public static final String STORAGE_TYPE = "storagetype"; public static final String STORAGE_POLICY = "storagepolicy"; public static final String STORAGE_MOTION_ENABLED = "storagemotionenabled"; @@ -587,6 +609,8 @@ public class ApiConstants { public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; + public static final String TARIFF_ID = "tariffid"; + public static final String TARIFF_NAME = "tariffname"; public static final String TASKS_FILTER = "tasksfilter"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; @@ -600,9 +624,12 @@ public class ApiConstants { public static final String TENANT_NAME = "tenantname"; public static final String TOTAL = "total"; public static final String TOTAL_SUBNETS = "totalsubnets"; + public static final String TO_CHECKPOINT_ID = "tocheckpointid"; + public static final String TOTAL_QUOTA = "totalquota"; public static final String TYPE = "type"; public static final String TRUST_STORE = "truststore"; public static final String TRUST_STORE_PASSWORD = "truststorepass"; + public static final String UNIT = "unit"; public static final String URL = "url"; public static final String USAGE_INTERFACE = "usageinterface"; public static final String USED = "used"; @@ -624,6 +651,7 @@ public class ApiConstants { public static final String USER_CONFIGURABLE = "userconfigurable"; public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist"; public static final String USER_SECRET_KEY = "usersecretkey"; + public static final String USE_VDDK = "usevddk"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String USE_VIRTUAL_ROUTER_IP_RESOLVER = "userouteripresolver"; public static final String UPDATE_IN_SEQUENCE = "updateinsequence"; @@ -644,6 +672,7 @@ public class ApiConstants { public static final String VIRTUAL_MACHINE_STATE = "vmstate"; public static final String VIRTUAL_MACHINES = "virtualmachines"; public static final String USAGE_ID = "usageid"; + public static final String USAGE_NAME = "usagename"; public static final String USAGE_TYPE = "usagetype"; public static final String INCLUDE_TAGS = "includetags"; @@ -657,6 +686,7 @@ public class ApiConstants { public static final String ETCD_SERVICE_OFFERING_NAME = "etcdofferingname"; public static final String REMOVE_VLAN = "removevlan"; public static final String VLAN_ID = "vlanid"; + public static final String ISOLATED = "isolated"; public static final String ISOLATED_PVLAN = "isolatedpvlan"; public static final String ISOLATED_PVLAN_TYPE = "isolatedpvlantype"; public static final String ISOLATION_URI = "isolationuri"; @@ -765,6 +795,7 @@ public class ApiConstants { public static final String ROLE_TYPE = "roletype"; public static final String ROLE_NAME = "rolename"; public static final String PERMISSION = "permission"; + public static final String PERMISSIONS = "permissions"; public static final String RULE = "rule"; public static final String RULES = "rules"; public static final String RULE_ID = "ruleid"; @@ -859,9 +890,17 @@ public class ApiConstants { public static final String IS_SOURCE_NAT = "issourcenat"; public static final String IS_STATIC_NAT = "isstaticnat"; public static final String ITERATIONS = "iterations"; + public static final String ITEMS = "items"; public static final String SORT_BY = "sortby"; public static final String CHANGE_CIDR = "changecidr"; + public static final String HSM_PROFILE = "hsmprofile"; + public static final String HSM_PROFILE_ID = "hsmprofileid"; public static final String PURPOSE = "purpose"; + public static final String KMS_KEY = "kmskey"; + public static final String KMS_KEY_ID = "kmskeyid"; + public static final String KMS_KEY_VERSION = "kmskeyversion"; + public static final String KEK_LABEL = "keklabel"; + public static final String KEY_BITS = "keybits"; public static final String IS_TAGGED = "istagged"; public static final String INSTANCE_NAME = "instancename"; public static final String CONSIDER_LAST_HOST = "considerlasthost"; @@ -982,6 +1021,7 @@ public class ApiConstants { public static final String REGION_ID = "regionid"; public static final String VPC_OFF_ID = "vpcofferingid"; public static final String VPC_OFF_NAME = "vpcofferingname"; + public static final String VPC_OFFERING_CONSERVE_MODE = "vpcofferingconservemode"; public static final String NETWORK = "network"; public static final String VPC_ID = "vpcid"; public static final String VPC_NAME = "vpcname"; @@ -1028,7 +1068,7 @@ public class ApiConstants { public static final String NSX_PROVIDER_PORT = "nsxproviderport"; public static final String NSX_CONTROLLER_ID = "nsxcontrollerid"; public static final String S3_ACCESS_KEY = "accesskey"; - public static final String S3_SECRET_KEY = "secretkey"; + public static final String SECRET_KEY = "secretkey"; public static final String S3_END_POINT = "endpoint"; public static final String S3_BUCKET_NAME = "bucket"; public static final String S3_SIGNER = "s3signer"; @@ -1171,6 +1211,7 @@ public class ApiConstants { public static final String CLEAN_UP_EXTRA_CONFIG = "cleanupextraconfig"; public static final String CLEAN_UP_PARAMETERS = "cleanupparameters"; public static final String VIRTUAL_SIZE = "virtualsize"; + public static final String UNCOMPRESSED_SIZE = "uncompressedsize"; public static final String NETSCALER_CONTROLCENTER_ID = "netscalercontrolcenterid"; public static final String NETSCALER_SERVICEPACKAGE_ID = "netscalerservicepackageid"; public static final String FETCH_ROUTER_HEALTH_CHECK_RESULTS = "fetchhealthcheckresults"; @@ -1240,6 +1281,13 @@ public class ApiConstants { public static final String MAX_SIZE = "maxsize"; public static final String NODE_TYPE_OFFERING_MAP = "nodeofferings"; public static final String NODE_TYPE_TEMPLATE_MAP = "nodetemplates"; + public static final String NODE_TYPE_AFFINITY_GROUP_MAP = "nodeaffinitygroups"; + public static final String CONTROL_AFFINITY_GROUP_IDS = "controlaffinitygroupids"; + public static final String CONTROL_AFFINITY_GROUP_NAMES = "controlaffinitygroupnames"; + public static final String WORKER_AFFINITY_GROUP_IDS = "workeraffinitygroupids"; + public static final String WORKER_AFFINITY_GROUP_NAMES = "workeraffinitygroupnames"; + public static final String ETCD_AFFINITY_GROUP_IDS = "etcdaffinitygroupids"; + public static final String ETCD_AFFINITY_GROUP_NAMES = "etcdaffinitygroupnames"; public static final String BOOT_TYPE = "boottype"; public static final String BOOT_MODE = "bootmode"; @@ -1261,6 +1309,7 @@ public class ApiConstants { public static final String PROVIDER_FOR_2FA = "providerfor2fa"; public static final String ISSUER_FOR_2FA = "issuerfor2fa"; public static final String MANDATE_2FA = "mandate2fa"; + public static final String PASSWORD_CHANGE_REQUIRED = "passwordchangerequired"; public static final String SECRET_CODE = "secretcode"; public static final String LOGIN = "login"; public static final String LOGOUT = "logout"; @@ -1283,6 +1332,8 @@ public class ApiConstants { public static final String OBJECT_LOCKING = "objectlocking"; public static final String ENCRYPTION = "encryption"; public static final String QUOTA = "quota"; + public static final String QUOTA_CONSUMED = "quotaconsumed"; + public static final String QUOTA_USAGE = "quotausage"; public static final String ACCESS_KEY = "accesskey"; public static final String SOURCE_NAT_IP = "sourcenatipaddress"; @@ -1297,7 +1348,7 @@ public class ApiConstants { public static final String IMPORT_SOURCE = "importsource"; public static final String TEMP_PATH = "temppath"; public static final String HEURISTIC_RULE = "heuristicrule"; - public static final String HEURISTIC_TYPE_VALID_OPTIONS = "Valid options are: ISO, SNAPSHOT, TEMPLATE and VOLUME."; + public static final String HEURISTIC_TYPE_VALID_OPTIONS = "Valid options are: ISO, SNAPSHOT, BACKUP, TEMPLATE and VOLUME."; public static final String MANAGEMENT = "management"; public static final String IS_VNF = "isvnf"; public static final String VNF_NICS = "vnfnics"; @@ -1307,8 +1358,10 @@ public class ApiConstants { public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement"; public static final String VNF_CIDR_LIST = "vnfcidrlist"; + public static final String AUTHORIZE_URL = "authorizeurl"; public static final String CLIENT_ID = "clientid"; public static final String REDIRECT_URI = "redirecturi"; + public static final String TOKEN_URL = "tokenurl"; public static final String IS_TAG_A_RULE = "istagarule"; @@ -1333,6 +1386,45 @@ public class ApiConstants { public static final String OBJECT_STORAGE_LIMIT = "objectstoragelimit"; public static final String OBJECT_STORAGE_TOTAL = "objectstoragetotal"; + public static final String KEEP_MAC_ADDRESS_ON_PUBLIC_NIC = "keepmacaddressonpublicnic"; + + public static final String PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC = + "Indicates whether to use the same MAC address for the public NIC of VRs on the same network. If \"true\", when creating redundant routers or recreating" + + " a VR, CloudStack will use the same MAC address for the public NIC of all VRs. Otherwise, if \"false\", new public NICs will always have " + + " a new MAC address."; + + // DNS provider related + public static final String NAME_SERVERS = "nameservers"; + public static final String DNS_USER_NAME = "dnsusername"; + public static final String DNS_API_KEY = "dnsapikey"; + public static final String DNS_ZONE_ID = "dnszoneid"; + public static final String DNS_ZONE = "dnszone"; + public static final String DNS_RECORD = "dnsrecord"; + public static final String DNS_SUB_DOMAIN = "dnssubdomain"; + public static final String DNS_SERVER_ID = "dnsserverid"; + public static final String CONTENT = "content"; + public static final String CONTENTS = "contents"; + public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix"; + public static final String AUTHORITATIVE = "authoritative"; + public static final String KIND = "kind"; + public static final String DNS_SEC = "dnssec"; + public static final String TTL = "ttl"; + public static final String CHANGE_TYPE = "changetype"; + public static final String RECORDS = "records"; + public static final String RR_SETS = "rrsets"; + public static final String X_API_KEY = "X-API-Key"; + public static final String DISABLED = "disabled"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String NATIVE_ZONE = "Native"; + public static final String NIC_DNS_NAME = "nicdnsname"; + public static final String TIME_STAMP = "timestamp"; + public static final String INSTANCE_ID = "instanceId"; + public static final String OLD_STATE = "oldState"; + public static final String NEW_STATE = "newState"; + public static final String OLD_HOST_NAME = "oldHostName"; + public static final String EXISTING = "existing"; + public static final String UNMANAGE = "unmanage"; + public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + "a boolean or a numeric value: if it results in a boolean value, the tariff value will be applied according to the result; if it results in a numeric value, the " + "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " + @@ -1352,9 +1444,14 @@ public class ApiConstants { public static final String VMWARE_DC = "vmwaredc"; + public static final String PARAMETER_DESCRIPTION_ISOLATED_BACKUPS = "Whether the backup will be isolated, defaults to false. " + + "Isolated backups are always created as full backups in independent chains. Therefore, they will never depend on any existing backup chain " + + "and no backup chain will depend on them. Currently only supported for the KBOSS provider."; + public static final String CSS = "css"; public static final String JSON_CONFIGURATION = "jsonconfiguration"; + public static final String LOGIN_BASE_DOMAIN = "loginbasedomain"; public static final String COMMON_NAMES = "commonnames"; @@ -1372,6 +1469,27 @@ public class ApiConstants { public static final String OBSOLETE_PARAMETERS = "obsoleteparameters"; public static final String EXCLUDED_PARAMETERS = "excludedparameters"; + public static final String COMPRESS = "compress"; + + public static final String VALIDATE = "validate"; + + public static final String VALIDATION_STEPS = "validationsteps"; + + public static final String ALLOW_QUICK_RESTORE = "allowquickrestore"; + + public static final String ALLOW_EXTRACT_FILE = "allowextractfile"; + + public static final String BACKUP_CHAIN_SIZE = "backupchainsize"; + + public static final String COMPRESSION_LIBRARY = "compressionlibrary"; + public static final String ATTEMPTS = "attempts"; + + public static final String EXECUTING = "executing"; + + public static final String SCHEDULED = "scheduled"; + public static final String SCHEDULED_DATE = "scheduleddate"; + public static final String BACKUP_PROVIDER = "backupprovider"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). @@ -1413,7 +1531,7 @@ public String toString() { } public enum HostDetails { - all, capacity, events, stats, min; + all, capacity, core, events, stats, min; } public enum VMDetails { diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java index cb75939d6bc5..1ee41ac86c22 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java @@ -21,8 +21,11 @@ import javax.servlet.http.HttpSession; +import org.apache.cloudstack.context.CallContext; + import com.cloud.domain.Domain; import com.cloud.exception.CloudAuthenticationException; +import com.cloud.user.Account; import com.cloud.user.UserAccount; public interface ApiServerService { @@ -49,5 +52,23 @@ public ResponseObject loginUser(HttpSession session, String username, String pas boolean resetPassword(UserAccount userAccount, String token, String password); + String getDomainId(Map params); + boolean isPostRequestsAndTimestampsEnforced(); + + AsyncCmdResult processAsyncCmd(BaseAsyncCmd cmdObj, Map params, CallContext ctx, Long callerUserId, Account caller) throws Exception; + + class AsyncCmdResult { + public final Long objectId; + public final String objectUuid; + public final BaseAsyncCmd asyncCmd; + public final long jobId; + + public AsyncCmdResult(Long objectId, String objectUuid, BaseAsyncCmd asyncCmd, long jobId) { + this.objectId = objectId; + this.objectUuid = objectUuid; + this.asyncCmd = asyncCmd; + this.jobId = jobId; + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java index 6859b0a7f406..c67c5a023e09 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCmd.java @@ -29,6 +29,7 @@ public abstract class BaseAsyncCmd extends BaseCmd { public static final String migrationSyncObject = "migration"; public static final String snapshotHostSyncObject = "snapshothost"; public static final String gslbSyncObject = "globalserverloadbalancer"; + public static final String user = "user"; private Object job; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java index 30baa71d6d85..b9b13dcfd884 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java @@ -20,7 +20,7 @@ public abstract class BaseAsyncCreateCustomIdCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.CUSTOM_ID, type = CommandType.STRING, description = "An optional field, in case you want to set a custom id to the resource. Allowed to Root Admins only") - private String customId; + protected String customId; public String getCustomId() { return customId; diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index a4de301cc991..483fa83630ad 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -35,10 +35,12 @@ import org.apache.cloudstack.acl.ProjectRoleService; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.gpu.GpuService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; @@ -220,6 +222,8 @@ public static enum CommandType { @Inject public Ipv6Service ipv6Service; @Inject + public ApiKeyPairService apiKeyPairService; + @Inject public VnfTemplateManager vnfTemplateManager; @Inject public BucketApiService _bucketService; @@ -229,6 +233,9 @@ public static enum CommandType { @Inject public RoutedIpv4Manager routedIpv4Manager; + @Inject + public DnsProviderManager dnsProviderManager; + public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException; @@ -498,4 +505,8 @@ public Map convertExternalDetailsToMap(Map externalDetails) { } return details; } + + public String getResourceUuid(String parameterName) { + return CallContext.current().getApiResourceUuid(parameterName); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java index 6da9db57ee30..94c5d8ff39fc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java @@ -153,8 +153,8 @@ public Map getDetails() { return (Map) (paramsCollection.toArray())[0]; } - public boolean isCleanupDetails(){ - return cleanupDetails == null ? false : cleanupDetails.booleanValue(); + public boolean isCleanupDetails() { + return cleanupDetails != null && cleanupDetails; } public CPU.CPUArch getCPUArch() { diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 8e92e877f5ca..6e880c89432f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -24,6 +24,8 @@ import org.apache.cloudstack.api.response.ConsoleSessionResponse; import org.apache.cloudstack.consoleproxy.ConsoleSession; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ApiConstants.HostDetails; @@ -41,6 +43,7 @@ import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.api.response.BackupRepositoryResponse; import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.CapacityResponse; import org.apache.cloudstack.api.response.ClusterResponse; @@ -77,6 +80,7 @@ import org.apache.cloudstack.api.response.IpForwardingRuleResponse; import org.apache.cloudstack.api.response.IpQuarantineResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -277,7 +281,8 @@ public interface ResponseGenerator { List createUserVmResponse(ResponseView view, String objectName, UserVm... userVms); - List createUserVmResponse(ResponseView view, String objectName, EnumSet details, UserVm... userVms); + List createUserVmResponse(ResponseView view, String objectName, EnumSet details, + UserVm... userVms); SystemVmResponse createSystemVmResponse(VirtualMachine systemVM); @@ -303,11 +308,13 @@ public interface ResponseGenerator { LoadBalancerResponse createLoadBalancerResponse(LoadBalancer loadBalancer); - LBStickinessResponse createLBStickinessPolicyResponse(List stickinessPolicies, LoadBalancer lb); + LBStickinessResponse createLBStickinessPolicyResponse(List stickinessPolicies, + LoadBalancer lb); LBStickinessResponse createLBStickinessPolicyResponse(StickinessPolicy stickinessPolicy, LoadBalancer lb); - LBHealthCheckResponse createLBHealthCheckPolicyResponse(List healthcheckPolicies, LoadBalancer lb); + LBHealthCheckResponse createLBHealthCheckPolicyResponse(List healthcheckPolicies, + LoadBalancer lb); LBHealthCheckResponse createLBHealthCheckPolicyResponse(HealthCheckPolicy healthcheckPolicy, LoadBalancer lb); @@ -315,7 +322,8 @@ public interface ResponseGenerator { PodResponse createMinimalPodResponse(Pod pod); - ZoneResponse createZoneResponse(ResponseView view, DataCenter dataCenter, Boolean showCapacities, Boolean showResourceIcon); + ZoneResponse createZoneResponse(ResponseView view, DataCenter dataCenter, Boolean showCapacities, + Boolean showResourceIcon); DataCenterGuestIpv6PrefixResponse createDataCenterGuestIpv6PrefixResponse(DataCenterGuestIpv6Prefix prefix); @@ -339,6 +347,8 @@ public interface ResponseGenerator { UserVm findUserVmById(Long vmId); + UserVm findUserVmByNicId(Long nicId); + Volume findVolumeById(Long volumeId); Account findAccountByNameDomain(String accountName, Long domainId); @@ -355,7 +365,8 @@ public interface ResponseGenerator { List createTemplateResponses(ResponseView view, long templateId, Long zoneId, boolean readyOnly); - List createTemplateResponses(ResponseView view, long templateId, Long snapshotId, Long volumeId, boolean readyOnly); + List createTemplateResponses(ResponseView view, long templateId, Long snapshotId, Long volumeId, + boolean readyOnly); SecurityGroupResponse createSecurityGroupResponseFromSecurityGroupRule(List securityRules); @@ -374,14 +385,15 @@ public interface ResponseGenerator { TemplateResponse createTemplateUpdateResponse(ResponseView view, VirtualMachineTemplate result); List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, - Long zoneId, boolean readyOnly); + Long zoneId, boolean readyOnly); List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, - List zoneIds, boolean readyOnly); + List zoneIds, boolean readyOnly); List createCapacityResponse(List result, DecimalFormat format); - TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseView view, List accountNames, Long id); + TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseView view, List accountNames, + Long id); AsyncJobResponse queryJobResult(QueryAsyncJobResultCmd cmd); @@ -395,7 +407,8 @@ List createTemplateResponses(ResponseView view, VirtualMachine Long getSecurityGroupId(String groupName, long accountId); - List createIsoResponses(ResponseView view, VirtualMachineTemplate iso, Long zoneId, boolean readyOnly); + List createIsoResponses(ResponseView view, VirtualMachineTemplate iso, Long zoneId, + boolean readyOnly); ProjectResponse createProjectResponse(Project project); @@ -496,13 +509,15 @@ List createTemplateResponses(ResponseView view, VirtualMachine GuestOsMappingResponse createGuestOSMappingResponse(GuestOSHypervisor osHypervisor); - HypervisorGuestOsNamesResponse createHypervisorGuestOSNamesResponse(List> hypervisorGuestOsNames); + HypervisorGuestOsNamesResponse createHypervisorGuestOSNamesResponse( + List> hypervisorGuestOsNames); SnapshotScheduleResponse createSnapshotScheduleResponse(SnapshotSchedule sched); UsageRecordResponse createUsageResponse(Usage usageRecord); - UsageRecordResponse createUsageResponse(Usage usageRecord, Map> resourceTagResponseMap, boolean oldFormat); + UsageRecordResponse createUsageResponse(Usage usageRecord, + Map> resourceTagResponseMap, boolean oldFormat); public Map> getUsageResourceTags(); @@ -514,7 +529,8 @@ List createTemplateResponses(ResponseView view, VirtualMachine public NicResponse createNicResponse(Nic result); - ApplicationLoadBalancerResponse createLoadBalancerContainerReponse(ApplicationLoadBalancerRule lb, Map lbInstances); + ApplicationLoadBalancerResponse createLoadBalancerContainerReponse(ApplicationLoadBalancerRule lb, + Map lbInstances); AffinityGroupResponse createAffinityGroupResponse(AffinityGroup group); @@ -540,9 +556,12 @@ List createTemplateResponses(ResponseView view, VirtualMachine ManagementServerResponse createManagementResponse(ManagementServerHost mgmt); - List createHealthCheckResponse(VirtualMachine router, List healthCheckResults); + List createHealthCheckResponse(VirtualMachine router, + List healthCheckResults); - RollingMaintenanceResponse createRollingMaintenanceResponse(Boolean success, String details, List hostsUpdated, List hostsSkipped); + RollingMaintenanceResponse createRollingMaintenanceResponse(Boolean success, String details, + List hostsUpdated, + List hostsSkipped); ResourceIconResponse createResourceIconResponse(ResourceIcon resourceIcon); @@ -552,11 +571,14 @@ List createTemplateResponses(ResponseView view, VirtualMachine DirectDownloadCertificateResponse createDirectDownloadCertificateResponse(DirectDownloadCertificate certificate); - List createDirectDownloadCertificateHostMapResponse(List hostMappings); + List createDirectDownloadCertificateHostMapResponse( + List hostMappings); - DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse(DirectDownloadManager.HostCertificateStatus status); + DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse( + DirectDownloadManager.HostCertificateStatus status); - DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, Long hostId, Pair result); + DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, + Long hostId, Pair result); FirewallResponse createIpv6FirewallRuleResponse(FirewallRule acl); @@ -583,4 +605,8 @@ List createTemplateResponses(ResponseView view, VirtualMachine GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin); ConsoleSessionResponse createConsoleSessionResponse(ConsoleSession consoleSession, ResponseView responseView); + + ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair); + + ListResponse createKeypairPermissionsResponse(List permissions); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java index ea25c56ee39e..cc154ed964b3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java @@ -177,7 +177,7 @@ public long getEntityOwnerId() { @Override public void execute() { validateParams(); - CallContext.current().setEventDetails("Account Name: " + getUsername() + ", Domain Id:" + getDomainId()); + CallContext.current().setEventDetails("Account Name: " + getUsername() + ", Domain ID:" + getResourceUuid(ApiConstants.DOMAIN_ID)); UserAccount userAccount = _accountService.createUserAccount(this); if (userAccount != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/DisableAccountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/DisableAccountCmd.java index 29774e254aa0..f7f8bd974272 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/DisableAccountCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/DisableAccountCmd.java @@ -108,12 +108,20 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Disabling Account: " + getAccountName() + " in domain: " + getDomainId(); + String message = "Disabling Account "; + + if (getId() != null) { + message += "with ID: " + getResourceUuid(ApiConstants.ID); + } else { + message += getAccountName() + " in Domain: " + getResourceUuid(ApiConstants.DOMAIN_ID); + } + + return message; } @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException { - CallContext.current().setEventDetails("Account Name: " + getAccountName() + ", Domain Id:" + getDomainId()); + CallContext.current().setEventDetails("Account Name: " + getAccountName() + ", Domain Id:" + getResourceUuid(ApiConstants.DOMAIN_ID)); Account result = _regionService.disableAccount(this); if (result != null){ AccountResponse response = _responseGenerator.createAccountResponse(ResponseView.Full, result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java index 232c4760e1e6..13405431f63e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/CreateRolePermissionCmd.java @@ -81,7 +81,7 @@ public void execute() { if (role == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role id provided"); } - CallContext.current().setEventDetails("Role id: " + role.getId() + ", rule:" + getRule() + ", permission: " + getPermission() + ", description: " + getDescription()); + CallContext.current().setEventDetails("Role ID: " + role.getUuid() + ", rule:" + getRule() + ", permission: " + getPermission() + ", description: " + getDescription()); final RolePermission rolePermission = roleService.createRolePermission(role, getRule(), getPermission(), getDescription()); if (rolePermission == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create role permission"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java index fd2d11aeda0a..80ec08260ab2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRoleCmd.java @@ -70,7 +70,7 @@ public void execute() { if (role == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Cannot find the role with provided id"); } - CallContext.current().setEventDetails("Role id: " + role.getId()); + CallContext.current().setEventDetails("Role ID: " + role.getUuid()); boolean result = roleService.deleteRole(role); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRolePermissionCmd.java index bedaca9e23af..cf4a62bf6c43 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DeleteRolePermissionCmd.java @@ -68,7 +68,7 @@ public void execute() { if (rolePermission == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission id provided"); } - CallContext.current().setEventDetails("Role permission id: " + rolePermission.getId()); + CallContext.current().setEventDetails("Role permission ID: " + rolePermission.getUuid()); boolean result = roleService.deleteRolePermission(rolePermission); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DisableRoleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DisableRoleCmd.java index 80cb92c8362f..2c5659b2bc4b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DisableRoleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/DisableRoleCmd.java @@ -55,7 +55,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE if (role == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Cannot find the role with provided id"); } - CallContext.current().setEventDetails("Role id: " + role.getId()); + CallContext.current().setEventDetails("Role ID: " + role.getUuid()); boolean result = roleService.disableRole(role); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/EnableRoleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/EnableRoleCmd.java index c4a6505d52f6..05dfbe1270fa 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/EnableRoleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/EnableRoleCmd.java @@ -55,7 +55,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE if (role == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Cannot find the role with provided id"); } - CallContext.current().setEventDetails("Role id: " + role.getId()); + CallContext.current().setEventDetails("Role ID: " + role.getUuid()); boolean result = roleService.enableRole(role); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/UpdateRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/UpdateRolePermissionCmd.java index 8f8115e9957e..992564413f6b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/UpdateRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/UpdateRolePermissionCmd.java @@ -111,7 +111,7 @@ public void execute() { if (getRuleId() != null || getRulePermission() != null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Parameters permission and ruleid must be mutually exclusive with ruleorder"); } - CallContext.current().setEventDetails("Reordering permissions for role id: " + role.getId()); + CallContext.current().setEventDetails("Reordering permissions for role with ID: " + role.getUuid()); final List rolePermissionsOrder = new ArrayList<>(); for (Long rolePermissionId : getRulePermissionOrder()) { final RolePermission rolePermission = roleService.findRolePermission(rolePermissionId); @@ -129,7 +129,7 @@ public void execute() { if (rolePermission == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid rule id provided"); } - CallContext.current().setEventDetails("Updating permission for rule id: " + getRuleId() + " to: " + getRulePermission().toString()); + CallContext.current().setEventDetails("Updating permission for rule with ID: " + getResourceUuid(ApiConstants.RULE_ID) + " to: " + getRulePermission().toString()); result = roleService.updateRolePermission(role, rolePermission, getRulePermission()); } SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/CreateProjectRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/CreateProjectRolePermissionCmd.java index d39c2312aa91..e085c10cee0b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/CreateProjectRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/CreateProjectRolePermissionCmd.java @@ -72,7 +72,7 @@ public void execute() { if (projectRole == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid project role ID provided"); } - CallContext.current().setEventDetails("Project Role ID: " + projectRole.getId() + ", Rule:" + getRule() + ", Permission: " + getPermission() + ", Description: " + getDescription()); + CallContext.current().setEventDetails("Project Role ID: " + projectRole.getUuid() + ", Rule:" + getRule() + ", Permission: " + getPermission() + ", Description: " + getDescription()); final ProjectRolePermission projectRolePermission = projRoleService.createProjectRolePermission(this); if (projectRolePermission == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create project role permission"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRoleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRoleCmd.java index 9f8d82489584..84f73e7a1a32 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRoleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRoleCmd.java @@ -69,7 +69,7 @@ public void execute() { if (role == null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Cannot find project role with provided id"); } - CallContext.current().setEventDetails("Deleting Project Role with id: " + role.getId()); + CallContext.current().setEventDetails("Deleting Project Role with ID: " + role.getUuid()); boolean result = projRoleService.deleteProjectRole(role, getProjectId()); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRolePermissionCmd.java index ac68278535e2..d7941a6a4cc3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/DeleteProjectRolePermissionCmd.java @@ -70,7 +70,7 @@ public void execute() { if (rolePermission == null || rolePermission.getProjectId() != getProjectId()) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid role permission id provided for the project"); } - CallContext.current().setEventDetails("Deleting Project Role permission with id: " + rolePermission.getId()); + CallContext.current().setEventDetails("Deleting Project Role permission with ID: " + rolePermission.getUuid()); boolean result = projRoleService.deleteProjectRolePermission(rolePermission); SuccessResponse response = new SuccessResponse(); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/UpdateProjectRolePermissionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/UpdateProjectRolePermissionCmd.java index b273b9f58493..fd0c043f2321 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/UpdateProjectRolePermissionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/acl/project/UpdateProjectRolePermissionCmd.java @@ -115,7 +115,7 @@ public void execute() { if (getProjectRuleId() != null || getProjectRolePermission() != null) { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Parameters permission and ruleid must be mutually exclusive with ruleorder"); } - CallContext.current().setEventDetails("Reordering permissions for role id: " + projectRole.getId()); + CallContext.current().setEventDetails("Reordering permissions for role with ID: " + projectRole.getUuid()); result = updateProjectRolePermissionOrder(projectRole); } else if (getProjectRuleId() != null || getProjectRolePermission() != null ) { @@ -123,7 +123,7 @@ public void execute() { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Parameters permission and ruleid must be mutually exclusive with ruleorder"); } ProjectRolePermission rolePermission = getValidProjectRolePermission(); - CallContext.current().setEventDetails("Updating project role permission for rule id: " + getProjectRuleId() + " to: " + getProjectRolePermission().toString()); + CallContext.current().setEventDetails("Updating project role permission for rule ID: " + getProjectRuleId() + " to: " + getProjectRolePermission().toString()); result = projRoleService.updateProjectRolePermission(projectId, projectRole, rolePermission, getProjectRolePermission()); } SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/CreateCounterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/CreateCounterCmd.java index 0798357b8bcb..d7be56bf3f46 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/CreateCounterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/CreateCounterCmd.java @@ -97,7 +97,7 @@ public void create() { @Override public void execute() { - CallContext.current().setEventDetails("Counter ID: " + getEntityId()); + CallContext.current().setEventDetails("Counter ID: " + getEntityUuid()); Counter ctr = _autoScaleService.getCounter(getEntityId()); CounterResponse response = _responseGenerator.createCounterResponse(ctr); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/DeleteCounterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/DeleteCounterCmd.java index 769f6fd8b142..8e941965e84b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/DeleteCounterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/autoscale/DeleteCounterCmd.java @@ -91,6 +91,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting a counter."; + return "Deleting auto scaling counter with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java new file mode 100644 index 000000000000..500a77f3d4fc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +import java.util.Arrays; +import java.util.List; +import java.util.function.LongFunction; + +@APICommand(name = "cloneBackupOffering", + description = "Clones a backup offering from an existing offering", + responseObject = BackupOfferingResponse.class, since = "4.23.0", + authorized = {RoleType.Admin}) +public class CloneBackupOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver { + + @Inject + protected BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + //////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, type = BaseCmd.CommandType.UUID, entityType = BackupOfferingResponse.class, + required = true, description = "The ID of the source backup offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, required = true, + description = "The name of the cloned offering") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = BaseCmd.CommandType.STRING, required = false, + description = "The description of the cloned offering") + private String description; + + @Parameter(name = ApiConstants.EXTERNAL_ID, type = BaseCmd.CommandType.STRING, required = false, + description = "The backup offering ID (from backup provider side)") + private String externalId; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "The zone ID", required = false) + private Long zoneId; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.STRING, + description = "the ID of the containing domain(s) as comma separated string, public for public offerings", + length = 4096) + private String domainIds; + + @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = BaseCmd.CommandType.BOOLEAN, + description = "Whether users are allowed to create adhoc backups and backup schedules", required = false) + private Boolean userDrivenBackups; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public String getName() { + return name; + } + + public String getExternalId() { + return externalId; + } + + public Long getZoneId() { + return zoneId; + } + + public String getDescription() { + return description; + } + + public Boolean getUserDrivenBackups() { + return userDrivenBackups; + } + + public List getDomainIds() { + if (domainIds != null && !domainIds.isEmpty()) { + return Arrays.asList(Arrays.stream(domainIds.split(",")).map(domainId -> Long.parseLong(domainId.trim())).toArray(Long[]::new)); + } + LongFunction> defaultDomainsProvider = null; + if (backupManager != null) { + defaultDomainsProvider = backupManager::getBackupOfferingDomains; + } + return resolveDomainIds(domainIds, sourceOfferingId, defaultDomainsProvider, "backup offering"); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + BackupOffering policy = backupManager.cloneBackupOffering(this); + if (policy == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone backup offering"); + } + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(policy); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_OFFERING_CLONE; + } + + @Override + public String getEventDescription() { + return "Cloning backup offering: " + name + " from source offering: " + (sourceOfferingId == null ? "" : sourceOfferingId.toString()); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java new file mode 100644 index 000000000000..c98bfb850529 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java @@ -0,0 +1,100 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createImageTransfer", + description = "Create image transfer for a disk in backup. This API is intended for testing only and is disabled by default.", + responseObject = ImageTransferResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class CreateImageTransferCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.BACKUP_ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + description = "ID of the backup") + private Long backupId; + + @Parameter(name = ApiConstants.VOLUME_ID, + type = CommandType.UUID, + entityType = VolumeResponse.class, + required = true, + description = "ID of the disk/volume") + private Long volumeId; + + @Parameter(name = ApiConstants.DIRECTION, + type = CommandType.STRING, + required = true, + description = "Direction of the transfer: upload, download") + private String direction; + + @Parameter(name = ApiConstants.FORMAT, + type = CommandType.STRING, + description = "Format for the image transfer: raw/cow. 'raw' will create an NBD backend. 'cow' will use the File backend." + + "For download, only the 'raw' format is supported. Default: raw") + private String format; + + public Long getBackupId() { + return backupId; + } + + public Long getVolumeId() { + return volumeId; + } + + public ImageTransfer.Direction getDirection() { + return ImageTransfer.Direction.valueOf(direction); + } + + public ImageTransfer.Format getFormat() { + return EnumUtils.getEnum(ImageTransfer.Format.class, format); + } + + @Override + public void execute() { + ImageTransferResponse response = kvmBackupExportService.createImageTransfer(this); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java new file mode 100644 index 000000000000..d0e17e86d427 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java @@ -0,0 +1,85 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "deleteVirtualMachineCheckpoint", + description = "Delete a VM checkpoint. This API is intended for testing only and is disabled by default.", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class DeleteVmCheckpointCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = "checkpointid", + type = CommandType.STRING, + required = true, + description = "Checkpoint ID") + private String checkpointId; + + public Long getVmId() { + return vmId; + } + + public String getCheckpointId() { + return checkpointId; + } + + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public void setCheckpointId(String checkpointId) { + this.checkpointId = checkpointId; + } + + @Override + public void execute() { + boolean result = kvmBackupExportService.deleteVmCheckpoint(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java new file mode 100644 index 000000000000..45173f8668ee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java @@ -0,0 +1,103 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "finalizeBackup", + description = "Finalize a VM backup session. This API is intended for testing only and is disabled by default.", + responseObject = BackupResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class FinalizeBackupCmd extends BaseAsyncCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Inject + private BackupManager backupManager; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + required = true, + description = "ID of the backup") + private Long backupId; + + public Long getVmId() { + return vmId; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public void execute() { + Backup backup = kvmBackupExportService.finalizeBackup(this); + + if (backup == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Backup"); + } + + BackupResponse response = backupManager.createBackupResponse(backup, null); + + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CREATE; + } + + @Override + public String getEventDescription() { + return "Finalizing backup " + backupId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java new file mode 100644 index 000000000000..dfc43e233bf2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "finalizeImageTransfer", + description = "Finalize an image transfer. This API is intended for testing only and is disabled by default.", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class FinalizeImageTransferCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ImageTransferResponse.class, + required = true, + description = "ID of the image transfer") + private Long imageTransferId; + + public Long getImageTransferId() { + return imageTransferId; + } + + @Override + public void execute() { + boolean result = kvmBackupExportService.finalizeImageTransfer(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java index 5e702585a2c3..4cf27c561508 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java @@ -54,7 +54,7 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { @Inject - private BackupManager backupManager; + protected BackupManager backupManager; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -86,7 +86,8 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { type = CommandType.LIST, collectionType = CommandType.UUID, entityType = DomainResponse.class, - description = "the ID of the containing domain(s), null for public offerings") + description = "the ID of the containing domain(s), null for public offerings", + since = "4.23.0") private List domainIds; ///////////////////////////////////////////////////// @@ -156,6 +157,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Importing backup offering: " + name + " (external ID: " + externalId + ") on zone ID " + zoneId ; + return "Importing backup offering: " + name + " (external ID: " + externalId + ") on zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID) ; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java new file mode 100644 index 000000000000..d810d21ab5f8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java @@ -0,0 +1,81 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "listImageTransfers", + description = "List image transfers for a backup. This API is intended for testing only and is disabled by default.", + responseObject = ImageTransferResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class ListImageTransfersCmd extends BaseListCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ImageTransferResponse.class, + description = "ID of the Image Transfer") + private Long id; + + @Parameter(name = ApiConstants.BACKUP_ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + description = "ID of the backup") + private Long backupId; + + public Long getId() { + return id; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public void execute() { + List responses = kvmBackupExportService.listImageTransfers(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java new file mode 100644 index 000000000000..a61661e982de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.CheckpointResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.KVMBackupExportService; + +@APICommand(name = "listVirtualMachineCheckpoints", + description = "List checkpoints for a VM. This API is intended for testing only and is disabled by default.", + responseObject = CheckpointResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class ListVmCheckpointsCmd extends BaseListCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + public Long getVmId() { + return vmId; + } + + @Override + public void execute() { + List responses = kvmBackupExportService.listVmCheckpoints(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return 0; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java new file mode 100644 index 000000000000..1bf6d45db049 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java @@ -0,0 +1,120 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "startBackup", + description = "Start a VM backup session using pull mode backup-begin on the KVM host. This API is intended for testing only and is disabled by default.", + responseObject = BackupResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) + public class StartBackupCmd extends BaseAsyncCreateCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Inject + private BackupManager backupManager; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + description = "the name of the backup") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, + type = CommandType.STRING, + description = "the description for the backup") + private String description; + + public Long getVmId() { + return vmId; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + @Override + public void execute() { + try { + Backup backup = kvmBackupExportService.startBackup(this); + BackupResponse response = backupManager.createBackupResponse(backup, null); + + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public void create() { + Backup backup = kvmBackupExportService.createBackup(this); + + if (backup != null) { + setEntityId(backup.getId()); + setEntityUuid(backup.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Backup"); + } + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CREATE; + } + + @Override + public String getEventDescription() { + return "Starting backup for Instance " + vmId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java index 463af000f58b..79dad4269c9b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java @@ -149,6 +149,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "issuing certificate for domain(s)=" + domains + ", ip(s)=" + addresses; + return "Issuing certificate for domain(s)=" + domains + ", ip(s)=" + addresses; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java index af24e1f10c89..d333a74fdb3b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java @@ -63,6 +63,12 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd { description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") private String provider; + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, + description = "When true, uses SSH to re-provision the agent's certificate, bypassing the NIO agent connection. " + + "Use this when agents are disconnected due to a CA change. Supported for KVM hosts and SystemVMs. Default is false", + since = "4.23.0") + private Boolean forced; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -79,6 +85,10 @@ public String getProvider() { return provider; } + public boolean isForced() { + return forced != null && forced; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -90,7 +100,7 @@ public void execute() { throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId()); } - boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider()); + boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider(), isForced()); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); setResponseObject(response); @@ -108,7 +118,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Provisioning certificate for host id=" + hostId + " using provider=" + provider; + return "Provisioning certificate for host with ID: " + getResourceUuid(ApiConstants.HOST_ID) + " using provider: " + provider; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java index 60f2c2b1deea..00e7da6e37c1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ExecuteClusterDrsPlanCmd.java @@ -142,6 +142,6 @@ public String getEventType() { @Override public String getEventDescription() { - return String.format("Executing DRS plan for cluster: %d", getId()); + return "Executing DRS plan for cluster with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java index f6f66415f533..055443934609 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java @@ -19,23 +19,23 @@ import java.util.ArrayList; import java.util.List; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.commons.lang3.StringUtils; - import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.commons.lang3.StringUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.Pair; @@ -94,6 +94,13 @@ public class ListCfgsByCmd extends BaseListCmd { description = "The ID of the Image Store to update the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "Lists configuration by group name (primarily used for UI)", since = "4.18.0") private String groupName; @@ -139,6 +146,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + public String getGroupName() { return groupName; } @@ -200,6 +211,9 @@ private void setScope(ConfigurationResponse cfgResponse) { if (getImageStoreId() != null){ cfgResponse.setScope("imagestore"); } + if (getManagementServerId() != null){ + cfgResponse.setScope("managementserver"); + } } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java index 2d511cff34db..3c3c36b29d7e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java @@ -23,16 +23,16 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.ImageStoreResponse; -import org.apache.cloudstack.framework.config.ConfigKey; - import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -84,6 +84,13 @@ public class ResetCfgCmd extends BaseCmd { description = "The ID of the Image Store to reset the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -116,6 +123,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -149,6 +160,9 @@ public void execute() { if (getImageStoreId() != null) { response.setScope(ConfigKey.Scope.ImageStore.name()); } + if (getManagementServerId() != null) { + response.setScope(ConfigKey.Scope.ManagementServer.name()); + } response.setValue(cfg.second()); this.setResponseObject(response); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java index 97dee8f638af..9db9529dc8d8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java @@ -16,9 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.admin.config; -import com.cloud.utils.crypt.DBEncryptionUtil; import org.apache.cloudstack.acl.RoleService; -import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; @@ -29,13 +27,17 @@ import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.lang3.StringUtils; import com.cloud.user.Account; +import com.cloud.utils.crypt.DBEncryptionUtil; @APICommand(name = "updateConfiguration", description = "Updates a configuration.", responseObject = ConfigurationResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -88,6 +90,13 @@ public class UpdateCfgCmd extends BaseCmd { validations = ApiArgValidator.PositiveNumber) private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -112,7 +121,7 @@ public Long getClusterId() { return clusterId; } - public Long getStoragepoolId() { + public Long getStoragePoolId() { return storagePoolId; } @@ -128,6 +137,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -182,7 +195,7 @@ public ConfigurationResponse setResponseScopes(ConfigurationResponse response) { if (getClusterId() != null) { response.setScope("cluster"); } - if (getStoragepoolId() != null) { + if (getStoragePoolId() != null) { response.setScope("storagepool"); } if (getAccountId() != null) { @@ -191,6 +204,9 @@ public ConfigurationResponse setResponseScopes(ConfigurationResponse response) { if (getDomainId() != null) { response.setScope("domain"); } + if (getManagementServerId() != null) { + response.setScope(ConfigKey.Scope.ManagementServer.name().toLowerCase()); + } return response; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java index 6a59788715ee..c140de5aa01e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java @@ -140,7 +140,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Getting diagnostics data files from System VM: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Getting diagnostics data files from System Instance with ID: " + getResourceUuid(ApiConstants.TARGET_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java index 577d86146fdd..d1f22baf6604 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/RunDiagnosticsCmd.java @@ -153,7 +153,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public String getEventDescription() { - return "Executing diagnostics on System VM: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Executing diagnostics on System Instance with ID: " + getResourceUuid(ApiConstants.TARGET_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/CreateDomainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/CreateDomainCmd.java index a20f69c90f58..d2775548a841 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/CreateDomainCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/CreateDomainCmd.java @@ -86,7 +86,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Domain Name: " + getDomainName() + ((getParentDomainId() != null) ? ", Parent DomainId :" + getParentDomainId() : "")); + CallContext.current().setEventDetails("Domain Name: " + getDomainName() + ((getParentDomainId() != null) ? ", Parent Domain ID:" + getResourceUuid(ApiConstants.PARENT_DOMAIN_ID) : "")); Domain domain = _domainService.createDomain(getDomainName(), getParentDomainId(), getNetworkDomain(), getDomainUUID()); if (domain != null) { DomainResponse response = _responseGenerator.createDomainResponse(domain); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/DeleteDomainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/DeleteDomainCmd.java index 6adb457f4f83..cf02e6a56bf8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/DeleteDomainCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/DeleteDomainCmd.java @@ -88,12 +88,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting domain: " + getId(); + return "Deleting domain with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Domain Id: " + getId()); + CallContext.current().setEventDetails("Domain ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _regionService.deleteDomain(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/UpdateDomainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/UpdateDomainCmd.java index adce521627fb..124a84931548 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/UpdateDomainCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/UpdateDomainCmd.java @@ -82,7 +82,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Domain Id: " + getId()); + CallContext.current().setEventDetails("Domain ID: " + getResourceUuid(ApiConstants.ID)); Domain domain = _regionService.updateDomain(this); if (domain != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java index 2ac07a9fb3a0..83aca1a2eb64 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java @@ -46,7 +46,7 @@ public class DiscoverGpuDevicesCmd extends BaseListCmd { @Override public void execute() { - CallContext.current().setEventDetails("Discovering GPU Devices on host id: " + getId()); + CallContext.current().setEventDetails("Discovering GPU Devices on host with ID: " + getResourceUuid(ApiConstants.ID)); ListResponse response = gpuService.discoverGpuDevices(this); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/AddGuestOsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/AddGuestOsCmd.java index 1868d0412a18..c0e995c497d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/AddGuestOsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/AddGuestOsCmd.java @@ -120,7 +120,7 @@ public void create() { @Override public void execute() { - CallContext.current().setEventDetails("Guest OS Id: " + getEntityId()); + CallContext.current().setEventDetails("Guest OS ID: " + getEntityUuid()); GuestOS guestOs = _mgr.getAddedGuestOs(getEntityId()); if (guestOs != null) { GuestOSResponse response = _responseGenerator.createGuestOSResponse(guestOs); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsCmd.java index d38682ce5bb4..f5c7d965c13f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsCmd.java @@ -62,7 +62,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Guest OS Id: " + id); + CallContext.current().setEventDetails("Guest OS ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _mgr.removeGuestOs(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -74,7 +74,7 @@ public void execute() { @Override public String getEventDescription() { - return "Removing Guest OS: " + getId(); + return "Removing Guest OS with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsMappingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsMappingCmd.java index a472ab672c55..bd4a53889f25 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsMappingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/RemoveGuestOsMappingCmd.java @@ -62,7 +62,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Guest OS Mapping Id: " + id); + CallContext.current().setEventDetails("Guest OS Mapping ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _mgr.removeGuestOsMapping(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -74,7 +74,7 @@ public void execute() { @Override public String getEventDescription() { - return "Removing Guest OS Mapping: " + getId(); + return "Removing Guest OS Mapping with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsCmd.java index 59909e09854a..035ff6a19e24 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsCmd.java @@ -123,7 +123,7 @@ public void execute() { @Override public String getEventDescription() { - return "Updating guest OS: " + getId(); + return "Updating guest OS with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsMappingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsMappingCmd.java index fc67ef0a7e76..161bb5323070 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsMappingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/guest/UpdateGuestOsMappingCmd.java @@ -86,7 +86,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Updating Guest OS Mapping: " + getId(); + return "Updating Guest OS with ID: " + getResourceUuid(ApiConstants.ID) + " mapping."; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/ConfigureHAForHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/ConfigureHAForHostCmd.java index d7707e197d64..cb427e659495 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/ConfigureHAForHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/ConfigureHAForHostCmd.java @@ -87,6 +87,7 @@ private void setupResponse(final boolean result, final String resourceUuid) { final HostHAResponse response = new HostHAResponse(); response.setId(resourceUuid); response.setProvider(getHaProvider().toLowerCase()); + response.setStatus(result); response.setResponseName(getCommandName()); setResponseObject(response); } @@ -102,7 +103,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE if (!result) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to configure HA provider for the host"); } - CallContext.current().setEventDetails("Host Id:" + host.getId() + " HA configured with provider: " + getHaProvider()); + CallContext.current().setEventDetails("Host ID:" + host.getUuid() + " HA configured with provider: " + getHaProvider()); CallContext.current().putContextParameter(Host.class, host.getUuid()); setupResponse(result, host.getUuid()); @@ -115,6 +116,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Configure HA for host: " + getHostId(); + return "Configuring HA for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForClusterCmd.java index 51554b7607dc..63c657a9e454 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForClusterCmd.java @@ -89,7 +89,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find cluster by ID: " + getClusterId()); } final boolean result = haConfigManager.disableHA(cluster); - CallContext.current().setEventDetails("Cluster Id:" + cluster.getId() + " HA enabled: false"); + CallContext.current().setEventDetails("Cluster ID:" + cluster.getUuid() + " HA enabled: false"); CallContext.current().putContextParameter(Cluster.class, cluster.getUuid()); setupResponse(result); @@ -102,7 +102,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Disable HA for cluster: " + getClusterId(); + return "Disabling HA for cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForHostCmd.java index ad9c64145322..b90f731ff565 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForHostCmd.java @@ -91,7 +91,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final boolean result = haConfigManager.disableHA(host.getId(), HAResource.ResourceType.Host); - CallContext.current().setEventDetails("Host Id:" + host.getId() + " HA enabled: false"); + CallContext.current().setEventDetails("Host ID:" + host.getUuid() + " HA enabled: false"); CallContext.current().putContextParameter(Host.class, host.getUuid()); setupResponse(result, host.getUuid()); @@ -104,6 +104,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Disable HA for host: " + getHostId(); + return "Disabling HA for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForZoneCmd.java index 1f0758459b5d..07a6fbd2b399 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/DisableHAForZoneCmd.java @@ -90,7 +90,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final boolean result = haConfigManager.disableHA(dataCenter); - CallContext.current().setEventDetails("Zone Id:" + dataCenter.getId() + " HA enabled: false"); + CallContext.current().setEventDetails("Zone ID:" + dataCenter.getUuid() + " HA enabled: false"); CallContext.current().putContextParameter(DataCenter.class, dataCenter.getUuid()); setupResponse(result); @@ -103,7 +103,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Disable HA for zone: " + getZoneId(); + return "Disabling HA for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForClusterCmd.java index 3bb7a4c3070d..635fba988c60 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForClusterCmd.java @@ -90,7 +90,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final boolean result = haConfigManager.enableHA(cluster); - CallContext.current().setEventDetails("Cluster Id:" + cluster.getId() + " HA enabled: true"); + CallContext.current().setEventDetails("Cluster ID:" + cluster.getUuid() + " HA enabled: true"); CallContext.current().putContextParameter(Cluster.class, cluster.getUuid()); setupResponse(result); @@ -103,6 +103,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Enable HA for cluster: " + getClusterId(); + return "Enabling HA for cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForHostCmd.java index f54767225432..0bda19a7ad3c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForHostCmd.java @@ -91,7 +91,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final boolean result = haConfigManager.enableHA(host.getId(), HAResource.ResourceType.Host); - CallContext.current().setEventDetails("Host Id:" + host.getId() + " HA enabled: true"); + CallContext.current().setEventDetails("Host ID:" + host.getUuid() + " HA enabled: true"); CallContext.current().putContextParameter(Host.class, host.getUuid()); setupResponse(result, host.getUuid()); @@ -104,6 +104,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Enable HA for host: " + getHostId(); + return "Enabling HA for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForZoneCmd.java index 99607315c543..f6d0f62bb120 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/ha/EnableHAForZoneCmd.java @@ -90,7 +90,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final boolean result = haConfigManager.enableHA(dataCenter); - CallContext.current().setEventDetails("Zone Id:" + dataCenter.getId() + " HA enabled: true"); + CallContext.current().setEventDetails("Zone ID:" + dataCenter.getUuid() + " HA enabled: true"); CallContext.current().putContextParameter(DataCenter.class, dataCenter.getUuid()); setupResponse(result); @@ -103,7 +103,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Enable HA for zone: " + getZoneId(); + return "Enabling HA for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java index a698a62254e6..5a1758345609 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java @@ -60,7 +60,8 @@ public class AddHostCmd extends BaseCmd { @Parameter(name = ApiConstants.POD_ID, type = CommandType.UUID, entityType = PodResponse.class, required = true, description = "The Pod ID for the host") private Long podId; - @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL") + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL, optionally add ssh port (format: 'host:port') for KVM hosts," + + " otherwise falls back to the port defined at the config 'kvm.host.discovery.ssh.port'") private String url; @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, description = "The Zone ID for the host") diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostAsDegradedCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostAsDegradedCmd.java index f68da1edcd17..56930d47b2ec 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostAsDegradedCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostAsDegradedCmd.java @@ -78,7 +78,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "declaring host: " + getId() + " as Degraded"; + return "Removing host with ID: " + getResourceUuid(ApiConstants.ID) + " from Degraded state."; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostMaintenanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostMaintenanceCmd.java index 111172200b9a..5d44bafb4b5c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostMaintenanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/CancelHostMaintenanceCmd.java @@ -76,7 +76,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Canceling maintenance for host: " + getId(); + return "Canceling maintenance for host with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/DeclareHostAsDegradedCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/DeclareHostAsDegradedCmd.java index 209d8b65fbab..1dd65a583706 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/DeclareHostAsDegradedCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/DeclareHostAsDegradedCmd.java @@ -78,7 +78,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "declaring host: " + getId() + " as Degraded"; + return "Declaring host with ID: " + getResourceUuid(ApiConstants.ID) + " as Degraded."; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java index abca619f82a7..4d6ef7419616 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java @@ -78,7 +78,7 @@ public void execute() { for (Host host : result.first()) { HostForMigrationResponse hostResponse = _responseGenerator.createHostForMigrationResponse(host); Boolean suitableForMigration = false; - if (hostsWithCapacity.contains(host)) { + if (hostsWithCapacity != null && hostsWithCapacity.contains(host)) { suitableForMigration = true; } hostResponse.setSuitableForMigration(suitableForMigration); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java index e202dfad77ba..8f5e6c784d6e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java @@ -252,7 +252,7 @@ protected ListResponse getHostResponses() { for (Host host : result.first()) { HostResponse hostResponse = _responseGenerator.createHostResponse(host, getDetails()); Boolean suitableForMigration = false; - if (hostsWithCapacity.contains(host)) { + if (hostsWithCapacity != null && hostsWithCapacity.contains(host)) { suitableForMigration = true; } hostResponse.setSuitableForMigration(suitableForMigration); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/PrepareForHostMaintenanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/PrepareForHostMaintenanceCmd.java index b76f500359a3..843c7fd7fcbe 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/PrepareForHostMaintenanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/PrepareForHostMaintenanceCmd.java @@ -76,7 +76,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "preparing host: " + getId() + " for maintenance"; + return "Preparing host with ID: " + getResourceUuid(ApiConstants.ID) + " for maintenance."; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReconnectHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReconnectHostCmd.java index 178a96cedbd6..b9892ed6033c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReconnectHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReconnectHostCmd.java @@ -77,7 +77,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "reconnecting host: " + getId(); + return "Reconnecting host with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReleaseHostReservationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReleaseHostReservationCmd.java index d7905421a8f3..bddb5b13e452 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReleaseHostReservationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/ReleaseHostReservationCmd.java @@ -72,7 +72,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "releasing reservation for host: " + getId(); + return "Releasing reservation from host with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index c085abd42c76..69bca7c79e21 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -49,9 +49,14 @@ public class UpdateHostCmd extends BaseCmd { @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, - description = "The ID of OS category to update the host with") + description = "the ID of OS category used to prioritize VMs with matching OS category during the allocation process. " + + "It cannot be used alongside the 'guestosrule' parameter.") private Long osCategoryId; + @Parameter(name = ApiConstants.GUEST_OS_RULE, type = CommandType.STRING, description = "the guest OS rule written in JavaScript to match with the OS of the VM." + + "It cannot be used alongside the 'oscategoryid' parameter.") + private String guestOsRule; + @Parameter(name = ApiConstants.ALLOCATION_STATE, type = CommandType.STRING, description = "Change resource state of host, valid values are [Enable, Disable]. Operation may failed if host in states not allowing Enable/Disable") @@ -96,6 +101,10 @@ public Long getOsCategoryId() { return osCategoryId; } + public String getGuestOsRule() { + return guestOsRule; + } + public String getAllocationState() { return allocationState; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/ConfigureInternalLoadBalancerElementCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/ConfigureInternalLoadBalancerElementCmd.java index 9bb28523ecad..51aa86546603 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/ConfigureInternalLoadBalancerElementCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/ConfigureInternalLoadBalancerElementCmd.java @@ -84,12 +84,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "configuring internal load balancer element: " + id; + return "Configuring internal load balancer element with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Internal load balancer element: " + id); + CallContext.current().setEventDetails("Internal load balancer element: " + getResourceUuid(ApiConstants.ID)); InternalLoadBalancerElementService service = _networkService.getInternalLoadBalancerElementById(id); VirtualRouterProvider result = service.configureInternalLoadBalancerElement(getId(), getEnabled()); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/CreateInternalLoadBalancerElementCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/CreateInternalLoadBalancerElementCmd.java index 474bbc831e5c..aa9e5f1ba7f4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/CreateInternalLoadBalancerElementCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/CreateInternalLoadBalancerElementCmd.java @@ -74,7 +74,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Virtual router element Id: " + getEntityId()); + CallContext.current().setEventDetails("Virtual router element ID: " + getEntityUuid()); InternalLoadBalancerElementService service = _networkService.getInternalLoadBalancerElementByNetworkServiceProviderId(getNspId()); VirtualRouterProvider result = service.getInternalLoadBalancerElement(getEntityId()); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StartInternalLBVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StartInternalLBVMCmd.java index b5aa3c8d9b07..d9d4e46726fc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StartInternalLBVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StartInternalLBVMCmd.java @@ -88,7 +88,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "starting Internal LB Instance: " + getId(); + return "Starting internal LB Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -103,7 +103,7 @@ public Long getApiResourceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Internal LB Instance ID: " + getId()); + CallContext.current().setEventDetails("Internal LB Instance ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = null; VirtualRouter router = _routerService.findRouter(getId()); if (router == null || router.getRole() != Role.INTERNAL_LB_VM) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StopInternalLBVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StopInternalLBVMCmd.java index 82eddb27c7dd..253c59e671e5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StopInternalLBVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/internallb/StopInternalLBVMCmd.java @@ -86,7 +86,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Stopping Internal LB Instance: " + getId(); + return "Stopping Internal LB Instance: " + getResourceUuid(ApiConstants.ID); } @Override @@ -105,7 +105,7 @@ public boolean isForced() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException { - CallContext.current().setEventDetails("Internal LB Instance Id: " + getId()); + CallContext.current().setEventDetails("Internal LB Instance ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = null; VirtualRouter vm = _routerService.findRouter(getId()); if (vm == null || vm.getRole() != Role.INTERNAL_LB_VM) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java new file mode 100644 index 000000000000..358f7f749f6e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java @@ -0,0 +1,131 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.kms; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.KMSKey; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = "migrateVolumesToKMS", + description = "Migrates encrypted volumes to KMS", + responseObject = AsyncJobResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class MigrateVolumesToKMSCmd extends BaseAsyncCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ACCOUNT, + type = CommandType.STRING, + description = "Migrate volumes for specific account") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.UUID, + entityType = DomainResponse.class, + description = "Domain ID") + private Long domainId; + + @Parameter(name = ApiConstants.VOLUME_IDS, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = VolumeResponse.class, + required = true, + description = "List of volume IDs to migrate") + private List volumeIds; + + @Parameter(name = ApiConstants.KMS_KEY_ID, + required = true, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "KMS Key ID to use for migrating volumes") + private Long kmsKeyId; + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public List getVolumeIds() { + return volumeIds; + } + + public Long getKmsKeyId() { + return kmsKeyId; + } + + @Override + public void execute() { + try { + kmsManager.migrateVolumesToKMS(this); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to migrate volumes to KMS: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + KMSKey key = _entityMgr.findById(KMSKey.class, kmsKeyId); + if (key != null) { + return key.getAccountId(); + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return com.cloud.event.EventTypes.EVENT_VOLUME_MIGRATE_TO_KMS; + } + + @Override + public String getEventDescription() { + return "Migrating volumes to KMS key: " + _uuidMgr.getUuid(KMSKey.class, kmsKeyId); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KmsKey; + } + + @Override + public Long getApiResourceId() { + return kmsKeyId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AddNetworkServiceProviderCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AddNetworkServiceProviderCmd.java index a0013f9d6e2b..3e42a0103d8b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AddNetworkServiceProviderCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/AddNetworkServiceProviderCmd.java @@ -101,7 +101,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Network ServiceProvider Id: " + getEntityId()); + CallContext.current().setEventDetails("Network ServiceProvider ID: " + getEntityUuid()); PhysicalNetworkServiceProvider result = _networkService.getCreatedPhysicalNetworkServiceProvider(getEntityId()); if (result != null) { ProviderResponse response = _responseGenerator.createNetworkServiceProviderResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java new file mode 100644 index 000000000000..19760ffaaa10 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmd.java @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.network; + +import java.util.List; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkOfferingResponse; + +import com.cloud.offering.NetworkOffering; + +@APICommand(name = "cloneNetworkOffering", + description = "Clones a network offering. All parameters are copied from the source offering unless explicitly overridden. " + + "Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.", + responseObject = NetworkOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneNetworkOfferingCmd extends NetworkOfferingBaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = NetworkOfferingResponse.class, + required = true, + description = "The ID of the source network offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = "addservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to add to the cloned offering (in addition to source offering services). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List addServices; + + @Parameter(name = "dropservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to remove from the cloned offering (that exist in source offering). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List dropServices; + + @Parameter(name = ApiConstants.TRAFFIC_TYPE, + type = CommandType.STRING, + description = "The traffic type for the network offering. Supported type in current release is GUEST only") + private String traffictype; + + @Parameter(name = ApiConstants.GUEST_IP_TYPE, type = CommandType.STRING, description = "Guest type of the network offering: Shared or Isolated") + private String guestIptype; + + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public List getAddServices() { + return addServices; + } + + public List getDropServices() { + return dropServices; + } + + public String getGuestIpType() { + return guestIptype; + } + + public String getTraffictype() { + return traffictype; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + NetworkOffering result = _configService.cloneNetworkOffering(this); + if (result != null) { + NetworkOfferingResponse response = _responseGenerator.createNetworkOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone network offering"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateGuestNetworkIpv6PrefixCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateGuestNetworkIpv6PrefixCmd.java index f6b035c57837..614dcf9d0751 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateGuestNetworkIpv6PrefixCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateGuestNetworkIpv6PrefixCmd.java @@ -83,7 +83,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating guest IPv6 prefix " + getPrefix() + " for zone=" + getZoneId(); + return "Creating guest IPv6 prefix " + getPrefix() + " for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmd.java index a482cb1d4f27..4d645376a909 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmd.java @@ -85,7 +85,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating guest IPv4 subnet " + getSubnet() + " in zone subnet=" + getParentId(); + return "Creating guest IPv4 subnet " + getSubnet() + " in zone subnet: " + getResourceUuid(ApiConstants.PARENT_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmd.java index 5f48cf9c6327..48a6002fb5c0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmd.java @@ -102,7 +102,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating guest IPv4 subnet " + getSubnet() + " for zone=" + getZoneId(); + return "Creating guest IPv4 subnet " + getSubnet() + " for zone: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateManagementNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateManagementNetworkIpRangeCmd.java index a7826e022a68..2780c4eaf050 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateManagementNetworkIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateManagementNetworkIpRangeCmd.java @@ -132,7 +132,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating management ip range from " + getStartIp() + " to " + getEndIp() + " and gateway=" + getGateWay() + ", netmask=" + getNetmask() + " of pod=" + getPodId(); + return "Creating management IP range from " + getStartIp() + " to " + getEndIp() + ", with gateway: " + getGateWay() + ", netmask:" + getNetmask() + " on pod:" + getPodId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java index a0559f57dab0..5c39060f9fa3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java @@ -16,505 +16,47 @@ // under the License. package org.apache.cloudstack.api.command.admin.network; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import com.cloud.network.Network; -import com.cloud.network.VirtualRouterProvider; -import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.cloudstack.api.response.ZoneResponse; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; - import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.NetworkOfferingResponse; -import org.apache.cloudstack.api.response.ServiceOfferingResponse; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.network.Network.Capability; -import com.cloud.network.Network.Service; import com.cloud.offering.NetworkOffering; -import com.cloud.offering.NetworkOffering.Availability; -import com.cloud.user.Account; - -import static com.cloud.network.Network.Service.Dhcp; -import static com.cloud.network.Network.Service.Dns; -import static com.cloud.network.Network.Service.Lb; -import static com.cloud.network.Network.Service.StaticNat; -import static com.cloud.network.Network.Service.SourceNat; -import static com.cloud.network.Network.Service.PortForwarding; -import static com.cloud.network.Network.Service.NetworkACL; -import static com.cloud.network.Network.Service.UserData; -import static com.cloud.network.Network.Service.Firewall; - -import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; -import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; -import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; @APICommand(name = "createNetworkOffering", description = "Creates a network offering.", responseObject = NetworkOfferingResponse.class, since = "3.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class CreateNetworkOfferingCmd extends BaseCmd { +public class CreateNetworkOfferingCmd extends NetworkOfferingBaseCmd { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the network offering") - private String networkOfferingName; - - @Parameter(name = ApiConstants.DISPLAY_TEXT, type = CommandType.STRING, description = "The display text of the network offering, defaults to the value of 'name'.") - private String displayText; - @Parameter(name = ApiConstants.TRAFFIC_TYPE, type = CommandType.STRING, required = true, description = "The traffic type for the network offering. Supported type in current release is GUEST only") private String traffictype; - @Parameter(name = ApiConstants.TAGS, type = CommandType.STRING, description = "The tags for the network offering.", length = 4096) - private String tags; - - @Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "True if network offering supports VLANs") - private Boolean specifyVlan; - - @Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "The availability of network offering. The default value is Optional. " - + " Another value is Required, which will make it as the default network offering for new networks ") - private String availability; - - @Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "Data transfer rate in megabits per second allowed") - private Integer networkRate; - - @Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, description = "True if the network offering is IP conserve mode enabled") - private Boolean conserveMode; - - @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, - type = CommandType.UUID, - entityType = ServiceOfferingResponse.class, - description = "The service offering ID used by virtual router provider") - private Long serviceOfferingId; - @Parameter(name = ApiConstants.GUEST_IP_TYPE, type = CommandType.STRING, required = true, description = "Guest type of the network offering: Shared or Isolated") private String guestIptype; - @Parameter(name = ApiConstants.INTERNET_PROTOCOL, - type = CommandType.STRING, - description = "The internet protocol of network offering. Options are IPv4 and dualstack. Default is IPv4. dualstack will create a network offering that supports both IPv4 and IPv6", - since = "4.17.0") - private String internetProtocol; - - @Parameter(name = ApiConstants.SUPPORTED_SERVICES, - type = CommandType.LIST, - collectionType = CommandType.STRING, - description = "Services supported by the network offering") - private List supportedServices; - - @Parameter(name = ApiConstants.SERVICE_PROVIDER_LIST, - type = CommandType.MAP, - description = "Provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network") - private Map serviceProviderList; - - @Parameter(name = ApiConstants.SERVICE_CAPABILITY_LIST, type = CommandType.MAP, description = "Desired service capabilities as part of network offering") - private Map serviceCapabilitystList; - - @Parameter(name = ApiConstants.SPECIFY_IP_RANGES, - type = CommandType.BOOLEAN, - description = "True if network offering supports specifying ip ranges; defaulted to false if not specified") - private Boolean specifyIpRanges; - - @Parameter(name = ApiConstants.IS_PERSISTENT, - type = CommandType.BOOLEAN, - description = "True if network offering supports persistent networks; defaulted to false if not specified") - private Boolean isPersistent; - - @Parameter(name = ApiConstants.FOR_VPC, - type = CommandType.BOOLEAN, - description = "True if network offering is meant to be used for VPC, false otherwise.") - private Boolean forVpc; - - @Deprecated - @Parameter(name = ApiConstants.FOR_NSX, - type = CommandType.BOOLEAN, - description = "true if network offering is meant to be used for NSX, false otherwise.", - since = "4.20.0") - private Boolean forNsx; - - @Parameter(name = ApiConstants.PROVIDER, - type = CommandType.STRING, - description = "Name of the provider providing the service", - since = "4.21.0") - private String provider; - - @Parameter(name = ApiConstants.NSX_SUPPORT_LB, - type = CommandType.BOOLEAN, - description = "True if network offering for NSX network offering supports Load balancer service.", - since = "4.20.0") - private Boolean nsxSupportsLbService; - - @Parameter(name = ApiConstants.NSX_SUPPORTS_INTERNAL_LB, - type = CommandType.BOOLEAN, - description = "True if network offering for NSX network offering supports Internal Load balancer service.", - since = "4.20.0") - private Boolean nsxSupportsInternalLbService; - - @Parameter(name = ApiConstants.NETWORK_MODE, - type = CommandType.STRING, - description = "Indicates the mode with which the network will operate. Valid option: NATTED or ROUTED", - since = "4.20.0") - private String networkMode; - - @Parameter(name = ApiConstants.FOR_TUNGSTEN, - type = CommandType.BOOLEAN, - description = "True if network offering is meant to be used for Tungsten-Fabric, false otherwise.") - private Boolean forTungsten; - - @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.2.0", description = "Network offering details in key/value pairs." - + " Supported keys are internallbprovider/publiclbprovider with service provider as a value, and" - + " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup") - protected Map details; - - @Parameter(name = ApiConstants.EGRESS_DEFAULT_POLICY, - type = CommandType.BOOLEAN, - description = "True if guest network default egress policy is allow; false if default egress policy is deny") - private Boolean egressDefaultPolicy; - - @Parameter(name = ApiConstants.KEEPALIVE_ENABLED, - type = CommandType.BOOLEAN, - required = false, - description = "If true keepalive will be turned on in the loadbalancer. At the time of writing this has only an effect on haproxy; the mode http and httpclose options are unset in the haproxy conf file.") - private Boolean keepAliveEnabled; - - @Parameter(name = ApiConstants.MAX_CONNECTIONS, - type = CommandType.INTEGER, - description = "Maximum number of concurrent connections supported by the Network offering") - private Integer maxConnections; - - @Parameter(name = ApiConstants.DOMAIN_ID, - type = CommandType.LIST, - collectionType = CommandType.UUID, - entityType = DomainResponse.class, - description = "The ID of the containing domain(s), null for public offerings") - private List domainIds; - - @Parameter(name = ApiConstants.ZONE_ID, - type = CommandType.LIST, - collectionType = CommandType.UUID, - entityType = ZoneResponse.class, - description = "The ID of the containing zone(s), null for public offerings", - since = "4.13") - private List zoneIds; - - @Parameter(name = ApiConstants.ENABLE, - type = CommandType.BOOLEAN, - description = "Set to true if the offering is to be enabled during creation. Default is false", - since = "4.16") - private Boolean enable; - - @Parameter(name = ApiConstants.SPECIFY_AS_NUMBER, type = CommandType.BOOLEAN, since = "4.20.0", - description = "true if network offering supports choosing AS number") - private Boolean specifyAsNumber; - - @Parameter(name = ApiConstants.ROUTING_MODE, - type = CommandType.STRING, - since = "4.20.0", - description = "the routing mode for the network offering. Supported types are: Static or Dynamic.") - private String routingMode; - ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// - public String getNetworkOfferingName() { - return networkOfferingName; - } - - public String getDisplayText() { - return StringUtils.isEmpty(displayText) ? networkOfferingName : displayText; - } - - public String getTags() { - return tags; - } - public String getTraffictype() { return traffictype; } - public Boolean getSpecifyVlan() { - return specifyVlan == null ? false : specifyVlan; - } - - public String getAvailability() { - return availability == null ? Availability.Optional.toString() : availability; - } - - public Integer getNetworkRate() { - return networkRate; - } - - public Long getServiceOfferingId() { - return serviceOfferingId; - } - - public boolean isExternalNetworkProvider() { - return Arrays.asList("NSX", "Netris").stream() - .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); - } - - public boolean isForNsx() { - return provider != null && provider.equalsIgnoreCase("NSX"); - } - - public boolean isForNetris() { - return provider != null && provider.equalsIgnoreCase("Netris"); - } - - public String getProvider() { - return provider; - } - - public List getSupportedServices() { - if (!isExternalNetworkProvider()) { - return supportedServices == null ? new ArrayList() : supportedServices; - } else { - List services = new ArrayList<>(List.of( - Dhcp.getName(), - Dns.getName(), - UserData.getName() - )); - if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { - services.addAll(Arrays.asList( - StaticNat.getName(), - SourceNat.getName(), - PortForwarding.getName())); - } - if (getNsxSupportsLbService() || (provider != null && isNetrisNatted(getProvider(), getNetworkMode()))) { - services.add(Lb.getName()); - } - if (Boolean.TRUE.equals(forVpc)) { - services.add(NetworkACL.getName()); - } else { - services.add(Firewall.getName()); - } - return services; - } - } - public String getGuestIpType() { return guestIptype; } - public String getInternetProtocol() { - return internetProtocol; - } - - public Boolean getSpecifyIpRanges() { - return specifyIpRanges == null ? false : specifyIpRanges; - } - - public Boolean getConserveMode() { - if (conserveMode == null) { - return true; - } - return conserveMode; - } - - public Boolean getIsPersistent() { - return isPersistent == null ? false : isPersistent; - } - - public Boolean getForVpc() { - return forVpc; - } - - public String getNetworkMode() { - return networkMode; - } - - public boolean getNsxSupportsLbService() { - return BooleanUtils.isTrue(nsxSupportsLbService); - } - - public boolean getNsxSupportsInternalLbService() { - return BooleanUtils.isTrue(nsxSupportsInternalLbService); - } - - public Boolean getForTungsten() { - return forTungsten; - } - - public Boolean getEgressDefaultPolicy() { - if (egressDefaultPolicy == null) { - return true; - } - return egressDefaultPolicy; - } - - public Boolean getKeepAliveEnabled() { - return keepAliveEnabled; - } - - public Integer getMaxconnections() { - return maxConnections; - } - - public Map> getServiceProviders() { - Map> serviceProviderMap = new HashMap<>(); - if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { - Collection servicesCollection = serviceProviderList.values(); - Iterator iter = servicesCollection.iterator(); - while (iter.hasNext()) { - HashMap services = (HashMap) iter.next(); - String service = services.get("service"); - String provider = services.get("provider"); - List providerList = null; - if (serviceProviderMap.containsKey(service)) { - providerList = serviceProviderMap.get(service); - } else { - providerList = new ArrayList(); - } - providerList.add(provider); - serviceProviderMap.put(service, providerList); - } - } else if (isExternalNetworkProvider()) { - getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); - } - return serviceProviderMap; - } - - private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { - String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() : - VirtualRouterProvider.Type.VirtualRouter.name(); - List unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService")); - List routerSupported = List.of("Dhcp", "Dns", "UserData"); - List allServices = Service.listAllServices().stream().map(Service::getName).collect(Collectors.toList()); - if (routerProvider.equals(VirtualRouterProvider.Type.VPCVirtualRouter.name())) { - unsupportedServices.add("Firewall"); - } else { - unsupportedServices.add("NetworkACL"); - } - for (String service : allServices) { - if (unsupportedServices.contains(service)) - continue; - if (routerSupported.contains(service)) - serviceProviderMap.put(service, List.of(routerProvider)); - else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || NetworkACL.getName().equalsIgnoreCase(service)) { - serviceProviderMap.put(service, List.of(provider)); - } - if (isNsxWithoutLb(getProvider(), getNsxSupportsLbService()) || isNetrisRouted(getProvider(), getNetworkMode())) { - serviceProviderMap.remove(Lb.getName()); - } - } - } - - public Map getServiceCapabilities(Service service) { - Map capabilityMap = null; - - if (serviceCapabilitystList != null && !serviceCapabilitystList.isEmpty()) { - capabilityMap = new HashMap(); - Collection serviceCapabilityCollection = serviceCapabilitystList.values(); - Iterator iter = serviceCapabilityCollection.iterator(); - while (iter.hasNext()) { - HashMap svcCapabilityMap = (HashMap) iter.next(); - Capability capability = null; - String svc = svcCapabilityMap.get("service"); - String capabilityName = svcCapabilityMap.get("capabilitytype"); - String capabilityValue = svcCapabilityMap.get("capabilityvalue"); - - if (capabilityName != null) { - capability = Capability.getCapability(capabilityName); - } - - if ((capability == null) || (capabilityName == null) || (capabilityValue == null)) { - throw new InvalidParameterValueException("Invalid capability:" + capabilityName + " capability value:" + capabilityValue); - } - - if (svc.equalsIgnoreCase(service.getName())) { - capabilityMap.put(capability, capabilityValue); - } else { - //throw new InvalidParameterValueException("Service is not equal ") - } - } - } - - return capabilityMap; - } - - public Map getDetails() { - if (details == null || details.isEmpty()) { - return null; - } - - Collection paramsCollection = details.values(); - Object objlist[] = paramsCollection.toArray(); - Map params = (Map) (objlist[0]); - for (int i = 1; i < objlist.length; i++) { - params.putAll((Map) (objlist[i])); - } - - return params; - } - - public String getServicePackageId() { - Map data = getDetails(); - if (data == null) - return null; - return data.get(NetworkOffering.Detail.servicepackageuuid + ""); - } - - public List getDomainIds() { - if (CollectionUtils.isNotEmpty(domainIds)) { - Set set = new LinkedHashSet<>(domainIds); - domainIds.clear(); - domainIds.addAll(set); - } - return domainIds; - } - - public List getZoneIds() { - if (CollectionUtils.isNotEmpty(zoneIds)) { - Set set = new LinkedHashSet<>(zoneIds); - zoneIds.clear(); - zoneIds.addAll(set); - } - return zoneIds; - } - - public Boolean getEnable() { - if (enable != null) { - return enable; - } - return false; - } - - public boolean getSpecifyAsNumber() { - return BooleanUtils.toBoolean(specifyAsNumber); - } - - public String getRoutingMode() { - return routingMode; - } - ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// - @Override - public long getEntityOwnerId() { - return Account.ACCOUNT_ID_SYSTEM; - } @Override public void execute() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java index f4ce9483bfbd..097b8a5b5458 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreatePhysicalNetworkCmd.java @@ -75,7 +75,7 @@ public class CreatePhysicalNetworkCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.ISOLATION_METHODS, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "The isolation method for the physical Network[VLAN/L3/GRE]") + description = "The isolation method for the physical Network[VLAN/VXLAN/GRE/STT/BCF_SEGMENT/SSP/ODL/L3VPN/VCS/NSX/NETRIS]") private List isolationMethods; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the physical Network") @@ -148,7 +148,7 @@ public String getEventDescription() { @Override public void execute() { - CallContext.current().setEventDetails("Physical Network ID: " + getEntityId()); + CallContext.current().setEventDetails("Physical Network ID: " + getEntityUuid()); PhysicalNetwork result = _networkService.getCreatedPhysicalNetwork(getEntityId()); if (result != null) { PhysicalNetworkResponse response = _responseGenerator.createPhysicalNetworkResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmd.java index 2df032c559c5..cc76b284e24a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmd.java @@ -82,7 +82,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Dedicating zone IPv4 subnet " + getId(); + return "Dedicating zone's IPv4 subnet with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteGuestNetworkIpv6PrefixCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteGuestNetworkIpv6PrefixCmd.java index e2ada4191a82..405bbb594edb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteGuestNetworkIpv6PrefixCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteGuestNetworkIpv6PrefixCmd.java @@ -63,7 +63,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting guest IPv6 prefix " + getId(); + return "Deleting guest IPv6 prefix with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmd.java index 28a646f9d036..f6b22f79dfc7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmd.java @@ -59,7 +59,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting guest IPv4 subnet " + getId(); + return "Deleting guest IPv4 subnet with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmd.java index 222bc1bad98d..0ff2a9ad70b8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmd.java @@ -59,7 +59,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting zone IPv4 subnet " + getId(); + return "Deleting zone IPv4 subnet with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteManagementNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteManagementNetworkIpRangeCmd.java index 5b50c90b3964..1e69aaa6c440 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteManagementNetworkIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteManagementNetworkIpRangeCmd.java @@ -100,7 +100,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting management ip range from " + getStartIp() + " to " + getEndIp() + " of Pod: " + getPodId(); + return "Deleting management IP range from " + getStartIp() + " to " + getEndIp() + " from Pod: " + getResourceUuid(ApiConstants.POD_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteNetworkServiceProviderCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteNetworkServiceProviderCmd.java index 23d14966c49b..2573e92b9860 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteNetworkServiceProviderCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteNetworkServiceProviderCmd.java @@ -91,7 +91,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting Physical network ServiceProvider: " + getId(); + return "Deleting Physical network ServiceProvider with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeletePhysicalNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeletePhysicalNetworkCmd.java index 70c35716b657..9994e8e391d7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeletePhysicalNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeletePhysicalNetworkCmd.java @@ -65,7 +65,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Physical Network Id: " + id); + CallContext.current().setEventDetails("Physical Network Id: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkService.deletePhysicalNetwork(getId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -77,7 +77,7 @@ public void execute() { @Override public String getEventDescription() { - return "Deleting Physical network: " + getId(); + return "Deleting Physical network with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteStorageNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteStorageNetworkIpRangeCmd.java index 4ffe58332ebf..dcab38561408 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteStorageNetworkIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/DeleteStorageNetworkIpRangeCmd.java @@ -64,7 +64,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting storage ip range " + getId(); + return "Deleting storage IP range with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateNetworkCmd.java index 8ac9c8da691d..ad78bd3b406c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateNetworkCmd.java @@ -115,7 +115,7 @@ public void execute() { @Override public String getEventDescription() { - StringBuilder eventMsg = new StringBuilder("Migrating network: " + getId()); + String description = "Migrating Network with ID: " + getResourceUuid(ApiConstants.NETWORK_ID); if (getNetworkOfferingId() != null) { Network network = _networkService.getNetwork(getId()); if (network == null) { @@ -128,11 +128,11 @@ public String getEventDescription() { throw new InvalidParameterValueException("Network offering id supplied is invalid"); } - eventMsg.append(". Original network offering id: " + oldOff.getUuid() + ", new network offering id: " + newOff.getUuid()); + description += ". Original Network Offering id: " + oldOff.getUuid() + ", new Network Offering id: " + newOff.getUuid(); } } - return eventMsg.toString(); + return description; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateVPCCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateVPCCmd.java index edef1f846ed7..2973fea33c6a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateVPCCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/MigrateVPCCmd.java @@ -120,7 +120,7 @@ public void execute() { } @Override - public String getEventDescription() { return "Migrating VPC : " + getId() + " to new VPC offering (" + vpcOfferingId + ")"; } + public String getEventDescription() { return "Migrating VPC with ID: " + getResourceUuid(ApiConstants.VPC_ID) + " to new VPC offering with ID: " + getResourceUuid(ApiConstants.VPC_OFF_ID);} @Override public String getEventType() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java new file mode 100644 index 000000000000..9b42be137314 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/NetworkOfferingBaseCmd.java @@ -0,0 +1,494 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.network; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.Network; +import com.cloud.network.VirtualRouterProvider; +import com.cloud.offering.NetworkOffering; +import com.cloud.user.Account; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.cloud.network.Network.Service.Dhcp; +import static com.cloud.network.Network.Service.Dns; +import static com.cloud.network.Network.Service.Firewall; +import static com.cloud.network.Network.Service.Lb; +import static com.cloud.network.Network.Service.NetworkACL; +import static com.cloud.network.Network.Service.PortForwarding; +import static com.cloud.network.Network.Service.SourceNat; +import static com.cloud.network.Network.Service.StaticNat; +import static com.cloud.network.Network.Service.UserData; + +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; + +public abstract class NetworkOfferingBaseCmd extends BaseCmd { + + public abstract String getGuestIpType(); + public abstract String getTraffictype(); + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the network offering") + private String networkOfferingName; + + @Parameter(name = ApiConstants.DISPLAY_TEXT, type = CommandType.STRING, description = "The display text of the network offering, defaults to the value of 'name'.") + private String displayText; + + @Parameter(name = ApiConstants.TAGS, type = CommandType.STRING, description = "The tags for the network offering.", length = 4096) + private String tags; + + @Parameter(name = ApiConstants.SPECIFY_VLAN, type = CommandType.BOOLEAN, description = "True if network offering supports VLANs") + private Boolean specifyVlan; + + @Parameter(name = ApiConstants.AVAILABILITY, type = CommandType.STRING, description = "The availability of network offering. The default value is Optional. " + + " Another value is Required, which will make it as the default network offering for new networks ") + private String availability; + + @Parameter(name = ApiConstants.NETWORKRATE, type = CommandType.INTEGER, description = "Data transfer rate in megabits per second allowed") + private Integer networkRate; + + @Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, description = "True if the network offering is IP conserve mode enabled") + private Boolean conserveMode; + + @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, + type = CommandType.UUID, + entityType = ServiceOfferingResponse.class, + description = "The service offering ID used by virtual router provider") + private Long serviceOfferingId; + + @Parameter(name = ApiConstants.INTERNET_PROTOCOL, + type = CommandType.STRING, + description = "The internet protocol of network offering. Options are IPv4 and dualstack. Default is IPv4. dualstack will create a network offering that supports both IPv4 and IPv6", + since = "4.17.0") + private String internetProtocol; + + @Parameter(name = ApiConstants.SUPPORTED_SERVICES, + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services supported by the network offering") + private List supportedServices; + + @Parameter(name = ApiConstants.SERVICE_PROVIDER_LIST, + type = CommandType.MAP, + description = "Provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network") + private Map serviceProviderList; + + @Parameter(name = ApiConstants.SERVICE_CAPABILITY_LIST, type = CommandType.MAP, description = "Desired service capabilities as part of network offering") + private Map serviceCapabilitiesList; + + @Parameter(name = ApiConstants.SPECIFY_IP_RANGES, + type = CommandType.BOOLEAN, + description = "True if network offering supports specifying ip ranges; defaulted to false if not specified") + private Boolean specifyIpRanges; + + @Parameter(name = ApiConstants.IS_PERSISTENT, + type = CommandType.BOOLEAN, + description = "True if network offering supports persistent networks; defaulted to false if not specified") + private Boolean isPersistent; + + @Parameter(name = ApiConstants.FOR_VPC, + type = CommandType.BOOLEAN, + description = "True if network offering is meant to be used for VPC, false otherwise.") + private Boolean forVpc; + + @Deprecated + @Parameter(name = ApiConstants.FOR_NSX, + type = CommandType.BOOLEAN, + description = "true if network offering is meant to be used for NSX, false otherwise.", + since = "4.20.0") + private Boolean forNsx; + + @Parameter(name = ApiConstants.PROVIDER, + type = CommandType.STRING, + description = "Name of the provider providing the service", + since = "4.21.0") + private String provider; + + @Parameter(name = ApiConstants.NSX_SUPPORT_LB, + type = CommandType.BOOLEAN, + description = "True if network offering for NSX network offering supports Load balancer service.", + since = "4.20.0") + private Boolean nsxSupportsLbService; + + @Parameter(name = ApiConstants.NSX_SUPPORTS_INTERNAL_LB, + type = CommandType.BOOLEAN, + description = "True if network offering for NSX network offering supports Internal Load balancer service.", + since = "4.20.0") + private Boolean nsxSupportsInternalLbService; + + @Parameter(name = ApiConstants.NETWORK_MODE, + type = CommandType.STRING, + description = "Indicates the mode with which the network will operate. Valid option: NATTED or ROUTED", + since = "4.20.0") + private String networkMode; + + @Parameter(name = ApiConstants.FOR_TUNGSTEN, + type = CommandType.BOOLEAN, + description = "True if network offering is meant to be used for Tungsten-Fabric, false otherwise.") + private Boolean forTungsten; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.2.0", description = "Network offering details in key/value pairs." + + " Supported keys are internallbprovider/publiclbprovider with service provider as a value, and" + + " promiscuousmode/macaddresschanges/forgedtransmits with true/false as value to accept/reject the security settings if available for a nic/portgroup") + protected Map details; + + @Parameter(name = ApiConstants.EGRESS_DEFAULT_POLICY, + type = CommandType.BOOLEAN, + description = "True if guest network default egress policy is allow; false if default egress policy is deny") + private Boolean egressDefaultPolicy; + + @Parameter(name = ApiConstants.KEEPALIVE_ENABLED, + type = CommandType.BOOLEAN, + required = false, + description = "If true keepalive will be turned on in the loadbalancer. At the time of writing this has only an effect on haproxy; the mode http and httpclose options are unset in the haproxy conf file.") + private Boolean keepAliveEnabled; + + @Parameter(name = ApiConstants.MAX_CONNECTIONS, + type = CommandType.INTEGER, + description = "Maximum number of concurrent connections supported by the Network offering") + private Integer maxConnections; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = DomainResponse.class, + description = "The ID of the containing domain(s), null for public offerings") + private List domainIds; + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = ZoneResponse.class, + description = "The ID of the containing zone(s), null for public offerings", + since = "4.13") + private List zoneIds; + + @Parameter(name = ApiConstants.ENABLE, + type = CommandType.BOOLEAN, + description = "Set to true if the offering is to be enabled during creation. Default is false", + since = "4.16") + private Boolean enable; + + @Parameter(name = ApiConstants.SPECIFY_AS_NUMBER, type = CommandType.BOOLEAN, since = "4.20.0", + description = "true if network offering supports choosing AS number") + private Boolean specifyAsNumber; + + @Parameter(name = ApiConstants.ROUTING_MODE, + type = CommandType.STRING, + since = "4.20.0", + description = "the routing mode for the network offering. Supported types are: Static or Dynamic.") + private String routingMode; + + private Map sourceDetailsMap; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getNetworkOfferingName() { + return networkOfferingName; + } + + public String getDisplayText() { + return StringUtils.isEmpty(displayText) ? networkOfferingName : displayText; + } + + public String getTags() { + return tags; + } + + public Boolean getSpecifyVlan() { + return specifyVlan == null ? false : specifyVlan; + } + + public String getAvailability() { + return availability == null ? NetworkOffering.Availability.Optional.toString() : availability; + } + + public Integer getNetworkRate() { + return networkRate; + } + + public Long getServiceOfferingId() { + return serviceOfferingId; + } + + public boolean isExternalNetworkProvider() { + return Arrays.asList("NSX", "Netris").stream() + .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); + } + + public boolean isForNsx() { + return provider != null && provider.equalsIgnoreCase("NSX"); + } + + public boolean isForNetris() { + return provider != null && provider.equalsIgnoreCase("Netris"); + } + + public String getProvider() { + return provider; + } + + public List getSupportedServices() { + if (!isExternalNetworkProvider()) { + return supportedServices == null ? new ArrayList() : supportedServices; + } else { + List services = new ArrayList<>(List.of( + Dhcp.getName(), + Dns.getName(), + UserData.getName() + )); + if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + services.addAll(Arrays.asList( + StaticNat.getName(), + SourceNat.getName(), + PortForwarding.getName())); + } + if (getNsxSupportsLbService() || (provider != null && isNetrisNatted(getProvider(), getNetworkMode()))) { + services.add(Lb.getName()); + } + if (Boolean.TRUE.equals(forVpc)) { + services.add(NetworkACL.getName()); + } else { + services.add(Firewall.getName()); + } + return services; + } + } + + public String getInternetProtocol() { + return internetProtocol; + } + + public Boolean getSpecifyIpRanges() { + return specifyIpRanges == null ? false : specifyIpRanges; + } + + public Boolean getConserveMode() { + if (conserveMode == null) { + return true; + } + return conserveMode; + } + + public Boolean getIsPersistent() { + return isPersistent == null ? false : isPersistent; + } + + public Boolean getForVpc() { + return forVpc; + } + + public String getNetworkMode() { + return networkMode; + } + + public boolean getNsxSupportsLbService() { + return BooleanUtils.isTrue(nsxSupportsLbService); + } + + public boolean getNsxSupportsInternalLbService() { + return BooleanUtils.isTrue(nsxSupportsInternalLbService); + } + + public Boolean getForTungsten() { + return forTungsten; + } + + public Boolean getEgressDefaultPolicy() { + if (egressDefaultPolicy == null) { + return true; + } + return egressDefaultPolicy; + } + + public Boolean getKeepAliveEnabled() { + return keepAliveEnabled; + } + + public Integer getMaxconnections() { + return maxConnections; + } + + public Map> getServiceProviders() { + Map> serviceProviderMap = new HashMap<>(); + if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { + Collection servicesCollection = serviceProviderList.values(); + Iterator iter = servicesCollection.iterator(); + while (iter.hasNext()) { + HashMap services = (HashMap) iter.next(); + String service = services.get("service"); + String provider = services.get("provider"); + List providerList = null; + if (serviceProviderMap.containsKey(service)) { + providerList = serviceProviderMap.get(service); + } else { + providerList = new ArrayList(); + } + providerList.add(provider); + serviceProviderMap.put(service, providerList); + } + } else if (isExternalNetworkProvider()) { + getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); + } + return serviceProviderMap; + } + + private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { + String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() : + VirtualRouterProvider.Type.VirtualRouter.name(); + List unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService")); + List routerSupported = List.of("Dhcp", "Dns", "UserData"); + List allServices = Network.Service.listAllServices().stream().map(Network.Service::getName).collect(Collectors.toList()); + if (routerProvider.equals(VirtualRouterProvider.Type.VPCVirtualRouter.name())) { + unsupportedServices.add("Firewall"); + } else { + unsupportedServices.add("NetworkACL"); + } + for (String service : allServices) { + if (unsupportedServices.contains(service)) + continue; + if (routerSupported.contains(service)) + serviceProviderMap.put(service, List.of(routerProvider)); + else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || NetworkACL.getName().equalsIgnoreCase(service)) { + serviceProviderMap.put(service, List.of(provider)); + } + if (isNsxWithoutLb(getProvider(), getNsxSupportsLbService()) || isNetrisRouted(getProvider(), getNetworkMode())) { + serviceProviderMap.remove(Lb.getName()); + } + } + } + + public Map getServiceCapabilities(Network.Service service) { + Map capabilityMap = null; + + if (serviceCapabilitiesList != null && !serviceCapabilitiesList.isEmpty()) { + capabilityMap = new HashMap(); + Collection serviceCapabilityCollection = serviceCapabilitiesList.values(); + Iterator iter = serviceCapabilityCollection.iterator(); + while (iter.hasNext()) { + HashMap svcCapabilityMap = (HashMap) iter.next(); + Network.Capability capability = null; + String svc = svcCapabilityMap.get("service"); + String capabilityName = svcCapabilityMap.get("capabilitytype"); + String capabilityValue = svcCapabilityMap.get("capabilityvalue"); + + if (capabilityName != null) { + capability = Network.Capability.getCapability(capabilityName); + } + + if ((capability == null) || (capabilityName == null) || (capabilityValue == null)) { + throw new InvalidParameterValueException("Invalid capability:" + capabilityName + " capability value:" + capabilityValue); + } + + if (svc.equalsIgnoreCase(service.getName())) { + capabilityMap.put(capability, capabilityValue); + } else { + //throw new InvalidParameterValueException("Service is not equal ") + } + } + } + + return capabilityMap; + } + + public Map getDetails() { + if (details == null || details.isEmpty()) { + return sourceDetailsMap; + } + Collection paramsCollection = details.values(); + Object objlist[] = paramsCollection.toArray(); + Map params = (Map) (objlist[0]); + for (int i = 1; i < objlist.length; i++) { + params.putAll((Map) (objlist[i])); + } + + return params; + } + + public String getServicePackageId() { + Map data = getDetails(); + if (data == null) + return null; + return data.get(NetworkOffering.Detail.servicepackageuuid + ""); + } + + public List getDomainIds() { + if (CollectionUtils.isNotEmpty(domainIds)) { + Set set = new LinkedHashSet<>(domainIds); + domainIds.clear(); + domainIds.addAll(set); + } + return domainIds; + } + + public List getZoneIds() { + if (CollectionUtils.isNotEmpty(zoneIds)) { + Set set = new LinkedHashSet<>(zoneIds); + zoneIds.clear(); + zoneIds.addAll(set); + } + return zoneIds; + } + + public Boolean getEnable() { + if (enable != null) { + return enable; + } + return false; + } + + public boolean getSpecifyAsNumber() { + return BooleanUtils.toBoolean(specifyAsNumber); + } + + public String getRoutingMode() { + return routingMode; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedGuestVlanRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedGuestVlanRangeCmd.java index 632ff9c6ac7f..56d042719f68 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedGuestVlanRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedGuestVlanRangeCmd.java @@ -72,7 +72,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Releasing a dedicated guest vlan range."; + return "Releasing dedicated guest VLAN range with ID: " + getResourceUuid(ApiConstants.ID); } // /////////////////////////////////////////////////// @@ -81,7 +81,7 @@ public String getEventDescription() { @Override public void execute() { - CallContext.current().setEventDetails("Dedicated guest vlan range Id: " + id); + CallContext.current().setEventDetails("Dedicated guest VLAN range ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkService.releaseDedicatedGuestVlanRange(getId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmd.java index 3e151b9b58f4..a5e763c0cb00 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmd.java @@ -59,7 +59,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Releasing a dedicated zone IPv4 subnet " + getId(); + return "Releasing dedicated zone IPv4 subnet with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmd.java index da7a23f50d9c..db5daa505bee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmd.java @@ -69,7 +69,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating zone IPv4 subnet " + getId(); + return "Updating zone IPv4 subnet with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkServiceProviderCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkServiceProviderCmd.java index db57abad2489..e0ce0aade1ee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkServiceProviderCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkServiceProviderCmd.java @@ -100,7 +100,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating physical network ServiceProvider: " + getId(); + return "Updating Physical Network ServiceProvider with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePhysicalNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePhysicalNetworkCmd.java index a9ad46fdd78f..6a6264e418ce 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePhysicalNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePhysicalNetworkCmd.java @@ -98,7 +98,7 @@ public void execute() { @Override public String getEventDescription() { - return "Updating Physical network: " + getId(); + return "Updating Physical Network with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java index 394d42a65a3a..0dfa83a68289 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdatePodManagementNetworkIpRangeCmd.java @@ -113,7 +113,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating pod management IP range " + getNewStartIP() + "-" + getNewEndIP() + " of Pod: " + getPodId(); + return "Updating pod management IP range " + getNewStartIP() + "-" + getNewEndIP() + " of Pod: " + getResourceUuid(ApiConstants.POD_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateStorageNetworkIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateStorageNetworkIpRangeCmd.java index a1f4d2ed1002..978e94a783a9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateStorageNetworkIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateStorageNetworkIpRangeCmd.java @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Update storage ip range " + getId() + " [StartIp=" + getStartIp() + ", EndIp=" + getEndIp() + ", vlan=" + getVlan() + ", netmask=" + getNetmask() + ']'; + return "Updating storage IP range " + getResourceUuid(ApiConstants.ID) + " [StartIp=" + getStartIp() + ", EndIp=" + getEndIp() + ", VLAN=" + getVlan() + ", netmask=" + getNetmask() + ']'; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmd.java index 1d6bffca342e..3c58cbb3c532 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmd.java @@ -80,7 +80,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Changing Bgp Peers for network " + getNetworkId(); + return "Changing BGP Peers for Network with ID: " + getResourceUuid(ApiConstants.NETWORK_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmd.java index 0c89f3f1d43c..8784f0672790 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmd.java @@ -80,7 +80,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Changing Bgp Peers for VPC " + getVpcId(); + return "Changing BGP Peers for VPC with ID: " + getResourceUuid(ApiConstants.VPC_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmd.java index 80642124938a..f1d9b6723091 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmd.java @@ -145,7 +145,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating Bgp Peer " + getAsNumber() + " for zone=" + getZoneId(); + return "Creating BGP Peer " + getAsNumber() + " for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmd.java index ec3d0ea11629..f1ef963e9872 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmd.java @@ -82,7 +82,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Dedicating Bgp Peer " + getId(); + return "Dedicating BGP Peer with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmd.java index a01711efa44f..a412e91bc48e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmd.java @@ -59,7 +59,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting Bgp Peer " + getId(); + return "Deleting BGP Peer with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmd.java index 92610c233ef0..c754d443c051 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmd.java @@ -59,7 +59,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Releasing a dedicated Bgp Peer " + getId(); + return "Releasing dedicated BGP Peer with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmd.java index ae44330ea033..f45c1ee5a2f3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmd.java @@ -120,7 +120,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating Bgp Peer " + getId(); + return "Updating BGP Peer with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java new file mode 100644 index 000000000000..8d822be203ad --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneDiskOfferingCmd.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DiskOfferingResponse; + +import com.cloud.offering.DiskOffering; + +@APICommand(name = "cloneDiskOffering", + description = "Clones a disk offering. All parameters from createDiskOffering are available. If not specified, values will be copied from the source offering.", + responseObject = DiskOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class CloneDiskOfferingCmd extends CreateDiskOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = DiskOfferingResponse.class, + required = true, + description = "The ID of the source disk offering to clone from") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + DiskOffering result = _configService.cloneDiskOffering(this); + if (result != null) { + DiskOfferingResponse response = _responseGenerator.createDiskOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone disk offering"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java new file mode 100644 index 000000000000..d01ca4c195da --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; + +import com.cloud.offering.ServiceOffering; + +@APICommand(name = "cloneServiceOffering", + description = "Clones a service offering. All parameters from createServiceOffering are available. If not specified, values will be copied from the source offering.", + responseObject = ServiceOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin}) +public class CloneServiceOfferingCmd extends CreateServiceOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = CommandType.UUID, + entityType = ServiceOfferingResponse.class, + required = true, + description = "The ID of the source service offering to clone from") + private Long sourceOfferingId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + + @Override + public void execute() { + try { + ServiceOffering result = _configService.cloneServiceOffering(this); + if (result != null) { + ServiceOfferingResponse response = _responseGenerator.createServiceOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone service offering"); + } + } catch (com.cloud.exception.InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (com.cloud.utils.exception.CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/ChangeOutOfBandManagementPasswordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/ChangeOutOfBandManagementPasswordCmd.java index b22697768681..b9a729bc1b77 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/ChangeOutOfBandManagementPasswordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/ChangeOutOfBandManagementPasswordCmd.java @@ -70,7 +70,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId()); } - CallContext.current().setEventDetails("Host Id: " + host.getId() + " Password: " + getPassword().charAt(0) + "****"); + CallContext.current().setEventDetails("Host ID: " + host.getUuid() + " Password: " + getPassword().charAt(0) + "****"); CallContext.current().putContextParameter(Host.class, host.getUuid()); final OutOfBandManagementResponse response = outOfBandManagementService.changePassword(host, getPassword()); @@ -101,7 +101,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "change out-of-band management password for host: " + getHostId(); + return "Changing out-of-band management password for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForClusterCmd.java index 26b0eac6be4a..5b0f3a802071 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForClusterCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.disableOutOfBandManagement(cluster); - CallContext.current().setEventDetails("Cluster Id:" + cluster.getId() + " out-of-band management enabled: false"); + CallContext.current().setEventDetails("Cluster ID:" + cluster.getUuid() + " out-of-band management enabled: false"); CallContext.current().putContextParameter(Cluster.class, cluster.getUuid()); response.setResponseName(getCommandName()); @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "disable out-of-band management password for cluster: " + getClusterId(); + return "Disabling out-of-band management password for cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForHostCmd.java index 6c9b48ef28f7..c70622134531 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForHostCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.disableOutOfBandManagement(host); - CallContext.current().setEventDetails("Host Id:" + host.getId() + " out-of-band management enabled: false"); + CallContext.current().setEventDetails("Host ID:" + host.getUuid() + " out-of-band management enabled: false"); CallContext.current().putContextParameter(Host.class, host.getUuid()); response.setId(host.getUuid()); @@ -94,7 +94,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "disable out-of-band management password for host: " + getHostId(); + return "Disabling out-of-band management password for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForZoneCmd.java index 4f262ca5c09f..2f2750580516 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/DisableOutOfBandManagementForZoneCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.disableOutOfBandManagement(zone); - CallContext.current().setEventDetails("Zone Id:" + zone.getId() + " out-of-band management enabled: false"); + CallContext.current().setEventDetails("Zone ID:" + zone.getUuid() + " out-of-band management enabled: false"); CallContext.current().putContextParameter(DataCenter.class, zone.getUuid()); response.setResponseName(getCommandName()); @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "disable out-of-band management password for zone: " + getZoneId(); + return "Disabling out-of-band management password for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForClusterCmd.java index 6620f86907e9..4419ed18ee59 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForClusterCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.enableOutOfBandManagement(cluster); - CallContext.current().setEventDetails("Cluster Id:" + cluster.getId() + " out-of-band management enabled: true"); + CallContext.current().setEventDetails("Cluster ID:" + cluster.getUuid() + " out-of-band management enabled: true"); CallContext.current().putContextParameter(Cluster.class, cluster.getUuid()); response.setResponseName(getCommandName()); @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "enable out-of-band management password for cluster: " + getClusterId(); + return "Enabling out-of-band management password for cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForHostCmd.java index 3cfee31f78a8..b3f2e8aaf821 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForHostCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.enableOutOfBandManagement(host); - CallContext.current().setEventDetails("Host Id:" + host.getId() + " out-of-band management enabled: true"); + CallContext.current().setEventDetails("Host ID:" + host.getUuid() + " out-of-band management enabled: true"); CallContext.current().putContextParameter(Host.class, host.getUuid()); response.setId(host.getUuid()); @@ -94,7 +94,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "enable out-of-band management password for host: " + getHostId(); + return "Enabling out-of-band management password for host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForZoneCmd.java index 99d2324b1b66..8f6eb2dc3500 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/EnableOutOfBandManagementForZoneCmd.java @@ -70,7 +70,7 @@ final public void execute() throws ResourceUnavailableException, InsufficientCap OutOfBandManagementResponse response = outOfBandManagementService.enableOutOfBandManagement(zone); - CallContext.current().setEventDetails("Zone Id:" + zone.getId() + " out-of-band management enabled: true"); + CallContext.current().setEventDetails("Zone ID:" + zone.getUuid() + " out-of-band management enabled: true"); CallContext.current().putContextParameter(DataCenter.class, zone.getUuid()); response.setResponseName(getCommandName()); @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "enable out-of-band management password for zone: " + getZoneId(); + return "Enabling out-of-band management password for zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/IssueOutOfBandManagementPowerActionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/IssueOutOfBandManagementPowerActionCmd.java index c63b03aad1b8..bba3fee6ceca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/IssueOutOfBandManagementPowerActionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/outofbandmanagement/IssueOutOfBandManagementPowerActionCmd.java @@ -75,7 +75,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } final PowerOperation powerOperation = PowerOperation.valueOf(getPowerAction()); - CallContext.current().setEventDetails("Host Id: " + host.getId() + " Action: " + powerOperation.toString()); + CallContext.current().setEventDetails("Host ID: " + host.getUuid() + " Action: " + powerOperation.toString()); CallContext.current().putContextParameter(Host.class, host.getUuid()); final OutOfBandManagementResponse response = outOfBandManagementService.executePowerOperation(host, powerOperation, getActionTimeout()); @@ -107,7 +107,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "issue out-out-band management power action: " + getPowerAction() + " on host: " + getHostId(); + return "Issuing out-out-band management power action: " + getPowerAction() + " to host: " + getResourceUuid(ApiConstants.HOST_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/region/DeletePortableIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/region/DeletePortableIpRangeCmd.java index da7293ea2198..ac8fe66a8144 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/region/DeletePortableIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/region/DeletePortableIpRangeCmd.java @@ -83,7 +83,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting a portable public ip range"; + return "Deleting a portable public ip range with ID: " + getResourceUuid(ApiConstants.ID) ; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateCmd.java index c5ae6890c3e5..7144bbaa6015 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/resource/UploadCustomCertificateCmd.java @@ -84,7 +84,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Uploading custom certificate to the db, and applying it to all the cpvms in the system"); + return ("Uploading custom certificate and applying it to all the CPVMs in the system"); } public static String getResultObjectName() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureOvsElementCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureOvsElementCmd.java index e40462149582..474e13de32f3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureOvsElementCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureOvsElementCmd.java @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "configuring ovs provider: " + id; + return "Configuring OVS provider with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -109,7 +109,7 @@ public Long getApiResourceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Ovs element: " + id); + CallContext.current().setEventDetails("OVS element: " + getResourceUuid(ApiConstants.ID)); OvsProvider result = _service.get(0).configure(this); if (result != null) { OvsProviderResponse ovsResponse = _responseGenerator diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureVirtualRouterElementCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureVirtualRouterElementCmd.java index 7be41834cd5a..ba0110f014a0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureVirtualRouterElementCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/ConfigureVirtualRouterElementCmd.java @@ -100,7 +100,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "configuring virtual router provider: " + id; + return "Configuring virtual router with provider that has ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -115,7 +115,7 @@ public Long getApiResourceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Virtual router element: " + id); + CallContext.current().setEventDetails("Virtual router element ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouterProvider result = _service.get(0).configure(this); if (result != null) { VirtualRouterProviderResponse routerResponse = _responseGenerator.createVirtualRouterProviderResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/CreateVirtualRouterElementCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/CreateVirtualRouterElementCmd.java index 7c17fd794da6..094e87ae0058 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/CreateVirtualRouterElementCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/CreateVirtualRouterElementCmd.java @@ -98,7 +98,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Virtual router element Id: " + getEntityUuid()); + CallContext.current().setEventDetails("Virtual router element ID: " + getEntityUuid()); VirtualRouterProvider result = _service.get(0).getCreatedElement(getEntityId()); if (result != null) { VirtualRouterProviderResponse response = _responseGenerator.createVirtualRouterProviderResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/DestroyRouterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/DestroyRouterCmd.java index 66fc2eb79e6d..d8355cadb0a0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/DestroyRouterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/DestroyRouterCmd.java @@ -74,7 +74,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "destroying router: " + this._uuidMgr.getUuid(VirtualMachine.class,getId()); + return "Destroying virtual router with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -90,7 +90,7 @@ public Long getApiResourceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException { CallContext ctx = CallContext.current(); - ctx.setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class,getId())); + ctx.setEventDetails("Router ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = _routerService.destroyRouter(getId(), ctx.getCallingAccount(), ctx.getCallingUserId()); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/GetRouterHealthCheckResultsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/GetRouterHealthCheckResultsCmd.java index 6d4d62929ded..a302ebee1046 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/GetRouterHealthCheckResultsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/GetRouterHealthCheckResultsCmd.java @@ -87,7 +87,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InvalidParameterValueException, ServerApiException { - CallContext.current().setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getRouterId())); + CallContext.current().setEventDetails("Router ID: " + getResourceUuid(ApiConstants.ROUTER_ID)); VirtualRouter router = _routerService.findRouter(getRouterId()); if (router == null || router.getRole() != VirtualRouter.Role.VIRTUAL_ROUTER) { throw new InvalidParameterValueException("Can't find router by routerId"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java index 36ffdcb42d00..d90208e36d34 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/RebootRouterCmd.java @@ -78,7 +78,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "rebooting router: " + this._uuidMgr.getUuid(VirtualMachine.class,getId()); + return "Rebooting router with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -97,7 +97,7 @@ public boolean isForced() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class,getId())); + CallContext.current().setEventDetails("Router ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = _routerService.rebootRouter(getId(), true, isForced()); if (result != null) { DomainRouterResponse response = _responseGenerator.createDomainRouterResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StartRouterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StartRouterCmd.java index 65849be0d685..263f53ca2e48 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StartRouterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StartRouterCmd.java @@ -81,7 +81,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "starting router: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Starting virtual router with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -96,7 +96,7 @@ public Long getApiResourceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Router Id: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = null; VirtualRouter router = _routerService.findRouter(getId()); if (router == null || router.getRole() != Role.VIRTUAL_ROUTER) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StopRouterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StopRouterCmd.java index 775ffcb4d38e..838762740281 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StopRouterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/StopRouterCmd.java @@ -79,7 +79,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Stopping router: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Stopping virtual router with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -98,7 +98,7 @@ public boolean isForced() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException { - CallContext.current().setEventDetails("Router Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Router ID: " + getResourceUuid(ApiConstants.ID)); VirtualRouter result = null; VirtualRouter router = _routerService.findRouter(getId()); if (router == null || router.getRole() != Role.VIRTUAL_ROUTER) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/UpgradeRouterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/UpgradeRouterTemplateCmd.java index 1bbc3a141dbc..6fdfa7d3642d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/router/UpgradeRouterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/router/UpgradeRouterTemplateCmd.java @@ -119,7 +119,7 @@ public Long getInstanceId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Upgrading router Template"); + CallContext.current().setEventDetails("Upgrading router with with ID: " + getResourceUuid(ApiConstants.ID) + " template"); List result = _routerService.upgradeRouterTemplate(this); if (result != null) { ListResponse response = _responseGenerator.createUpgradeRouterTemplateResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java index 2fe3c7cd106a..75fcf125eb10 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/AddImageStoreS3CMD.java @@ -27,7 +27,7 @@ import static org.apache.cloudstack.api.ApiConstants.S3_HTTPS_FLAG; import static org.apache.cloudstack.api.ApiConstants.S3_MAX_ERROR_RETRY; import static org.apache.cloudstack.api.ApiConstants.S3_SIGNER; -import static org.apache.cloudstack.api.ApiConstants.S3_SECRET_KEY; +import static org.apache.cloudstack.api.ApiConstants.SECRET_KEY; import static org.apache.cloudstack.api.ApiConstants.S3_SOCKET_TIMEOUT; import static org.apache.cloudstack.api.ApiConstants.S3_USE_TCP_KEEPALIVE; import static org.apache.cloudstack.api.BaseCmd.CommandType.BOOLEAN; @@ -64,7 +64,7 @@ public final class AddImageStoreS3CMD extends BaseCmd implements ClientOptions { @Parameter(name = S3_ACCESS_KEY, type = STRING, required = true, description = "S3 access key") private String accessKey; - @Parameter(name = S3_SECRET_KEY, type = STRING, required = true, description = "S3 secret key") + @Parameter(name = SECRET_KEY, type = STRING, required = true, description = "S3 secret key") private String secretKey; @Parameter(name = S3_END_POINT, type = STRING, required = true, description = "S3 endpoint") @@ -101,7 +101,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE Map dm = new HashMap(); dm.put(ApiConstants.S3_ACCESS_KEY, getAccessKey()); - dm.put(ApiConstants.S3_SECRET_KEY, getSecretKey()); + dm.put(ApiConstants.SECRET_KEY, getSecretKey()); dm.put(ApiConstants.S3_END_POINT, getEndPoint()); dm.put(ApiConstants.S3_BUCKET_NAME, getBucketName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/CancelPrimaryStorageMaintenanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/CancelPrimaryStorageMaintenanceCmd.java index 35f7b19f709d..7b69d25caef4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/CancelPrimaryStorageMaintenanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/CancelPrimaryStorageMaintenanceCmd.java @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "canceling maintenance for primary storage pool: " + getId(); + return "Canceling maintenance mode for primary storage pool with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java index d3b6a0746106..3bb16dfea5a4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ChangeStoragePoolScopeCmd.java @@ -28,7 +28,6 @@ import org.apache.cloudstack.context.CallContext; import com.cloud.event.EventTypes; -import com.cloud.storage.StoragePool; @APICommand(name = "changeStoragePoolScope", description = "Changes the scope of a storage pool when the pool is in Disabled state." + "This feature is officially tested and supported for Hypervisors: KVM and VMware, Protocols: NFS and Ceph, and Storage Provider: DefaultPrimary. " + @@ -61,15 +60,7 @@ public String getEventType() { @Override public String getEventDescription() { - String description = "Change storage pool scope. Storage pool Id: "; - StoragePool pool = _entityMgr.findById(StoragePool.class, getId()); - if (pool != null) { - description += pool.getUuid(); - } else { - description += getId(); - } - description += " to " + getScope(); - return description; + return "Changing storage pool with ID: " + getResourceUuid(ApiConstants.ID) + " to scope " + scope; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ConfigureStorageAccessCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ConfigureStorageAccessCmd.java index bfa2589921f7..c5459adfd2d0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ConfigureStorageAccessCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ConfigureStorageAccessCmd.java @@ -130,6 +130,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "configuring storage access groups"; + return "Configuring storage access groups"; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmd.java index 92019e70eca2..1d927ac5cbd6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmd.java @@ -94,6 +94,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Downloading object at path " + getPath() + " on image store " + getStoreId(); + return "Downloading object at path " + getPath() + " on image store " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/PreparePrimaryStorageForMaintenanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/PreparePrimaryStorageForMaintenanceCmd.java index 818b3a5bbeab..9f0efe7f7a1b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/PreparePrimaryStorageForMaintenanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/PreparePrimaryStorageForMaintenanceCmd.java @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "preparing storage pool: " + getId() + " for maintenance"; + return "Preparing storage pool with ID: " + getResourceUuid(ApiConstants.ID) + " for maintenance"; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/SyncStoragePoolCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/SyncStoragePoolCmd.java index 9f81f2f6c86c..684243c08299 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/SyncStoragePoolCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/SyncStoragePoolCmd.java @@ -67,7 +67,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Attempting to synchronise storage pool with management server"; + return "Attempting to synchronise storage pool with ID:" + getResourceUuid(ApiConstants.ID) + " with management server"; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/DestroySystemVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/DestroySystemVmCmd.java index 3a7e1caa4dc3..6b776d067784 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/DestroySystemVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/DestroySystemVmCmd.java @@ -76,7 +76,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Destroying system Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Destroying System VM with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -91,14 +91,14 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("System VM ID: " + getResourceUuid(ApiConstants.ID)); VirtualMachine instance = _mgr.destroySystemVM(this); if (instance != null) { SystemVmResponse response = _responseGenerator.createSystemVmResponse(instance); response.setResponseName(getCommandName()); setResponseObject(response); } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Fail to destroy system vm"); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to destroy System VM"); } } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java index 8319883ec442..feeb3f44553d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java @@ -120,7 +120,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Attempting to migrate Instance Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVirtualMachineId()) + " to host Id: " + this._uuidMgr.getUuid(Host.class, getHostId()); + return "Attempting to migrate System VM with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } @Override @@ -138,7 +138,7 @@ public void execute() { if (destStoragePool == null) { throw new InvalidParameterValueException("Unable to find the storage pool to migrate the Instance"); } - CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStorageId()); + CallContext.current().setEventDetails("System VM ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to storage pool with ID: " + getResourceUuid(ApiConstants.STORAGE_ID)); migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), destStoragePool); } else { Host destinationHost = null; @@ -153,7 +153,7 @@ public void execute() { } else if (! isAutoSelect()) { throw new InvalidParameterValueException("Please specify a host or storage as destination, or pass 'autoselect=true' to automatically select a destination host which do not require storage migration"); } - CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); + CallContext.current().setEventDetails("System VM ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to host with ID: " + getResourceUuid(ApiConstants.HOST_ID)); if (destinationHost == null) { migratedVm = _userVmService.migrateVirtualMachine(getVirtualMachineId(), null); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/PatchSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/PatchSystemVMCmd.java index 6a4361061dfa..13f6167513b0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/PatchSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/PatchSystemVMCmd.java @@ -19,7 +19,6 @@ import com.cloud.event.EventTypes; import com.cloud.user.Account; import com.cloud.utils.Pair; -import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -72,7 +71,7 @@ public String getEventType() { @Override public String getEventDescription() { - return String.format("Attempting to live patch System VM with Id: %s ", this._uuidMgr.getUuid(VirtualMachine.class, getId())); + return String.format("Attempting to live patch System VM with ID: %s ", getResourceUuid(ApiConstants.ID)); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java index 81affc714525..0a0ffe847adc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/RebootSystemVmCmd.java @@ -86,7 +86,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "rebooting system vm: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Rebooting System VM with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -105,14 +105,14 @@ public boolean isForced() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); VirtualMachine result = _mgr.rebootSystemVM(this); if (result != null) { SystemVmResponse response = _responseGenerator.createSystemVmResponse(result); response.setResponseName(getCommandName()); setResponseObject(response); } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Fail to reboot system vm"); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reboot System Instance"); } } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/ScaleSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/ScaleSystemVMCmd.java index eaf927ae0f72..061a2ad2deed 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/ScaleSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/ScaleSystemVMCmd.java @@ -98,7 +98,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("SystemVm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("System VM ID: " + getResourceUuid(ApiConstants.ID)); ServiceOffering serviceOffering = _entityMgr.findById(ServiceOffering.class, serviceOfferingId); if (serviceOffering == null) { @@ -137,6 +137,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Upgrading system vm: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()) + " to service offering: " + this._uuidMgr.getUuid(ServiceOffering.class, getServiceOfferingId()); + return "Upgrading System VM with ID: " + getResourceUuid(ApiConstants.ID) + " to service offering: " + getResourceUuid(ApiConstants.SERVICE_OFFERING_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StartSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StartSystemVMCmd.java index bfb6a240a62d..734b553c2dd5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StartSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StartSystemVMCmd.java @@ -87,7 +87,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "starting system vm: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Starting System VM with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -102,7 +102,7 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); VirtualMachine instance = _mgr.startSystemVM(getId()); if (instance != null) { SystemVmResponse response = _responseGenerator.createSystemVmResponse(instance); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StopSystemVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StopSystemVmCmd.java index e86b6a281215..bdcb5b407b39 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StopSystemVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/StopSystemVmCmd.java @@ -89,7 +89,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Stopping system vm: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Stopping System VM with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -108,7 +108,7 @@ public boolean isForced() { @Override public void execute() throws ResourceUnavailableException, ConcurrentOperationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); VirtualMachine result = _mgr.stopSystemVM(this); if (result != null) { SystemVmResponse response = _responseGenerator.createSystemVmResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/UpgradeSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/UpgradeSystemVMCmd.java index 4567c25a0d1a..a27c04374c32 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/UpgradeSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/UpgradeSystemVMCmd.java @@ -87,7 +87,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); ServiceOffering serviceOffering = _entityMgr.findById(ServiceOffering.class, serviceOfferingId); if (serviceOffering == null) { @@ -100,7 +100,7 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Fail to reboot system vm"); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reboot System VM"); } } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/AddTrafficTypeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/AddTrafficTypeCmd.java index 2ba3b321887d..50abd953e63f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/AddTrafficTypeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/AddTrafficTypeCmd.java @@ -148,7 +148,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("TrafficType Id: " + getEntityId()); + CallContext.current().setEventDetails("Traffic type ID: " + getEntityUuid()); PhysicalNetworkTrafficType result = _networkService.getPhysicalNetworkTrafficType(getEntityId()); if (result != null) { TrafficTypeResponse response = _responseGenerator.createTrafficTypeResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/DeleteTrafficTypeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/DeleteTrafficTypeCmd.java index d8813eefa6af..3f07020e5fa2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/DeleteTrafficTypeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/DeleteTrafficTypeCmd.java @@ -71,7 +71,7 @@ public void execute() { @Override public String getEventDescription() { - return "Deleting Traffic Type: " + getId(); + return "Deleting Traffic Type with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/UpdateTrafficTypeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/UpdateTrafficTypeCmd.java index 0de4cfb7edda..29b06a3b5259 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/UpdateTrafficTypeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/usage/UpdateTrafficTypeCmd.java @@ -118,7 +118,7 @@ public void execute() { @Override public String getEventDescription() { - return "Updating Traffic Type: " + getId(); + return "Updating Traffic Type with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java index f03bb1c4ddd3..684103cf8d39 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.user.Account; @@ -78,6 +79,12 @@ public class CreateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.USER_ID, type = CommandType.STRING, description = "User UUID, required for adding account from external provisioning system") private String userUUID; + @Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED, + type = CommandType.BOOLEAN, + description = "Provide true to mandate the User to reset password on next login.", + since = "4.23.0") + private Boolean passwordChangeRequired; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -118,6 +125,10 @@ public String getUserUUID() { return userUUID; } + public Boolean isPasswordChangeRequired() { + return BooleanUtils.isTrue(passwordChangeRequired); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -147,7 +158,7 @@ public void execute() { CallContext.current().setEventDetails("UserName: " + getUserName() + ", FirstName :" + getFirstName() + ", LastName: " + getLastName()); User user = _accountService.createUser(getUserName(), getPassword(), getFirstName(), getLastName(), getEmail(), getTimezone(), getAccountName(), getDomainId(), - getUserUUID()); + getUserUUID(), isPasswordChangeRequired()); if (user != null) { UserResponse response = _responseGenerator.createUserResponse(user); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserCmd.java index f954ac939c3c..01886187f9b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserCmd.java @@ -82,7 +82,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - CallContext.current().setEventDetails("UserId: " + getId()); + CallContext.current().setEventDetails("User ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _regionService.deleteUser(this); if (!result) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete user"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java new file mode 100644 index 000000000000..6cf55514ba36 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DeleteUserKeysCmd.java @@ -0,0 +1,81 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +@APICommand(name = "deleteUserKeys", description = "Deletes a keypair from a user", responseObject = SuccessResponse.class, + since = "4.23.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteUserKeysCmd extends BaseAsyncCmd { + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, required = true, description = "ID of the keypair to be deleted.") + private Long id; + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(id); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getId() { + return id; + } + + @Override + public void execute() { + _accountService.deleteApiKey(this); + + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DELETE_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + ApiKeyPair keyPair = apiKeyPairService.findById(id); + return String.format("Deleting API key pair with ID [%s]%s", + keyPair == null ? id : keyPair.getUuid(), + keyPair == null ? "." : String.format(" and name [%s].", keyPair.getName())); + } + + @Override + public Long getSyncObjId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java index 6ce669d8523d..cc2bc0906a24 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/DisableUserCmd.java @@ -78,12 +78,12 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "disabling user: " + this._uuidMgr.getUuid(User.class, getId()); + return "Disabling User with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("User ID: " + this._uuidMgr.getUuid(User.class, getId())); + CallContext.current().setEventDetails("User ID: " + getResourceUuid(ApiConstants.ID)); UserAccount user = _regionService.disableUser(this); if (user != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/EnableUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/EnableUserCmd.java index 77d8d530daf2..9141a9bccf8a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/EnableUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/EnableUserCmd.java @@ -71,7 +71,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("UserId: " + getId()); + CallContext.current().setEventDetails("User ID: " + getResourceUuid(ApiConstants.ID)); UserAccount user = _regionService.enableUser(this); if (user != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserCmd.java index 3427cef33661..a3dcf08c3a31 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserCmd.java @@ -22,7 +22,7 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.UserResponse; - +import org.apache.cloudstack.api.ApiArgValidator; import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.UserAccount; @@ -35,7 +35,7 @@ public class GetUserCmd extends BaseCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, required = true, description = "API key of the user") + @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, required = true, description = "API key of the user", validations = {ApiArgValidator.NotNullOrEmpty}) private String apiKey; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java index f04c5f6c478c..a651befcff7e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/GetUserKeysCmd.java @@ -32,33 +32,33 @@ import java.util.Map; @APICommand(name = "getUserKeys", - description = "This command allows the user to query the seceret and API keys for the account", - responseObject = RegisterUserKeyResponse.class, - requestHasSensitiveInfo = false, - responseHasSensitiveInfo = true, - authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, - since = "4.10.0") - -public class GetUserKeysCmd extends BaseCmd{ - - @Parameter(name= ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") + description = "Queries the last registered secret and API keys of a user.", + responseObject = RegisterUserKeyResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.10.0") +public class GetUserKeysCmd extends BaseCmd { + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user whose keys are required") private Long id; - - public Long getID(){ + public Long getId() { return id; - }public long getEntityOwnerId(){ - User user = _entityMgr.findById(User.class, getID()); - if(user != null){ + } + + public long getEntityOwnerId() { + User user = _entityMgr.findById(User.class, getId()); + if (user != null) { return user.getAccountId(); } - else return Account.ACCOUNT_ID_SYSTEM; + return Account.ACCOUNT_ID_SYSTEM; } - public void execute(){ + + public void execute() { Pair> keys = _accountService.getKeys(this); RegisterUserKeyResponse response = new RegisterUserKeyResponse(); - if(keys != null){ + if (keys != null){ response.setApiKeyAccess(keys.first()); response.setApiKey(keys.second().get("apikey")); response.setSecretKey(keys.second().get("secretkey")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java new file mode 100644 index 000000000000..4345cae92911 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeyRulesCmd.java @@ -0,0 +1,68 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.user; + + +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.BaseRolePermissionResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import java.util.List; + +@APICommand(name = "listUserKeyRules", + description = "Lists the rules defined for a API key pair.", + responseObject = BaseRolePermissionResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class ListUserKeyRulesCmd extends BaseCmd { + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the key pair.", required = true) + private Long id; + + public Long getId() { + return id; + } + + public long getEntityOwnerId() { + ApiKeyPair keyPair = apiKeyPairService.findById(getId()); + if (keyPair != null) { + return keyPair.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { + List permissions = _accountService.listKeyRules(this); + ListResponse response = _responseGenerator.createKeypairPermissionsResponse(permissions); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java new file mode 100644 index 000000000000..ded05d6381a0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUserKeysCmd.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.user; + + +import com.cloud.user.Account; +import com.cloud.user.UserAccount; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +@APICommand(name = "listUserKeys", + description = "Lists the API key pairs (API and secret keys) of a user.", + responseObject = ApiKeyPairResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.User, RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin}, + since = "4.23.0") +public class ListUserKeysCmd extends BaseListCmd { + @ACL + @Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, description = "ID of the user that owns the keys.") + private Long userId; + + @ACL + @Parameter(name = ApiConstants.KEYPAIR_ID, type = CommandType.UUID, entityType = ApiKeyPairResponse.class, description = "ID of the key pair.") + private Long keyPairId; + + @Parameter(name = ApiConstants.API_KEY_FILTER, type = CommandType.STRING, description = "API key of the key pair.") + private String apiKeyFilter; + + @Parameter(name = ApiConstants.SHOW_PERMISSIONS, type = CommandType.BOOLEAN, description = "Whether API Key rules should be returned. Defaults to false.") + private boolean showPermissions = false; + + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin}, + description = "Lists all API key pairs of users that are accessible by the calling account. Only available for administrators. Defaults to false.") + private boolean listAll = false; + + public Long getUserId() { + return userId; + } + + public Long getKeyId() { + return keyPairId; + } + + public String getApiKeyFilter() { + return apiKeyFilter; + } + + public Boolean getShowPermissions() { + return showPermissions; + } + + public boolean getListAll() { + return listAll; + } + + public long getEntityOwnerId() { + if (getKeyId() != null) { + ApiKeyPair keypair = apiKeyPairService.findById(getKeyId()); + if (keypair != null) { + return keypair.getAccountId(); + } + } else if (getUserId() != null) { + UserAccount userAccount = _accountService.getUserAccountById(getUserId()); + if (userAccount != null) { + return userAccount.getAccountId(); + } + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public void execute() { + ListResponse finalResponse = _accountService.listKeys(this); + finalResponse.setObjectName("userkeys"); + finalResponse.setResponseName(getCommandName()); + setResponseObject(finalResponse); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java index a160486c51c4..aab20f108f9e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java @@ -116,7 +116,7 @@ public void execute() { Preconditions.checkNotNull(getId(),"I have to have an user to move!"); Preconditions.checkState(ObjectUtils.anyNotNull(getAccountId(),getAccountName()),"provide either an account name or an account id!"); - CallContext.current().setEventDetails("UserId: " + getId()); + CallContext.current().setEventDetails("User ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _regionService.moveUser(this); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java deleted file mode 100644 index 61f47e2799c6..000000000000 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeyCmd.java +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. -package org.apache.cloudstack.api.command.admin.user; - -import org.apache.cloudstack.api.ApiCommandResourceType; - -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.response.RegisterUserKeyResponse; -import org.apache.cloudstack.api.response.UserResponse; - -import com.cloud.user.Account; -import com.cloud.user.User; - -@APICommand(name = "registerUserKeys", - responseObject = RegisterUserKeyResponse.class, - description = "This command allows a user to register for the developer API, returning a secret key and an API key. This request is made through the integration API port, so it is a privileged command and must be made on behalf of a user. It is up to the implementer just how the username and password are entered, and then how that translates to an integration API request. Both secret key and API key should be returned to the user", - requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class RegisterUserKeyCmd extends BaseCmd { - - - ///////////////////////////////////////////////////// - //////////////// API parameters ///////////////////// - ///////////////////////////////////////////////////// - - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User id") - private Long id; - - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// - - @Override - public long getEntityOwnerId() { - User user = _entityMgr.findById(User.class, getId()); - if (user != null) { - return user.getAccountId(); - } - - return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked - } - - @Override - public Long getApiResourceId() { - return id; - } - - @Override - public ApiCommandResourceType getApiResourceType() { - return ApiCommandResourceType.User; - } - - @Override - public void execute() { - String[] keys = _accountService.createApiKeyAndSecretKey(this); - RegisterUserKeyResponse response = new RegisterUserKeyResponse(); - if (keys != null) { - response.setApiKey(keys[0]); - response.setSecretKey(keys[1]); - } - response.setObjectName("userkeys"); - response.setResponseName(getCommandName()); - this.setResponseObject(response); - } -} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java new file mode 100644 index 000000000000..28c79517f4b9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -0,0 +1,212 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.user; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.user.User; +import org.apache.cloudstack.acl.Rule; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.commons.lang3.StringUtils; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; +import org.apache.cloudstack.api.response.UserResponse; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@APICommand(name = "registerUserKeys", + responseObject = ApiKeyPairResponse.class, + description = "Registers an API key pair (API and secret keys) for a user.", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) +public class RegisterUserKeysCmd extends BaseAsyncCmd { + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "ID of the user.") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.", length = 1024) + private String description; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API key pair. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Expiration date of the API key pair. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.RULES, type = CommandType.MAP, description = "The rules of the API key pair. If no rules are informed, " + + "defaults to allowing all account permissions. Otherwise, only the explicitly informed permissions for the key pair will be " + + "considered. Lower indexed rules take precedence over higher. Thus, in the following example: " + + "\"rules[0].rule=deleteUserKeys rules[0].permission=deny rules[1].rule=*UserKey* rules[1].permission=allow\", all rules matching " + + "the expression \"*UserKeys*\" will be allowed, except for \"deleteUserKeys\".") + private Map rules; + + public void setUserId(Long userId) { + this.id = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setRules(Map rules) { + this.rules = rules; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + public List> getRules() { + List> rulesDetails = new ArrayList<>(); + + if (rules == null) { + return rulesDetails; + } + + for (Object ruleObject : rules.values()) { + HashMap detail = (HashMap) ruleObject; + Map ruleDetails = new HashMap<>(); + + String rule = detail.get(ApiConstants.RULE); + if (StringUtils.isEmpty(rule)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty rule has been provided in the [rules] parameter."); + } + + String permission = detail.get(ApiConstants.PERMISSION); + if (StringUtils.isEmpty(permission)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Rule [%s] has no permission associated with it," + + " please specify if it is either [allow] or [deny].", rule)); + } + ruleDetails.put(ApiConstants.RULE, new Rule(rule)); + ruleDetails.put(ApiConstants.PERMISSION, roleService.getRolePermission(permission)); + + String description = detail.get(ApiConstants.DESCRIPTION); + if (StringUtils.isNotEmpty(description)) { + if (description.length() > 255) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Rule description cannot be longer than 255 characters."); + } + ruleDetails.put(ApiConstants.DESCRIPTION, description); + } + + rulesDetails.add(ruleDetails); + } + return rulesDetails; + } + + @Override + public long getEntityOwnerId() { + User user = _entityMgr.findById(User.class, getUserId()); + List accessibleUsers = _queryService.searchForAccessibleUsers(); + if (user != null && accessibleUsers.stream().anyMatch(u -> u == user.getId())) { + return user.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getUserId() { + return id; + } + + @Override + public Long getApiResourceId() { + User user = _entityMgr.findById(User.class, getUserId()); + if (user != null) { + return user.getId(); + } + return null; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public void execute() { + ApiKeyPair apiKeyPair = _accountService.createApiKeyAndSecretKey(this); + ApiKeyPairResponse response = new ApiKeyPairResponse(); + if (apiKeyPair != null) { + response = _responseGenerator.createKeyPairResponse(apiKeyPair); + } + response.setObjectName("userkeys"); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY; + } + + @Override + public String getEventDescription() { + String userUuid = getResourceUuid(ApiConstants.ID); + return String.format("Registering API keypair for user [%s].", userUuid == null ? id : userUuid); + } + + @Override + public String getSyncObjType() { + return BaseAsyncCmd.user; + } + + @Override + public Long getSyncObjId() { + return getUserId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index cc0b2e4954ce..3f5ce2415022 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.region.RegionService; +import org.apache.commons.lang.BooleanUtils; import com.cloud.user.Account; import com.cloud.user.User; @@ -38,13 +39,15 @@ requestHasSensitiveInfo = true, responseHasSensitiveInfo = true) public class UpdateUserCmd extends BaseCmd { + @Inject + private RegionService _regionService; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, description = "The API key for the user. Must be specified with userSecretKey") - private String apiKey; + @Parameter(name = ApiConstants.USER_API_KEY, type = CommandType.STRING, description = "Updates the latest API key of the user. Must be specified with usersecretkey") + private String userApiKey; @Parameter(name = ApiConstants.EMAIL, type = CommandType.STRING, description = "Email") private String email; @@ -67,8 +70,8 @@ public class UpdateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.CURRENT_PASSWORD, type = CommandType.STRING, description = "Current password that was being used by the user. You must inform the current password when updating the password.", acceptedOnAdminPort = false) private String currentPassword; - @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "The secret key for the user. Must be specified with userApiKey") - private String secretKey; + @Parameter(name = ApiConstants.USER_SECRET_KEY, type = CommandType.STRING, description = "Updates the latest secret key of the user. Must be specified with userapikey.") + private String userSecretKey; @Parameter(name = ApiConstants.API_KEY_ACCESS, type = CommandType.STRING, description = "Determines if Api key access for this user is enabled, disabled or inherits the value from its parent, the owning account", since = "4.20.1.0", authorized = {RoleType.Admin}) private String apiKeyAccess; @@ -85,15 +88,18 @@ public class UpdateUserCmd extends BaseCmd { "This parameter is only used to mandate 2FA, not to disable 2FA", since = "4.18.0.0") private Boolean mandate2FA; - @Inject - private RegionService _regionService; + @Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED, + type = CommandType.BOOLEAN, + description = "Provide true to mandate the User to reset password on next login.", + since = "4.23.0") + private Boolean passwordChangeRequired; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// public String getApiKey() { - return apiKey; + return userApiKey; } public String getEmail() { @@ -121,7 +127,7 @@ public String getCurrentPassword() { } public String getSecretKey() { - return secretKey; + return userSecretKey; } public String getApiKeyAccess() { @@ -156,7 +162,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("UserId: " + getId()); + CallContext.current().setEventDetails("User ID: " + getResourceUuid(ApiConstants.ID)); UserAccount user = _regionService.updateUser(this); if (user != null) { @@ -193,4 +199,8 @@ public Long getApiResourceId() { public ApiCommandResourceType getApiResourceType() { return ApiCommandResourceType.User; } + + public Boolean isPasswordChangeRequired() { + return BooleanUtils.isTrue(passwordChangeRequired); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java index e11d20d06466..50ff97ac676f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java @@ -85,6 +85,9 @@ public class AssignVMCmd extends BaseCmd { "In case no security groups are provided the Instance is part of the default security group.") private List securityGroupIdList; + // Internal flag to allow assignment without adding a network + private boolean skipNetwork = false; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -113,6 +116,34 @@ public List getSecurityGroupIdList() { return securityGroupIdList; } + public boolean isSkipNetwork() { + return skipNetwork; + } + + ///////////////////////////////////////////////////// + /////////////////// Setters ///////////////////////// + ///////////////////////////////////////////////////// + + public void setVirtualMachineId(Long virtualMachineId) { + this.virtualMachineId = virtualMachineId; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + public void setSkipNetwork(boolean skipNetwork) { + this.skipNetwork = skipNetwork; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java index d95f17ef304c..280e4a8bab77 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/CreateVMFromBackupCmdByAdmin.java @@ -45,6 +45,8 @@ public class CreateVMFromBackupCmdByAdmin extends CreateVMFromBackupCmd implemen @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "destination Cluster ID to deploy the VM to - parameter available for root admin only", since = "4.21") private Long clusterId; + private String instanceType; + public Long getPodId() { return podId; } @@ -52,4 +54,17 @@ public Long getPodId() { public Long getClusterId() { return clusterId; } + + @Override + public String getInstanceType() { + return instanceType; + } + + public CreateVMFromBackupCmdByAdmin(){} + + public CreateVMFromBackupCmdByAdmin(String hypervisor, String instanceType) { + this.displayVm = false; + this.hypervisor = hypervisor; + this.instanceType = instanceType; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java index e64c8b3f46c6..b7c3b2ba41a6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java @@ -41,6 +41,19 @@ public class DeployVMCmdByAdmin extends DeployVMCmd implements AdminCmd { @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "Destination Cluster ID to deploy the Instance to - parameter available for root admin only", since = "4.13") private Long clusterId; + @Parameter(name = ApiConstants.BLANK_INSTANCE, + type = CommandType.BOOLEAN, + description = "Whether to create a blank instance without storage and network", + since = "4.23.0") + private Boolean blankInstance; + + // Internal flag to allow deploying instance with a given type + private String instanceType; + + ///////////////////////////////////////////////////// + ////////////////// Getters ////////////////////////// + ///////////////////////////////////////////////////// + public Long getPodId() { return podId; } @@ -48,4 +61,37 @@ public Long getPodId() { public Long getClusterId() { return clusterId; } + + @Override + public boolean isBlankInstance() { + return Boolean.TRUE.equals(blankInstance); + } + + @Override + public String getInstanceType() { + if (!isBlankInstance()) { + return null; + } + return instanceType; + } + + ///////////////////////////////////////////////////// + ////////////////// Setters ////////////////////////// + ///////////////////////////////////////////////////// + + public void setClusterId(Long clusterId) { + this.clusterId = clusterId; + } + + public void setBlankInstance(boolean blankInstance) { + this.blankInstance = blankInstance; + } + + public void setInstanceType(String instanceType) { + this.instanceType = instanceType; + } + + public void setCustomId(String customId) { + this.customId = customId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java index 93d4b610b900..cbe6494d4004 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.command.admin.vm; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.command.admin.AdminCmd; import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; @@ -27,4 +29,20 @@ @APICommand(name = "destroyVirtualMachine", description = "Destroys an Instance. Once destroyed, only the administrator can recover it.", responseObject = UserVmResponse.class, responseView = ResponseView.Full, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class DestroyVMCmdByAdmin extends DestroyVMCmd implements AdminCmd {} +public class DestroyVMCmdByAdmin extends DestroyVMCmd implements AdminCmd { + + @Parameter( name = ApiConstants.FORCED, + type = CommandType.BOOLEAN, + description = "Force destroy the Instance", + since = "4.23.0") + Boolean forced; + + @Override + public boolean isForced() { + return Boolean.TRUE.equals(forced); + } + + public void setForced(Boolean forced) { + this.forced = forced; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ExpungeVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ExpungeVMCmd.java index 74ebab59de48..2d9c5f93383c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ExpungeVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ExpungeVMCmd.java @@ -81,7 +81,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Expunging Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Expunging Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -96,7 +96,7 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, ConcurrentOperationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); try { UserVm result = _userVmService.expungeVm(this.getId()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java index ad7e8a48b252..3284dbafe7ca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java @@ -271,8 +271,7 @@ public String getEventType() { @Override public String getEventDescription() { - String vmName = this.name; - return String.format("Importing unmanaged Instance: %s", vmName); + return "Importing unmanaged Instance: " + name; } public boolean isForced() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java index f7940460d6cd..db7dcc3fb44f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; @@ -171,6 +172,21 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { description = "(only for importing VMs from VMware to KVM) optional - if true, forces virt-v2v conversions to write directly on the provided storage pool (avoid using temporary conversion pool).") private Boolean forceConvertToPool; + @Parameter(name = ApiConstants.OS_ID, + type = CommandType.UUID, + entityType = GuestOSResponse.class, + since = "4.22.1", + description = "(only for importing VMs from VMware to KVM) optional - the ID of the guest OS for the imported VM.") + private Long guestOsId; + + @Parameter(name = ApiConstants.USE_VDDK, + type = CommandType.BOOLEAN, + since = "4.22.1", + description = "(only for importing VMs from VMware to KVM) optional - if true, uses VDDK on the KVM conversion host for converting the VM. " + + "This parameter is mutually exclusive with " + ApiConstants.FORCE_MS_TO_IMPORT_VM_FILES + ".") + private Boolean useVddk; + + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -247,6 +263,10 @@ public Long getStoragePoolId() { return storagePoolId; } + public boolean getUseVddk() { + return BooleanUtils.toBooleanDefaultIfNull(useVddk, true); + } + public String getTmpPath() { return tmpPath; } @@ -268,6 +288,10 @@ public boolean getForceConvertToPool() { return BooleanUtils.toBooleanDefaultIfNull(forceConvertToPool, false); } + public Long getGuestOsId() { + return guestOsId; + } + @Override public String getEventDescription() { String vmName = getName(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java index f6335307a382..467b92d415d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java @@ -124,15 +124,15 @@ public String getEventType() { @Override public String getEventDescription() { - String eventDescription; + String description = "Attempting to migrate Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); + if (getHostId() != null) { - eventDescription = String.format("Attempting to migrate Instance id: %s to host Id: %s", getVirtualMachineId(), getHostId()); + description += " to host with ID: " +getResourceUuid(ApiConstants.HOST_ID); } else if (getStoragePoolId() != null) { - eventDescription = String.format("Attempting to migrate Instance id: %s to storage pool Id: %s", getVirtualMachineId(), getStoragePoolId()); - } else { - eventDescription = String.format("Attempting to migrate Instance id: %s", getVirtualMachineId()); + description = " to storage pool with ID: " + getResourceUuid(ApiConstants.STORAGE_ID); } - return eventDescription; + + return description; } @Override @@ -158,7 +158,7 @@ public void execute() { if (destStoragePool == null) { throw new InvalidParameterValueException("Unable to find the storage pool to migrate the Instance"); } - CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStoragePoolId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to storage pool with ID: " + getResourceUuid(ApiConstants.STORAGE_ID)); } else if (getHostId() != null) { destinationHost = _resourceService.getHost(getHostId()); if (destinationHost == null) { @@ -167,7 +167,7 @@ public void execute() { if (destinationHost.getType() != Host.Type.Routing) { throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the Instance, please specify another one"); } - CallContext.current().setEventDetails("Instance Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); + CallContext.current().setEventDetails("Instance Id: " + getVirtualMachineId() + " to host with ID: " + getResourceUuid(ApiConstants.HOST_ID)); } else if (! isAutoSelect()) { throw new InvalidParameterValueException("Please specify a host or storage as destination, or pass 'autoselect=true' to automatically select a destination host which do not require storage migration"); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java index 0142f6fc81ab..ad298513ac0b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVirtualMachineWithVolumeCmd.java @@ -135,7 +135,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Attempting to migrate Instance Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVirtualMachineId()) + " to host Id: " + this._uuidMgr.getUuid(Host.class, getHostId()); + return "Attempting to migrate Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java index b1d17cf02f62..2c9f09dcd626 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/UnmanageVMInstanceCmd.java @@ -97,7 +97,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Unmanaging Instance. Instance ID = " + vmId; + return "Unmanaging Instance with ID: " + getResourceUuid(ApiConstants.ID); } public Long getHostId() { @@ -121,7 +121,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { UnmanageVMInstanceResponse response = new UnmanageVMInstanceResponse(); try { - CallContext.current().setEventDetails("VM ID = " + vmId); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); Pair result = unmanagedVMsManager.unmanageVMInstance(vmId, hostId, isForced()); if (result.first()) { response.setSuccess(true); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java index 0840b4ce6f99..de90fee102de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/DestroyVolumeCmdByAdmin.java @@ -40,7 +40,7 @@ public class DestroyVolumeCmdByAdmin extends DestroyVolumeCmd implements AdminCm @Override public void execute() { CallContext.current().setEventDetails("Volume Id: " + getId()); - Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false, null); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java index e276c8a00b65..b7c084ee6e78 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/RecoverVolumeCmdByAdmin.java @@ -19,6 +19,7 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; @@ -38,7 +39,7 @@ public class RecoverVolumeCmdByAdmin extends RecoverVolumeCmd implements AdminCm @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + getId()); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); Volume result = _volumeService.recoverVolume(getId()); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java index dcc8b2af8d74..ac573dd4ecb9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmd.java @@ -81,7 +81,7 @@ public String getEventType() { @Override public String getEventDescription() { - return String.format("Unmanaging Volume with ID %s", volumeId); + return "Unmanaging Volume with ID: " + getResourceUuid(ApiConstants.ID); } ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java new file mode 100644 index 000000000000..2148ff5c2d4f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CloneVPCOfferingCmd.java @@ -0,0 +1,109 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.vpc; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.network.vpc.VpcOffering; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.VpcOfferingResponse; + +import java.util.List; + +@APICommand(name = "cloneVPCOffering", + description = "Clones an existing VPC offering. All parameters are copied from the source offering unless explicitly overridden. " + + "Use 'addServices' and 'dropServices' to modify the service list without respecifying everything.", + responseObject = VpcOfferingResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0") +public class CloneVPCOfferingCmd extends CreateVPCOfferingCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SOURCE_OFFERING_ID, + type = BaseCmd.CommandType.UUID, + entityType = VpcOfferingResponse.class, + required = true, + description = "The ID of the source VPC offering to clone from") + private Long sourceOfferingId; + + @Parameter(name = "addservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to add to the cloned offering (in addition to source offering services). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List addServices; + + @Parameter(name = "dropservices", + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Services to remove from the cloned offering (that exist in source offering). " + + "If specified along with 'supportedservices', this parameter is ignored.") + private List dropServices; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getSourceOfferingId() { + return sourceOfferingId; + } + + public List getAddServices() { + return addServices; + } + + public List getDropServices() { + return dropServices; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void create() throws ResourceAllocationException { + // Set a temporary entity ID (source offering ID) to prevent NullPointerException + // in ApiServer.queueCommand(). This will be updated in execute() with the actual + // cloned offering ID. + if (sourceOfferingId != null) { + setEntityId(sourceOfferingId); + } + } + + @Override + public void execute() { + VpcOffering result = _vpcProvSvc.cloneVPCOffering(this); + if (result != null) { + setEntityId(result.getId()); + setEntityUuid(result.getUuid()); + + VpcOfferingResponse response = _responseGenerator.createVpcOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to clone VPC offering"); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java index 6b425bc10d21..2b934a60da7a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java @@ -28,7 +28,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.cloud.exception.InvalidParameterValueException; import com.cloud.network.Network; import com.cloud.network.VirtualRouterProvider; import com.cloud.offering.NetworkOffering; @@ -161,6 +160,12 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { description = "the routing mode for the VPC offering. Supported types are: Static or Dynamic.") private String routingMode; + @Parameter(name = ApiConstants.CONSERVE_MODE, type = CommandType.BOOLEAN, + since = "4.23.0", + description = "True if the VPC offering is IP conserve mode enabled, allowing public IPs to be used across multiple VPC tiers. Default value is false") + private Boolean conserveMode; + + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -179,9 +184,7 @@ public boolean isExternalNetworkProvider() { } public List getSupportedServices() { - if (!isExternalNetworkProvider() && CollectionUtils.isEmpty(supportedServices)) { - throw new InvalidParameterValueException("Supported services needs to be provided"); - } + // For external network providers, auto-populate services based on network mode if (isExternalNetworkProvider()) { supportedServices = new ArrayList<>(List.of( Dhcp.getName(), @@ -311,6 +314,10 @@ public String getRoutingMode() { return routingMode; } + public boolean isConserveMode() { + return BooleanUtils.toBoolean(conserveMode); + } + @Override public void create() throws ResourceAllocationException { VpcOffering vpcOff = _vpcProvSvc.createVpcOffering(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeletePrivateGatewayCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeletePrivateGatewayCmd.java index cd88d81da675..a6b0538062b1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeletePrivateGatewayCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeletePrivateGatewayCmd.java @@ -66,7 +66,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting private gateway id=" + id); + return "Deleting private gateway with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -76,7 +76,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, ConcurrentOperationException { - CallContext.current().setEventDetails("Network ACL Id: " + id); + CallContext.current().setEventDetails("Network ACL ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _vpcService.deleteVpcPrivateGateway(id); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeleteVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeleteVPCOfferingCmd.java index b322a6d55890..f579eeb87e4d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeleteVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/DeleteVPCOfferingCmd.java @@ -76,7 +76,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting VPC offering id=" + getId(); + return "Deleting VPC offering with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java index d4565cbada26..97f30f6fa2ef 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java @@ -127,7 +127,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating VPC offering id=" + getId(); + return "Updating VPC offering with ID:" + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/DeleteZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/DeleteZoneCmd.java index 565baab6e4c6..28d14a318753 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/DeleteZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/DeleteZoneCmd.java @@ -60,7 +60,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Zone Id: " + getId()); + CallContext.current().setEventDetails("Zone ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _configService.deleteZone(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/MarkDefaultZoneForAccountCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/MarkDefaultZoneForAccountCmd.java index 5d3f5dcd47fa..42040290a411 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/MarkDefaultZoneForAccountCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/MarkDefaultZoneForAccountCmd.java @@ -95,7 +95,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Marking account with the default zone: " + getDefaultZoneId(); + return "Marking zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID) + " as default for account " + getResourceUuid(ApiConstants.ACCOUNT) + " in domain: " + getResourceUuid(ApiConstants.DOMAIN_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/UpdateZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/UpdateZoneCmd.java index 3e7fffb5fafc..888ee6603ddf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/UpdateZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/UpdateZoneCmd.java @@ -178,7 +178,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Zone Id: " + getId()); + CallContext.current().setEventDetails("Zone ID: " + getResourceUuid(ApiConstants.ID)); DataCenter result = _configService.editZone(this); if (result != null) { ZoneResponse response = _responseGenerator.createZoneResponse(ResponseView.Full, result, false, false); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java index 93021487040b..63a0a6ca51e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddAccountToProjectCmd.java @@ -18,6 +18,7 @@ import java.util.List; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.BaseCmd; @@ -106,12 +107,12 @@ public ProjectAccount.Role getRoleType() { ///////////////////////////////////////////////////// @Override - public void execute() { + public void execute() throws ResourceAllocationException { if (accountName == null && email == null) { throw new InvalidParameterValueException("Either accountName or email is required"); } - CallContext.current().setEventDetails("Project ID: " + projectId + "; accountName " + accountName); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.PROJECT_ID) + "; accountName " + accountName); boolean result = _projectService.addAccountToProject(getProjectId(), getAccountName(), getEmail(), getProjectRoleId(), getRoleType()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -146,10 +147,12 @@ public String getEventType() { @Override public String getEventDescription() { + String projectUuid = getResourceUuid(ApiConstants.PROJECT_ID); + if (accountName != null) { - return "Adding Account " + getAccountName() + " to project: " + getProjectId(); + return "Adding account " + getAccountName() + " to project: " + projectUuid; } else { - return "Sending invitation to email " + email + " to join project: " + getProjectId(); + return "Sending invitation to email " + email + " to join project: " + projectUuid; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java index 9bdc85bc5c71..683522039b17 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/AddUserToProjectCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.account; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiArgValidator; @@ -103,7 +104,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Adding User " + getUsername() + " to Project: " + getProjectId(); + return "Adding User " + getUsername() + " to Project: " + getResourceUuid(ApiConstants.PROJECT_ID); } ///////////////////////////////////////////////////// @@ -111,7 +112,7 @@ public String getEventDescription() { ///////////////////////////////////////////////////// @Override - public void execute() { + public void execute() throws ResourceAllocationException { validateInput(); boolean result = _projectService.addUserToProject(getProjectId(), getUsername(), getEmail(), getProjectRoleId(), getRoleType()); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteAccountFromProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteAccountFromProjectCmd.java index 510f97ff7437..74e6f2c804c7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteAccountFromProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteAccountFromProjectCmd.java @@ -70,7 +70,7 @@ public String getAccountName() { @Override public void execute() { - CallContext.current().setEventDetails("Project ID: " + projectId + "; accountName " + accountName); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.PROJECT_ID) + "; accountName " + accountName); boolean result = _projectService.deleteAccountFromProject(projectId, accountName); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -103,7 +103,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Removing Account " + accountName + " from project: " + projectId; + return "Removing Account " + accountName + " from project: " + getResourceUuid(ApiConstants.PROJECT_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteUserFromProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteUserFromProjectCmd.java index 5db604fe05c3..2677b206bdc1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteUserFromProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/account/DeleteUserFromProjectCmd.java @@ -78,7 +78,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Removing User " + userId + " from project: " + projectId; + return "Removing User " + getResourceUuid(ApiConstants.USER_ID) + " from project: " + getResourceUuid(ApiConstants.PROJECT_ID); } @Override @@ -107,7 +107,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - CallContext.current().setEventDetails("Project ID: " + projectId + "; User ID: " + userId); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.PROJECT_ID) + "; User ID: " + getResourceUuid(ApiConstants.USER_ID)); boolean result = _projectService.deleteUserFromProject(getProjectId(), getUserId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/AssociateIPAddrCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/AssociateIPAddrCmd.java index c18d08299c3a..a62f9f316606 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/address/AssociateIPAddrCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/AssociateIPAddrCmd.java @@ -334,7 +334,7 @@ public void create() throws ResourceAllocationException { @Override public void execute() throws ResourceUnavailableException, ResourceAllocationException, ConcurrentOperationException, InsufficientCapacityException { - CallContext.current().setEventDetails("IP ID: " + getEntityId()); + CallContext.current().setEventDetails("IP address ID: " + getEntityUuid()); IpAddress result = null; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/DisassociateIPAddrCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/DisassociateIPAddrCmd.java index 531f9bb86495..835a2a69e3c9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/address/DisassociateIPAddrCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/DisassociateIPAddrCmd.java @@ -82,7 +82,7 @@ public Long getIpAddressId() { @Override public void execute() throws InsufficientAddressCapacityException { Long ipAddressId = getIpAddressId(); - CallContext.current().setEventDetails("IP ID: " + ipAddressId); + CallContext.current().setEventDetails("IP address ID: " + getResourceUuid(ApiConstants.ID)); boolean result = false; if (!isPortable()) { result = _networkService.releaseIpAddress(ipAddressId); @@ -108,7 +108,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Disassociating IP address with ID=" + id); + return ("Disassociating IP address with ID:" + getResourceUuid(ApiConstants.ID)); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ReleaseIPAddrCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ReleaseIPAddrCmd.java index 0d2ff89930d5..2fe94b29346d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ReleaseIPAddrCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ReleaseIPAddrCmd.java @@ -73,7 +73,7 @@ public long getEntityOwnerId() { @Override public void execute() throws InsufficientAddressCapacityException { - CallContext.current().setEventDetails("IP ID: " + getIpAddressId()); + CallContext.current().setEventDetails("IP address ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkService.releaseReservedIpAddress(getIpAddressId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/DeleteAffinityGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/DeleteAffinityGroupCmd.java index 6eb75c7c1838..4eace28f5555 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/DeleteAffinityGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/DeleteAffinityGroupCmd.java @@ -123,7 +123,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting Affinity Group"; + return "Deleting Affinity Group with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/UpdateVMAffinityGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/UpdateVMAffinityGroupCmd.java index e522747780f2..7a7e3ee298a9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/UpdateVMAffinityGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/affinitygroup/UpdateVMAffinityGroupCmd.java @@ -141,7 +141,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE throw new InvalidParameterValueException("affinitygroupids parameter or affinitygroupnames parameter must be given"); } - CallContext.current().setEventDetails("VM ID: " + getId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result = _affinityGroupService.updateVMAffinityGroups(getId(), getAffinityGroupIdList()); ArrayList dc = new ArrayList(); dc.add(VMDetails.valueOf("affgrp")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScalePolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScalePolicyCmd.java index e160a1a90103..f4bfcf4f135d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScalePolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScalePolicyCmd.java @@ -160,7 +160,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "creating AutoScale Policy"; + return "Creating AutoScale Policy"; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmGroupCmd.java index 66a1a56fbe2c..7c04a4c9d53b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateAutoScaleVmGroupCmd.java @@ -185,7 +185,7 @@ public String getCreateEventDescription() { @Override public String getEventDescription() { - return "Configuring AutoScale Instance Group. Instance Group Id: " + getEntityId(); + return "Configuring AutoScale Instance Group with ID: " + getEntityId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateConditionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateConditionCmd.java index 2bb101de5593..61745ccda7d1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateConditionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/CreateConditionCmd.java @@ -127,7 +127,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public String getEventDescription() { - return "creating a condition"; + return "Creating AutoScale condition"; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScalePolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScalePolicyCmd.java index fac7ffe37d30..45315cc744ca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScalePolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScalePolicyCmd.java @@ -79,12 +79,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting AutoScale Policy: " + getId(); + return "Deleting AutoScale Policy with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Policy Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Policy ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _autoScaleService.deleteAutoScalePolicy(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmGroupCmd.java index c2dd6d5424d9..99d6d903ba45 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmGroupCmd.java @@ -89,12 +89,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting autoscale Instance group: " + getId(); + return "Deleting AutoScale Instance group with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Instance Group Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Instance Group ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _autoScaleService.deleteAutoScaleVmGroup(id, getCleanup()); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmProfileCmd.java index 9e2f63deda2e..bf1e8ecb1677 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteAutoScaleVmProfileCmd.java @@ -79,12 +79,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting autoscale Instance profile: " + getId(); + return "Deleting AutoScale Instance profile with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Instance Profile Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Instance Profile ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _autoScaleService.deleteAutoScaleVmProfile(id); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteConditionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteConditionCmd.java index 2eeed8b49da1..38b77c1553f5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteConditionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DeleteConditionCmd.java @@ -100,6 +100,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting a condition."; + return "Deleting AutoScale condition with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DisableAutoScaleVmGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DisableAutoScaleVmGroupCmd.java index 814f35c9f70a..316fefd62deb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DisableAutoScaleVmGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/DisableAutoScaleVmGroupCmd.java @@ -96,7 +96,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Disabling AutoScale Instance Group. Instance Group Id: " + getId(); + return "Disabling AutoScale Instance Group with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/EnableAutoScaleVmGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/EnableAutoScaleVmGroupCmd.java index 962c5af0e2c1..8aea4690425e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/EnableAutoScaleVmGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/EnableAutoScaleVmGroupCmd.java @@ -96,7 +96,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Enabling AutoScale Instance Group. Instance Group Id: " + getId(); + return "Enabling AutoScale Instance Group with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScalePolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScalePolicyCmd.java index 368f9c01fe18..05af6a53a5d3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScalePolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScalePolicyCmd.java @@ -77,7 +77,7 @@ public class UpdateAutoScalePolicyCmd extends BaseAsyncCmd { @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Policy Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Policy ID: " + getResourceUuid(ApiConstants.ID)); AutoScalePolicy result = _autoScaleService.updateAutoScalePolicy(this); if (result != null) { AutoScalePolicyResponse response = _responseGenerator.createAutoScalePolicyResponse(result); @@ -130,7 +130,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating Auto Scale Policy. Policy Id: " + getId(); + return "Updating AutoScale Policy with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmGroupCmd.java index 128a1368c529..3e13ce10bfb4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmGroupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmGroupCmd.java @@ -97,7 +97,7 @@ public class UpdateAutoScaleVmGroupCmd extends BaseAsyncCustomIdCmd { @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Instance Group Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Instance Group ID: " + getResourceUuid(ApiConstants.ID)); AutoScaleVmGroup result = _autoScaleService.updateAutoScaleVmGroup(this); if (result != null) { AutoScaleVmGroupResponse response = _responseGenerator.createAutoScaleVmGroupResponse(result); @@ -151,7 +151,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating AutoScale Instance Group. Instance Group Id: " + getId(); + return "Updating AutoScale Instance Group with ID: " + getId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java index 5192f0382db6..9495989df189 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateAutoScaleVmProfileCmd.java @@ -124,7 +124,7 @@ public class UpdateAutoScaleVmProfileCmd extends BaseAsyncCustomIdCmd { @Override public void execute() { - CallContext.current().setEventDetails("AutoScale Policy Id: " + getId()); + CallContext.current().setEventDetails("AutoScale Policy ID: " + getResourceUuid(ApiConstants.ID)); AutoScaleVmProfile result = _autoScaleService.updateAutoScaleVmProfile(this); if (result != null) { AutoScaleVmProfileResponse response = _responseGenerator.createAutoScaleVmProfileResponse(result); @@ -190,7 +190,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating AutoScale Instance Profile. Instance Profile Id: " + getId(); + return "Updating AutoScale Instance Profile with ID: " + getId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateConditionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateConditionCmd.java index a386db478438..43de212da7b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateConditionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/autoscale/UpdateConditionCmd.java @@ -110,6 +110,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating a condition."; + return "Updating Instance AutoScale condition with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java index e8914e45c429..29b1e740b67e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java @@ -21,6 +21,7 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseAsyncCmd; @@ -102,6 +103,16 @@ public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + @Override + public Long getApiResourceId() { + return vmId; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.VirtualMachine; + } + @Override public String getEventType() { return EventTypes.EVENT_VM_BACKUP_OFFERING_ASSIGN; @@ -109,6 +120,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Assigning Instance to backup offering ID: " + offeringId; + return "Assigning Instance with ID " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to backup offering with ID: " + getResourceUuid(ApiConstants.BACKUP_OFFERING_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java index df417ef3a60f..5b7bd518df0e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java @@ -19,7 +19,6 @@ import javax.inject.Inject; -import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -76,12 +75,18 @@ public class CreateBackupCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.QUIESCE_VM, type = CommandType.BOOLEAN, required = false, - description = "Quiesce the instance before checkpointing the disks for backup. Applicable only to NAS backup provider. " + + description = "Quiesce the instance before checkpointing the disks for backup. Applicable only to NAS and KBOSS backup providers. " + "The filesystem is frozen before the backup starts and thawed immediately after. " + "Requires the instance to have the QEMU Guest Agent installed and running.", since = "4.21.0") private Boolean quiesceVM; + @Parameter(name = ApiConstants.ISOLATED, + type = CommandType.BOOLEAN, + description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS, + since = "4.23.0") + private boolean isolated; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -102,6 +107,10 @@ public Boolean getQuiesceVM() { return quiesceVM; } + public boolean isIsolated() { + return isolated; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -124,7 +133,12 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE @Override public ApiCommandResourceType getApiResourceType() { - return ApiCommandResourceType.Backup; + return ApiCommandResourceType.VirtualMachine; + } + + @Override + public Long getApiResourceId() { + return vmId; } @Override @@ -139,8 +153,7 @@ public String getEventType() { @Override public String getEventDescription() { - String vmUuid = _uuidMgr.getUuid(VirtualMachine.class, getVmId()); - return "Creating backup for Instance " + vmUuid; + return "Creating backup for Instance " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java new file mode 100644 index 000000000000..c5d29b615439 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupOfferingCmd.java @@ -0,0 +1,185 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; + + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = "createBackupOffering", description = "Creates a backup offering", responseObject = BackupOfferingResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class CreateBackupOfferingCmd extends BaseCmd { + + @Inject + protected BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Backup offering name.", required = true) + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = true, + description = "The description of the backup offering") + private String description; + + @Parameter(name = ApiConstants.COMPRESS, type = CommandType.BOOLEAN, description = "Whether the backups should be compressed or not.") + private Boolean compress; + + @Parameter(name = ApiConstants.VALIDATE, type = CommandType.BOOLEAN, description = "Whether the backups should be validated or not.") + private Boolean validate; + + @Parameter(name = ApiConstants.VALIDATION_STEPS, type = CommandType.STRING, description = "Which validation steps should be performed. Accepts a comma-separated list of " + + "steps. Accepted values are: wait_for_boot, screenshot and execute_command.") + private String validationSteps; + + @Parameter(name = ApiConstants.ALLOW_QUICK_RESTORE, type = CommandType.BOOLEAN, description = "Whether quick restore is enabled for the backups or not.") + private Boolean allowQuickRestore; + + @Parameter(name = ApiConstants.ALLOW_EXTRACT_FILE, type = CommandType.BOOLEAN, description = "Whether files may be extracted from backups or not.") + private Boolean allowExtractFile; + + @Parameter(name = ApiConstants.BACKUP_CHAIN_SIZE, type = CommandType.INTEGER, description = "Backup chain size for backups created with this offering.") + private Integer backupChainSize; + + @Parameter(name = ApiConstants.COMPRESSION_LIBRARY, type = CommandType.STRING, description = "Compression library, for offerings that support compression. Accepted values " + + "are zstd and zlib. By default, zstd is used for images that support it. If the image only supports zlib, it will be used regardless of this parameter.") + private String compressionLibrary; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "Restrict the backup offering to the Zone identified by this ID.", required = true) + private Long zoneId; + + @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = CommandType.BOOLEAN, + description = "Whether users are allowed to create ad-hoc backups and backup schedules when using this offering.", required = true) + private Boolean userDrivenBackups; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = DomainResponse.class, + description = "Restrict the backup offering to the Domains identified by these IDs.") + private List domainIds; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public boolean isCompress() { + return Boolean.TRUE.equals(compress); + } + + public boolean isValidate() { + return Boolean.TRUE.equals(validate); + } + + public boolean isAllowQuickRestore() { + return Boolean.TRUE.equals(allowQuickRestore); + } + + public boolean isAllowExtractFile() { + return Boolean.TRUE.equals(allowExtractFile); + } + + public Integer getBackupChainSize() { + return backupChainSize; + } + + public Backup.CompressionLibrary getCompressionLibrary() { + if (compressionLibrary == null) { + return null; + } + try { + return Backup.CompressionLibrary.valueOf(compressionLibrary); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException(String.format("Invalid compression library, accepted values are zstd and zlib, received [%s].", compressionLibrary)); + } + } + + public String getValidationSteps() { + if (validationSteps == null) { + return Backup.ValidationSteps.screenshot.name(); + } + StringBuilder sb = new StringBuilder(); + for (String step : validationSteps.strip().split(",")) { + try { + Backup.ValidationSteps enumStep = Backup.ValidationSteps.valueOf(step); + sb.append(enumStep.name()); + sb.append(","); + } catch (IllegalArgumentException ex) { + logger.error("Invalid validation step informed [{}].", step, ex); + throw new InvalidParameterValueException(String.format("Invalid validation step [%s] informed. Accepted values are: wait_for_boot, screenshot and execute_command.", step)); + } + } + sb.deleteCharAt(sb.lastIndexOf(",")); + return sb.toString(); + } + + public String getDescription() { + return description; + } + + public Long getZoneId() { + return zoneId; + } + + public List getDomainIds() { + return domainIds; + } + + public Boolean getUserDrivenBackups() { + return userDrivenBackups; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + BackupOffering offering = backupManager.createBackupOffering(this); + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(offering); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return 0; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java index 67ad7c71503f..812d78425e89 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java @@ -21,6 +21,7 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; @@ -81,12 +82,18 @@ public class CreateBackupScheduleCmd extends BaseCmd { @Parameter(name = ApiConstants.QUIESCE_VM, type = CommandType.BOOLEAN, required = false, - description = "Quiesce the instance before checkpointing the disks for backup. Applicable only to NAS backup provider. " + + description = "Quiesce the Instance before checkpointing the disks for backup. Applicable only to NAS and KBOSS backup providers. " + "The filesystem is frozen before the backup starts and thawed immediately after. " + "Requires the instance to have the QEMU Guest Agent installed and running.", since = "4.21.0") private Boolean quiesceVM; + @Parameter(name = ApiConstants.ISOLATED, + type = CommandType.BOOLEAN, + description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS, + since = "4.23.0") + private boolean isolated; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -115,6 +122,10 @@ public Boolean getQuiesceVM() { return quiesceVM; } + public boolean isIsolated() { + return isolated; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -139,4 +150,14 @@ public void execute() throws ServerApiException { public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public Long getApiResourceId() { + return vmId; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.VirtualMachine; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java index 8c32dac6c3ac..faaf1735e1e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java @@ -28,7 +28,6 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.SuccessResponse; -import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.backup.BackupManager; import org.apache.cloudstack.context.CallContext; import org.apache.commons.lang3.BooleanUtils; @@ -112,7 +111,6 @@ public String getEventType() { @Override public String getEventDescription() { - String backupUuid = _uuidMgr.getUuid(Backup.class, getId()); - return "Deleting backup ID " + backupUuid; + return "Deleting Backup with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java new file mode 100644 index 000000000000..997d0d24c6f0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DownloadValidationScreenshotCmd.java @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.backup; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupService; + +import javax.inject.Inject; + +@APICommand(name = "downloadValidationScreenshot", description = "Download validation screenshot of given backup.", + responseObject = ExtractResponse.class, since = "4.23.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class DownloadValidationScreenshotCmd extends BaseAsyncCmd { + + @Inject + private InternalBackupService internalBackupService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL + @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, required = true, + description = "ID of the backup.") + private Long backupId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getBackupId() { + return backupId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getEventType() { + return EventTypes.EVENT_SCREENSHOT_DOWNLOAD; + } + + @Override + public String getEventDescription() { + Backup backup = _entityMgr.findById(Backup.class, getBackupId()); + if (backup == null) { + throw new InvalidParameterValueException(String.format("Unable to find backup with ID [%s].", getBackupId())); + } + return "Downloading validation screenshot of backup " + backup.getUuid(); + } + + @Override + public void execute() { + ExtractResponse response = internalBackupService.downloadScreenshot(getBackupId()); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Backup backup = _entityMgr.findById(Backup.class, getBackupId()); + if (backup != null) { + return backup.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java new file mode 100644 index 000000000000..575df7ae0831 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/FinishBackupChainCmd.java @@ -0,0 +1,86 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.VirtualMachineResponse; +import org.apache.cloudstack.backup.InternalBackupService; + +import javax.inject.Inject; + +@APICommand(name = "finishBackupChain", description = "Finish the backup chain of VM. Currently only has effect on VMs with KBOSS backup offerings.", + responseObject = SuccessResponse.class, since = "4.23.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class FinishBackupChainCmd extends BaseCmd { + @Inject + private InternalBackupService internalBackupService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = VirtualMachineResponse.class, required = true, + description = "ID of the VM to finish the chain.") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + boolean result = internalBackupService.finishBackupChain(getVmId()); + SuccessResponse response = new SuccessResponse(); + response.setSuccess(result); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, getVmId()); + if (vm != null) { + return vm.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java new file mode 100644 index 000000000000..39ca444ae2ee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupServiceJobsCmd.java @@ -0,0 +1,105 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.backup; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupServiceJobResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ZoneResponse; + +@APICommand(name = "listBackupServiceJobs", description = "List backup service jobs", responseObject = BackupServiceJobResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}, since = "4.23.0") +public class ListBackupServiceJobsCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.LONG, entityType = BackupServiceJobResponse.class, description = "List only job with given ID.") + private Long id; + + @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, description = "List jobs for the given backup.") + private Long backupId; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "List jobs in the given host. When passing this parameter, only jobs that are currently executing will be returned.") + private Long hostId; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, description = "List jobs in the given zone.") + private Long zoneId; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "List jobs with the given type. Accepted values are StartCompression, FinalizeCompression and " + + "BackupValidation.") + private String type; + + @Parameter(name = ApiConstants.EXECUTING, type = CommandType.BOOLEAN, description = "List executing jobs.") + private Boolean executing; + + @Parameter(name = ApiConstants.SCHEDULED, type = CommandType.BOOLEAN, description = "List scheduled jobs.") + private Boolean scheduled; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Long getBackupId() { + return backupId; + } + + public Long getHostId() { + return hostId; + } + + public Long getZoneId() { + return zoneId; + } + + public String getType() { + return type; + } + + public boolean getExecuting() { + return Boolean.TRUE.equals(executing); + } + + public boolean getScheduled() { + return Boolean.TRUE.equals(scheduled); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, + NetworkRuleConflictException { + ListResponse response = _queryService.listBackupServiceJobs(this); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java index fb9c92f433e5..35cadd9f2e5e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java @@ -89,6 +89,12 @@ public class ListBackupsCmd extends BaseListProjectAndAccountResourcesCmd { description = "list backups by backup offering") private Long backupOfferingId; + @Parameter(name = ApiConstants.STATUS, + type = CommandType.STRING, + since = "4.23.0", + description = "list backups by status") + private String backupStatus; + @Parameter(name = ApiConstants.LIST_VM_DETAILS, type = CommandType.BOOLEAN, since = "4.21.0", @@ -119,6 +125,10 @@ public Long getZoneId() { return zoneId; } + public String getBackupStatus() { + return backupStatus; + } + public Boolean getListVmDetails() { return listVmDetails; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java index 81f11edb7d90..62c2f03d8807 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java @@ -21,6 +21,7 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseAsyncCmd; @@ -99,6 +100,16 @@ public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + @Override + public Long getApiResourceId() { + return vmId; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.VirtualMachine; + } + @Override public String getEventType() { return EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE; @@ -106,6 +117,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Removing Instance ID" + vmId + " from backup offering"; + return "Removing Instance with ID:" + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " from backup offering"; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java index 3d096c0bb388..9c67aa6a9f69 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java @@ -26,9 +26,9 @@ import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.BackupResponse; -import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.backup.BackupManager; import org.apache.cloudstack.context.CallContext; @@ -39,6 +39,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.BooleanUtils; @APICommand(name = "restoreBackup", description = "Restores an existing stopped or deleted Instance using an Instance backup", @@ -60,6 +61,14 @@ public class RestoreBackupCmd extends BaseAsyncCmd { description = "ID of the backup") private Long backupId; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, entityType = BackupResponse.class, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "If quickrestore is true, which host to start the VM on;" + + " otherwise, ignored. Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0", authorized = {RoleType.Admin}) + private Long hostId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -68,6 +77,14 @@ public Long getBackupId() { return backupId; } + public boolean isQuickRestore() { + return BooleanUtils.isTrue(quickRestore); + } + + public Long getHostId() { + return hostId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -75,7 +92,7 @@ public Long getBackupId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try { - boolean result = backupManager.restoreBackup(backupId); + boolean result = backupManager.restoreBackup(backupId, isQuickRestore(), getHostId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); response.setResponseName(getCommandName()); @@ -100,7 +117,6 @@ public String getEventType() { @Override public String getEventDescription() { - String backupUuid = _uuidMgr.getUuid(Backup.class, getBackupId()); - return "Restoring Instance from backup: " + backupUuid; + return "Restoring Instance from backup with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java index cee367a149c2..e05845866c2b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java @@ -20,12 +20,15 @@ import javax.inject.Inject; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.BackupResponse; @@ -53,6 +56,7 @@ public class RestoreVolumeFromBackupAndAttachToVMCmd extends BaseAsyncCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// + @ACL @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, @@ -60,12 +64,14 @@ public class RestoreVolumeFromBackupAndAttachToVMCmd extends BaseAsyncCmd { description = "ID of the Instance backup") private Long backupId; + @ACL @Parameter(name = ApiConstants.VOLUME_ID, type = CommandType.STRING, required = true, description = "ID of the volume backed up") private String volumeUuid; + @ACL @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, @@ -73,6 +79,14 @@ public class RestoreVolumeFromBackupAndAttachToVMCmd extends BaseAsyncCmd { description = "ID of the Instance where to attach the restored volume") private Long vmId; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "If quickrestore is true, which host to start the VM on;" + + " otherwise, ignored. Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0", authorized = {RoleType.Admin}) + private Long hostId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -89,6 +103,14 @@ public Long getBackupId() { return backupId; } + public boolean isQuickRestore() { + return org.apache.commons.lang3.BooleanUtils.isTrue(quickRestore); + } + + public Long getHostId() { + return hostId; + } + @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); @@ -101,7 +123,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try { - boolean result = backupManager.restoreBackupVolumeAndAttachToVM(volumeUuid, backupId, vmId); + boolean result = backupManager.restoreBackupVolumeAndAttachToVM(volumeUuid, backupId, vmId, isQuickRestore(), getHostId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); response.setResponseName(getCommandName()); @@ -121,6 +143,16 @@ public String getEventType() { @Override public String getEventDescription() { - return "Restoring volume "+ volumeUuid + " from backup " + backupId + " and attaching it to Instance " + vmId; + return "Restoring volume "+ volumeUuid + " from backup " + getResourceUuid(ApiConstants.BACKUP_ID) + " and attaching it to Instance " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); + } + + @Override + public Long getApiResourceId() { + return vmId; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.VirtualMachine; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/CreateBucketCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/CreateBucketCmd.java index f1c149854d92..099b56368676 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/CreateBucketCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/CreateBucketCmd.java @@ -181,7 +181,7 @@ public void create() throws ResourceAllocationException { @Override public void execute() { - CallContext.current().setEventDetails("Bucket Id: " + getEntityUuid()); + CallContext.current().setEventDetails("Bucket ID: " + getEntityUuid()); Bucket bucket; try { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/DeleteBucketCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/DeleteBucketCmd.java index 8cd2790e4ae2..abbb1760f9d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/DeleteBucketCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/DeleteBucketCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.bucket; import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.storage.object.Bucket; import com.cloud.user.Account; @@ -82,8 +83,8 @@ public ApiCommandResourceType getApiResourceType() { } @Override - public void execute() throws ConcurrentOperationException { - CallContext.current().setEventDetails("Bucket Id: " + this._uuidMgr.getUuid(Bucket.class, getId())); + public void execute() throws ConcurrentOperationException, ResourceAllocationException { + CallContext.current().setEventDetails("Bucket ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _bucketService.deleteBucket(id, CallContext.current().getCallingAccount()); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/UpdateBucketCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/UpdateBucketCmd.java index f913373c04b6..dc873c300497 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/UpdateBucketCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bucket/UpdateBucketCmd.java @@ -113,7 +113,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ConcurrentOperationException { - CallContext.current().setEventDetails("Bucket Id: " + this._uuidMgr.getUuid(Bucket.class, getId())); + CallContext.current().setEventDetails("Bucket ID: " + getResourceUuid(ApiConstants.ID)); boolean result = false; try { result = _bucketService.updateBucket(this, CallContext.current().getCallingAccount()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java new file mode 100644 index 000000000000..298ddd64a31c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -0,0 +1,169 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.BooleanUtils; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "addDnsServer", + description = "Adds a new external DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = true, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AddDnsServerCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + /// + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") + private String url; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + private String provider; + + @Parameter(name = ApiConstants.DNS_USER_NAME, type = CommandType.STRING, + description = "Username or email associated with the DNS provider account (used for authentication)") + private String dnsUserName; + + @Parameter(name = ApiConstants.DNS_API_KEY, required = true, type = CommandType.STRING, description = "API key or token for the DNS provider") + private String dnsApiKey; + + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, + description = "Whether this DNS server can be used by accounts other than the owner to create and manage DNS zones") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, + description = "Domain suffix that restricts DNS zones created by non-owner accounts to subdomains of this " + + "suffix (for example, sub.example.com under example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING, + required = true, + description = "Comma separated list of name servers; used to create NS records for the DNS Zone (for example, ns1.example.com, ns2.example.com)") + private List nameServers; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using " + + "format details[i].keyname=keyvalue. Example: details[0].pdnsServerId=localhost") + protected Map details; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { return name; } + + public String getUrl() { return url; } + + public String getDnsApiKey() { + return dnsApiKey; + } + + public Integer getPort() { + return port; + } + + public Boolean isPublic() { + return BooleanUtils.isTrue(isPublic); + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public List getNameServers() { + return nameServers; + } + + public DnsProviderType getProvider() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, provider, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.addDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to add DNS server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } + + public String getDnsUserName() { + return dnsUserName; + } + + public Map getDetails() { + Map detailsMap = new HashMap<>(); + if (MapUtils.isNotEmpty(details)) { + Collection props = details.values(); + for (Object prop : props) { + HashMap detail = (HashMap) prop; + for (Map.Entry entry: detail.entrySet()) { + detailsMap.put(entry.getKey(),entry.getValue()); + } + } + } + return detailsMap; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java new file mode 100644 index 000000000000..5ffffc7c304d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.Network; +import com.cloud.user.Account; + +@APICommand(name = "associateDnsZoneToNetwork", + description = "Associates a DNS Zone with a Network for VM auto-registration", + responseObject = DnsZoneNetworkMapResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AssociateDnsZoneToNetworkCmd extends BaseCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the network") + private Long networkId; + + @Parameter(name = "subdomain", type = CommandType.STRING, + description = "Optional subdomain to append (e.g., 'dev' creates vm1.dev.example.com)") + private String subDomain; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + DnsZoneNetworkMapResponse response = dnsProviderManager.associateZoneToNetwork(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + Network network = _entityMgr.findById(Network.class, networkId); + if (network != null) { + return network.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getDnsZoneId() { + return dnsZoneId; + } + + public Long getNetworkId() { + return networkId; + } + + public String getSubDomain() { + return subDomain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java new file mode 100644 index 000000000000..d2fe3a5024a7 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -0,0 +1,100 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createDnsRecord", + description = "Creates a DNS record directly on the provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateDnsRecordCmd extends BaseAsyncCmd { + + @ACL + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone") + private Long dnsZoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "DNS record name") + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "DNS record type (e.g., A, AAAA, CNAME, MX, TXT, etc.)") + private String type; + + @Parameter(name = ApiConstants.CONTENTS, type = CommandType.LIST, collectionType = CommandType.STRING, required = true, + description = "The content of the record (IP address for A/AAAA, FQDN for CNAME/NS, quoted string for TXT, etc.)") + private List contents; + + @Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live") + private Integer ttl; + + // Getters + public Long getDnsZoneId() { return dnsZoneId; } + public String getName() { return name; } + + public List getContents() { return contents; } + public Integer getTtl() { return (ttl == null) ? 3600 : ttl; } + + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + + @Override + public void execute() { + try { + DnsRecordResponse response = dnsProviderManager.createDnsRecord(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_CREATE; } + + @Override + public String getEventDescription() { return "Creating DNS Record: " + getName(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java new file mode 100644 index 000000000000..10a706e3a815 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -0,0 +1,154 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.Arrays; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createDnsZone", + description = "Creates a new DNS Zone on a specific server", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "The name of the DNS zone (e.g. example.com)") + private String name; + + @ACL + @Parameter(name = ApiConstants.DNS_SERVER_ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "The ID of the DNS server to host this zone") + private Long dnsServerId; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, + description = "The type of zone (Public, Private). Defaults to Public.") + private String type; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the DNS zone") + private String description; + + @Parameter(name = ApiConstants.EXISTING, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class, + description = "If true, imports an existing DNS zone from the DNS provider into CloudStack. " + + "If false, creates the zone in the DNS provider and registers it in CloudStack. Default is false") + private Boolean existing = false; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public Long getDnsServerId() { + return dnsServerId; + } + + public DnsZone.ZoneType getType() { + if (StringUtils.isBlank(type)) { + return DnsZone.ZoneType.Public; + } + DnsZone.ZoneType zoneType = EnumUtils.getEnumIgnoreCase(DnsZone.ZoneType.class, type); + if (zoneType == null) { + throw new IllegalArgumentException("Invalid type value, supported values are: " + Arrays.toString(DnsZone.ZoneType.values())); + } + return zoneType; + } + + public String getDescription() { + return description; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void create() throws ResourceAllocationException { + try { + DnsZone zone = dnsProviderManager.allocateDnsZone(this); + if (zone != null) { + setEntityId(zone.getId()); + setEntityUuid(zone.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Zone entity"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to allocate DNS Zone: " + e.getMessage()); + } + } + + @Override + public void execute() { + try { + DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId(), isExistingZone()); + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_CREATE; + } + + @Override + public String getEventDescription() { + return "creating DNS zone: " + getName(); + } + + public Boolean isExistingZone() { + return Boolean.TRUE.equals(existing); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java new file mode 100644 index 000000000000..1d7914c346e9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "deleteDnsRecord", + description = "Deletes a DNS record from the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsRecordCmd extends BaseAsyncCmd { + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true) + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true) + private String type; + + // Getters + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + public Long getDnsZoneId() { return dnsZoneId; } + public String getName() { return name; } + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsRecord(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Record"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Error deleting DNS Record: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS Record: " + getName(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java new file mode 100644 index 000000000000..099fc62f354c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsServer", + description = "Removes a DNS server integration", + responseObject = SuccessResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsServerCmd extends BaseAsyncCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "the ID of the DNS server") + private Long id; + + @Parameter(name = ApiConstants.CLEANUP, type = CommandType.BOOLEAN, + entityType = DnsZoneResponse.class, description = "If true, all associated DNS zones will be cleaned up " + + "when the server is removed. Default: true") + private Boolean cleanup = true; + + @Parameter(name = ApiConstants.UNMANAGE, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class, + description = "If true, the DNS zone is only removed from CloudStack (unmanaged); if false, it is removed " + + "from both CloudStack and the DNS provider. Default: false") + private Boolean unmanage = false; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsServer(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsServer server = _entityMgr.findById(DnsServer.class, id); + if (server != null) { + return server.getAccountId(); + } + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS server ID: " + getId(); } + + public Boolean getCleanup() { + return Boolean.TRUE.equals(cleanup); + } + + public Boolean isUnmanage() { + return Boolean.TRUE.equals(unmanage); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java new file mode 100644 index 000000000000..a6d5a710ae56 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -0,0 +1,109 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsZone", + description = "Removes a DNS Zone from CloudStack and the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsZoneCmd extends BaseAsyncCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "The ID of the DNS zone") + private Long id; + + @Parameter(name = ApiConstants.UNMANAGE, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class, + description = "If true, removes the DNS zone only from CloudStack; if false, removes it from " + + "both CloudStack and the DNS provider. Default: false") + private Boolean unmanage = false; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsZone(getId(), isUnmanage()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Zone"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsZone zone = _entityMgr.findById(DnsZone.class, id); + if (zone != null) { + return zone.getAccountId(); + } + // Fallback or System if not found (likely to fail in execute() anyway) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_DELETE; + } + + @Override + public String getEventDescription() { + return "Deleting DNS Zone ID: " + getId(); + } + + public Boolean isUnmanage() { + return Boolean.TRUE.equals(unmanage); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java new file mode 100644 index 000000000000..b0aa0902e9e6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.Network; +import com.cloud.user.Account; + +@APICommand(name = "disassociateDnsZoneFromNetwork", + description = "Removes the association between a DNS Zone and a Network", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the Network") + private Long networkId; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = dnsProviderManager.disassociateZoneFromNetwork(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to disassociate DNS zone from network."); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + Network network = _entityMgr.findById(Network.class, networkId); + if (network != null) { + return network.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getNetworkId() { + return networkId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java new file mode 100644 index 000000000000..800f030754b9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.response.DnsProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProvider; + +@APICommand(name = "listDnsProviders", + description = "Lists available DNS plugin providers", + responseObject = DnsProviderResponse.class, + entityType = {DnsProvider.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsProvidersCmd extends BaseListCmd { + + @Override + public void execute() { + List providers = dnsProviderManager.listProviderNames(); + ListResponse response = new ListResponse<>(); + List responses = new ArrayList<>(); + for (String name : providers) { + DnsProviderResponse resp = new DnsProviderResponse(name); + resp.setName(name); + responses.add(resp); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java new file mode 100644 index 000000000000..246c8bee9aab --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsRecord; + +@APICommand(name = "listDnsRecords", + description = "Lists DNS records from the external provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsRecordsCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone to list records from") + private Long dnsZoneId; + + public Long getDnsZoneId() { + return dnsZoneId; + } + + @Override + public void execute() { + // The manager will fetch live data from the plugin + ListResponse response = dnsProviderManager.listDnsRecords(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java new file mode 100644 index 000000000000..ca7ac1944a9d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "listDnsServers", + description = "Lists DNS servers owned by the account.", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsServersCmd extends BaseListAccountResourcesCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "the ID of the DNS server") + private Long id; + + @Parameter(name = ApiConstants.PROVIDER_TYPE, type = CommandType.STRING, + description = "filter by provider type (e.g. PowerDNS, Cloudflare)") + private String providerType; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public DnsProviderType getProviderType() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, providerType, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ListResponse response = dnsProviderManager.listDnsServers(this); + response.setResponseName(getCommandName()); + response.setObjectName("dnsserver"); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java new file mode 100644 index 000000000000..e71bdabaf617 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsZone; + +@APICommand(name = "listDnsZones", + description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsZonesCmd extends BaseListCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + description = "List DNS zone by ID") + private Long id; + + @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "List DNS zones belonging to a specific DNS server") + private Long dnsServerId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List by zone name") + private String name; + + public Long getId() { return id; } + public Long getDnsServerId() { return dnsServerId; } + public String getName() { return name; } + + @Override + public void execute() { + ListResponse response = dnsProviderManager.listDnsZones(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java new file mode 100644 index 000000000000..6b790fa8ade8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.user.Account; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "updateDnsServer", + description = "Update DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = true, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateDnsServerCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "The ID of the DNS server to update") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, description = "API URL of the provider") + private String url; + + @Parameter(name = ApiConstants.DNS_API_KEY, type = CommandType.STRING, description = "API Key or Credentials for the external provider") + private String dnsApiKey; + + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, + description = "Whether this DNS server can be used by accounts other than the owner to create and manage DNS zones") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, + description = "Domain suffix that restricts DNS zones created by non-owner accounts to subdomains of this " + + "suffix (for example, sub.example.com under example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "Comma separated list of name servers; used to create NS records for the DNS Zone (for example, ns1.example.com, ns2.example.com)") + private List nameServers; + + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "Update state for the DNS server (Enabled, Disabled)") + private String state; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { return id; } + public String getName() { return name; } + public String getUrl() { return url; } + public String getDnsApiKey() { + return dnsApiKey; + } + public Integer getPort() { + return port; + } + public Boolean isPublic() { + return isPublic; + } + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + public String getNameServers() { + if (nameServers == null) { + return null; + } + return StringUtils.join(nameServers.stream() + .filter(StringUtils::isNotBlank) + .map(StringUtils::trim) + .toArray(String[]::new), ","); + } + + @Override + public long getEntityOwnerId() { + DnsServer server = _entityMgr.findById(DnsServer.class, id); + if (server != null) { + return server.getAccountId(); + } + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.updateDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to update DNS server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } + + public DnsServer.State getState() { + if (StringUtils.isBlank(state)) { + return null; + } + DnsServer.State dnsState = EnumUtils.getEnumIgnoreCase(DnsServer.State.class, state); + if (dnsState == null) { + throw new IllegalArgumentException("Invalid state value: " + state); + } + return dnsState; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java new file mode 100644 index 000000000000..5952b73ea6e4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -0,0 +1,95 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.user.Account; + +@APICommand(name = "updateDnsZone", + description = "Updates a DNS Zone's metadata", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateDnsZoneCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the DNS zone to be updated") + private String description; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + DnsZone result = dnsProviderManager.updateDnsZone(this); + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsZone dnsZone = _entityMgr.findById(DnsZone.class, id); + if (dnsZone != null) { + return dnsZone.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java index f972cbdc6758..e2c84ac85c7a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateEgressFirewallRuleCmd.java @@ -147,7 +147,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; FirewallRule rule = _entityMgr.findById(FirewallRule.class, getEntityId()); try { - CallContext.current().setEventDetails("Rule Id: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = _firewallService.applyEgressFirewallRules(rule, callerContext.getCallingAccount()); // State is different after the rule is applied, so get new object here rule = _entityMgr.findById(FirewallRule.class, getEntityId()); @@ -212,7 +212,7 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java index 92be579a1ced..30dd1a2d015a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmd.java @@ -150,7 +150,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; FirewallRule rule = _entityMgr.findById(FirewallRule.class, getEntityId()); try { - CallContext.current().setEventDetails("Rule ID: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = _firewallService.applyIngressFwRules(rule.getSourceIpAddressId(), callerContext.getCallingAccount()); // State is different after the rule is applied, so get new object here @@ -223,13 +223,9 @@ public State getState() { } @Override - public long getNetworkId() { - IpAddress ip = _entityMgr.findById(IpAddress.class, getIpAddressId()); - Long ntwkId = null; - - if (ip.getAssociatedWithNetworkId() != null) { - ntwkId = ip.getAssociatedWithNetworkId(); - } + public Long getNetworkId() { + IpAddress ip = getIp(); + Long ntwkId = isVpcIp(ip) ? getVpcNetworkIdForFirewallRule(ip) : getIsolatedNetworkIdForFirewallRule(ip); if (ntwkId == null) { throw new InvalidParameterValueException("Unable to create firewall rule for the IP address ID=" + ipAddressId + @@ -238,6 +234,12 @@ public long getNetworkId() { return ntwkId; } + @Override + public Long getVpcId() { + IpAddress ip = getIp(); + return isVpcIp(ip) ? ip.getVpcId() : null; + } + @Override public long getEntityOwnerId() { Account account = CallContext.current().getCallingAccount(); @@ -300,7 +302,21 @@ public String getSyncObjType() { @Override public Long getSyncObjId() { - return getIp().getAssociatedWithNetworkId(); + Long syncObjId = getIp().getAssociatedWithNetworkId(); + return syncObjId != null ? syncObjId : getNetworkId(); + } + + private boolean isVpcIp(IpAddress ip) { + return ip.getVpcId() != null; + } + + private Long getIsolatedNetworkIdForFirewallRule(IpAddress ip) { + return ip.getAssociatedWithNetworkId(); + } + + private Long getVpcNetworkIdForFirewallRule(IpAddress ip) { + // VPC flow is independent from tier association; manager resolves execution network. + return ip.getNetworkId(); } private IpAddress getIp() { @@ -311,6 +327,7 @@ private IpAddress getIp() { return ip; } + @Override public Integer getIcmpCode() { if (icmpCode != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java index db6b788178ab..66fc118395ea 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/CreatePortForwardingRuleCmd.java @@ -54,7 +54,6 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class CreatePortForwardingRuleCmd extends BaseAsyncCreateCmd implements PortForwardingRule { - // /////////////////////////////////////////////////// // ////////////// API parameters ///////////////////// // /////////////////////////////////////////////////// @@ -177,7 +176,7 @@ public Boolean getOpenFirewall() { } } - private Long getVpcId() { + public Long getVpcId() { if (ipAddressId != null) { IpAddress ipAddr = _networkService.getIp(ipAddressId); if (ipAddr == null || !ipAddr.readyToUse()) { @@ -199,7 +198,7 @@ public void execute() throws ResourceUnavailableException { boolean success = true; PortForwardingRule rule = null; try { - CallContext.current().setEventDetails("Rule Id: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); if (getOpenFirewall()) { success = success && _firewallService.applyIngressFirewallRules(ipAddressId, callerContext.getCallingAccount()); @@ -276,15 +275,9 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { IpAddress ip = _entityMgr.findById(IpAddress.class, getIpAddressId()); - Long ntwkId = null; - - if (ip.getAssociatedWithNetworkId() != null) { - ntwkId = ip.getAssociatedWithNetworkId(); - } else { - ntwkId = networkId; - } + Long ntwkId = _networkService.getPreferredNetworkIdForPublicIpRuleAssignment(ip, networkId); if (ntwkId == null) { throw new InvalidParameterValueException("Unable to create port forwarding rule for the ipAddress id=" + ipAddressId + " as ip is not associated with any network and no networkId is passed in"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteEgressFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteEgressFirewallRuleCmd.java index 60c7839bdc6d..4b606683a396 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteEgressFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteEgressFirewallRuleCmd.java @@ -71,7 +71,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting egress firewall rule id=" + id); + return ("Deleting egress firewall rule with ID: " + getResourceUuid(ApiConstants.ID)); } @Override @@ -89,7 +89,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _firewallService.revokeEgressFirewallRule(id, true); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteFirewallRuleCmd.java index 7c4d5f2249f5..ff2dce8dacf0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeleteFirewallRuleCmd.java @@ -69,7 +69,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting firewall rule ID=" + id); + return ("Deleting firewall rule with ID:" + getResourceUuid(ApiConstants.ID)); } @Override @@ -87,7 +87,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _firewallService.revokeIngressFwRule(id, true); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeletePortForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeletePortForwardingRuleCmd.java index 47dd9e039eb3..d0b607d7af48 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeletePortForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/DeletePortForwardingRuleCmd.java @@ -73,7 +73,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting port forwarding rule for ID=" + id); + return "Deleting port forwarding rule with ID:" + getResourceUuid(ApiConstants.ID); } @Override @@ -92,7 +92,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Rule ID: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); //revoke corresponding firewall rule first boolean result = _firewallService.revokeRelatedFirewallRule(id, true); result = result && _rulesService.revokePortForwardingRule(id, true); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateEgressFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateEgressFirewallRuleCmd.java index 7516a78f0bac..26d561dbe03d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateEgressFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateEgressFirewallRuleCmd.java @@ -69,7 +69,7 @@ public Boolean getDisplay() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); FirewallRule rule = _firewallService.updateEgressFirewallRule(id, this.getCustomId(), getDisplay()); FirewallResponse fwResponse = new FirewallResponse(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateFirewallRuleCmd.java index 347434d23940..1c2ea2b1897e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/firewall/UpdateFirewallRuleCmd.java @@ -70,7 +70,7 @@ public Boolean getDisplay() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule ID: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); FirewallRule rule = _firewallService.updateIngressFirewallRule(id, this.getCustomId(), getDisplay()); FirewallResponse fwResponse = new FirewallResponse(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/guest/ListGuestOsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/guest/ListGuestOsCmd.java index 4ff2ebf0a667..d558e4c4f245 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/guest/ListGuestOsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/guest/ListGuestOsCmd.java @@ -45,6 +45,11 @@ public class ListGuestOsCmd extends BaseListCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSResponse.class, description = "List by OS type ID") private Long id; + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, + entityType = GuestOSResponse.class, since = "4.22.1", + description = "Comma separated list of OS types") + private List ids; + @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, description = "List by OS Category ID") private Long osCategoryId; @@ -63,6 +68,10 @@ public Long getId() { return id; } + public List getIds() { + return ids; + } + public Long getOsCategoryId() { return osCategoryId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java index 8566b413cc12..2e3849cc8503 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java @@ -57,6 +57,10 @@ public class CreateGuiThemeCmd extends BaseCmd { "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @Parameter(name = ApiConstants.LOGIN_BASE_DOMAIN, type = CommandType.STRING, length = 65535, description = "The ACS domain to be used as base " + + "for the login when accessing the GUI through the common name defined in the theme. If a common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + "the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -93,6 +97,10 @@ public String getCommonNames() { return commonNames; } + public String getLoginBaseDomain() { + return loginBaseDomain; + } + public String getDomainIds() { return domainIds; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java index daef2235ce89..e50961aa30d1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java @@ -60,6 +60,10 @@ public class UpdateGuiThemeCmd extends BaseCmd { "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @Parameter(name = ApiConstants.LOGIN_BASE_DOMAIN, type = CommandType.STRING, length = 65535, description = "The ACS domain to be used as base for " + + "the login when accessing the GUI through the common name defined in the theme. If a common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + "the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -96,6 +100,10 @@ public String getJsonConfiguration() { return jsonConfiguration; } + public String getLoginBaseDomain() { + return loginBaseDomain; + } + public String getCommonNames() { return commonNames; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/CreateIpv6FirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/CreateIpv6FirewallRuleCmd.java index 8db66112cdbe..237af7e4601b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/CreateIpv6FirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/CreateIpv6FirewallRuleCmd.java @@ -232,7 +232,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; FirewallRule rule = ipv6Service.getIpv6FirewallRule(getEntityId()); try { - CallContext.current().setEventDetails("Rule ID: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = ipv6Service.applyIpv6FirewallRule(rule.getId()); // State is different after the rule is applied, so get new object here diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/DeleteIpv6FirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/DeleteIpv6FirewallRuleCmd.java index 19ecbda290c6..6df5ce1438a1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/DeleteIpv6FirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/DeleteIpv6FirewallRuleCmd.java @@ -66,7 +66,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting IPv6 firewall rule ID=" + id); + return "Deleting IPv6 firewall rule with ID:" + getResourceUuid(ApiConstants.ID); } @Override @@ -81,7 +81,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("IPv6 firewall rule ID: " + id); + CallContext.current().setEventDetails("IPv6 firewall rule ID: " + getResourceUuid(ApiConstants.ID)); boolean result = ipv6Service.revokeIpv6FirewallRule(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/UpdateIpv6FirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/UpdateIpv6FirewallRuleCmd.java index 353f28e908b5..f090de4e8849 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/UpdateIpv6FirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/ipv6/UpdateIpv6FirewallRuleCmd.java @@ -156,7 +156,7 @@ public Integer getIcmpType() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + getId()); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); FirewallRule rules = ipv6Service.updateIpv6FirewallRule(this); FirewallResponse ruleResponse = _responseGenerator.createIpv6FirewallRuleResponse(rules); setResponseObject(ruleResponse); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/AttachIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/AttachIsoCmd.java index 27026d62a674..47d8d6c35f26 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/AttachIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/AttachIsoCmd.java @@ -99,7 +99,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "attaching ISO: " + getId() + " to Instance: " + getVirtualMachineId(); + return "Attaching ISO with ID: " + getResourceUuid(ApiConstants.ID) + " to Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -114,7 +114,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + getVirtualMachineId() + " ISO ID: " + getId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " ISO ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _templateService.attachIso(id, virtualMachineId, isForced()); if (result) { UserVm userVm = _responseGenerator.findUserVmById(virtualMachineId); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DeleteIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DeleteIsoCmd.java index b00b11ab1d31..28dfd25b2428 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DeleteIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DeleteIsoCmd.java @@ -87,7 +87,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting ISO " + getId(); + return "Deleting ISO with ID: " + getResourceUuid(ApiConstants.ID) + " from zone " + getResourceUuid(ApiConstants.ZONE_ID); } @Override @@ -102,7 +102,7 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("ISO Id: " + getId()); + CallContext.current().setEventDetails("ISO ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _templateService.deleteIso(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java index 03d433827984..2560d837de12 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/DetachIsoCmd.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserVmResponse; import com.cloud.event.EventTypes; @@ -51,6 +52,10 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd { description = "If true, ejects the ISO before detaching on VMware. Default: false", since = "4.15.1") protected Boolean forced; + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class, + description = "The ID of the ISO to detach. Required when the Instance has more than one ISO attached.", since = "4.23.0") + protected Long id; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -89,7 +94,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "detaching ISO from Instance: " + getVirtualMachineId(); + return "Detaching ISO from Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -104,7 +109,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - boolean result = _templateService.detachIso(virtualMachineId, null, isForced()); + boolean result = _templateService.detachIso(virtualMachineId, id, isForced()); if (result) { UserVm userVm = _entityMgr.findById(UserVm.class, virtualMachineId); UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", userVm).get(0); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java index 6cd8b312f979..279db0f3104e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/iso/ExtractIsoCmd.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.command.user.iso; -import com.cloud.dc.DataCenter; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; @@ -102,15 +101,13 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - String isoId = this._uuidMgr.getUuid(VirtualMachineTemplate.class, getId()); - String baseDescription = String.format("Extracting ISO: %s", isoId); + String description = "Extracting ISO: " + getResourceUuid(ApiConstants.ID); - Long zoneId = getZoneId(); - if (zoneId == null) { - return baseDescription; + if (getZoneId() == null) { + description += "from zone: " + getResourceUuid(ApiConstants.ZONE_ID); } - return String.format("%s from zone: %s", baseDescription, this._uuidMgr.getUuid(DataCenter.class, zoneId)); + return description; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java index b55d1b234f19..2c8401831132 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java @@ -19,6 +19,7 @@ import java.util.Date; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; import org.apache.cloudstack.api.Parameter; @@ -40,6 +41,12 @@ public class ListAsyncJobsCmd extends BaseListAccountResourcesCmd { @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = CommandType.UUID, entityType = ManagementServerResponse.class, description = "The id of the management server", since="4.19") private Long managementServerId; + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the job", since="4.22.1") + private String resourceId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the job", since="4.22.1") + private String resourceType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -52,6 +59,14 @@ public Long getManagementServerId() { return managementServerId; } + public String getResourceId() { + return resourceId; + } + + public String getResourceType() { + return resourceType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java index 93a443757212..5c3b0084574b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/job/QueryAsyncJobResultCmd.java @@ -16,8 +16,8 @@ // under the License. package org.apache.cloudstack.api.command.user.job; - import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; @@ -34,9 +34,15 @@ public class QueryAsyncJobResultCmd extends BaseCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType = AsyncJobResponse.class, required = true, description = "The ID of the asynchronous job") + @Parameter(name = ApiConstants.JOB_ID, type = CommandType.UUID, entityType = AsyncJobResponse.class, description = "The ID of the asynchronous job") private Long id; + @Parameter(name = ApiConstants.RESOURCE_ID, validations = {ApiArgValidator.UuidString}, type = CommandType.STRING, description = "the ID of the resource associated with the job", since="4.22.1") + private String resourceId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, description = "the type of the resource associated with the job", since="4.22.1") + private String resourceType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -45,6 +51,14 @@ public Long getId() { return id; } + public String getResourceId() { + return resourceId; + } + + public String getResourceType() { + return resourceType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/CreateKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/CreateKMSKeyCmd.java new file mode 100644 index 000000000000..c2b8fb6fe09e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/CreateKMSKeyCmd.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.command.user.kms; + +import com.cloud.exception.ResourceAllocationException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "createKMSKey", + description = "Creates a new KMS key (Key Encryption Key) for encryption", + responseObject = KMSKeyResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class CreateKMSKeyCmd extends BaseCmd implements UserCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.NAME, + required = true, + type = CommandType.STRING, + description = "Name of the KMS key") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, + type = CommandType.STRING, + description = "Description of the KMS key") + private String description; + + @Parameter(name = ApiConstants.ZONE_ID, + required = true, + type = CommandType.UUID, + entityType = ZoneResponse.class, + description = "Zone ID where the key will be valid") + private Long zoneId; + + @Parameter(name = ApiConstants.ACCOUNT, + type = CommandType.STRING, + description = "Account name (for creating keys for child accounts - requires domain admin or admin)") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.UUID, + entityType = DomainResponse.class, + description = "Domain ID (for creating keys for child accounts - requires domain admin or admin)") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, + type = CommandType.UUID, + entityType = ProjectResponse.class, + description = "ID of the project to create the KMS key for") + private Long projectId; + + @Parameter(name = ApiConstants.KEY_BITS, + type = CommandType.INTEGER, + description = "Key size in bits: 128, 192, or 256 (default: 256)") + private Integer keyBits; + + @Parameter(name = ApiConstants.HSM_PROFILE_ID, + type = CommandType.UUID, + entityType = HSMProfileResponse.class, + required = true, + description = "ID of HSM profile to create key in") + private Long hsmProfileId; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getZoneId() { + return zoneId; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + + public Integer getKeyBits() { + return keyBits != null ? keyBits : 256; + } + + public Long getHsmProfileId() { + return hsmProfileId; + } + + @Override + public void execute() throws ResourceAllocationException { + try { + KMSKeyResponse response = kmsManager.createKMSKey(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to create KMS key: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true); + if (accountId != null) { + return accountId; + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KmsKey; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java new file mode 100644 index 000000000000..58f67ec60c4d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.command.user.kms; + +import com.cloud.event.EventTypes; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.KMSKey; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "deleteKMSKey", + description = "Deletes a KMS key (only if not in use)", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, + required = true, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "The UUID of the KMS key to delete") + private Long id; + + @Override + public void execute() { + try { + SuccessResponse response = kmsManager.deleteKMSKey(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to delete KMS key: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + KMSKey key = _entityMgr.findById(KMSKey.class, id); + if (key != null) { + return key.getAccountId(); + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KmsKey; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_KMS_KEY_DELETE; + } + + @Override + public String getEventDescription() { + return "deleting KMS key: " + getId(); + } + + public Long getId() { + return id; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/ListKMSKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/ListKMSKeysCmd.java new file mode 100644 index 000000000000..c6e4292099fe --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/ListKMSKeysCmd.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.command.user.kms; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "listKMSKeys", + description = "Lists KMS keys available to the caller", + responseObject = KMSKeyResponse.class, + responseView = ResponseView.Restricted, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class ListKMSKeysCmd extends BaseListProjectAndAccountResourcesCmd implements UserCmd { + private static final String s_name = "listkmskeysresponse"; + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "List KMS key by UUID") + private Long id; + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.UUID, + entityType = ZoneResponse.class, + description = "Filter by zone ID") + private Long zoneId; + + @Parameter(name = ApiConstants.ENABLED, + type = CommandType.BOOLEAN, + description = "Filter by enabled status") + private Boolean enabled; + + @Parameter(name = ApiConstants.HSM_PROFILE_ID, + type = CommandType.UUID, + entityType = HSMProfileResponse.class, + description = "Filter by HSM profile ID") + private Long hsmProfileId; + + public Long getId() { + return id; + } + + public Long getZoneId() { + return zoneId; + } + + public Boolean getEnabled() { + return enabled; + } + + public Long getHsmProfileId() { + return hsmProfileId; + } + + @Override + public void execute() { + ListResponse listResponse = kmsManager.listKMSKeys(this); + listResponse.setResponseName(getCommandName()); + setResponseObject(listResponse); + } + + @Override + public String getCommandName() { + return s_name; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java new file mode 100644 index 000000000000..e3c6c5822b2f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java @@ -0,0 +1,128 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.kms; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.KMSKey; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "rotateKMSKey", + description = "Rotates KMS key (KEK) by creating new version and scheduling gradual re-encryption", + responseObject = AsyncJobResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class RotateKMSKeyCmd extends BaseAsyncCmd { + private static final String s_name = "rotatekmskeyresponse"; + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, + required = true, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "KMS Key UUID to rotate") + private Long id; + + @Parameter(name = ApiConstants.KEY_BITS, + type = CommandType.INTEGER, + description = "Key size for new KEK (default: same as current)") + private Integer keyBits; + + @Parameter(name = ApiConstants.HSM_PROFILE_ID, + type = CommandType.UUID, + entityType = HSMProfileResponse.class, + description = "The target HSM profile ID for the new KEK version. If provided, migrates the key to " + + "this HSM.") + private Long hsmProfileId; + + public Long getId() { + return id; + } + + public Integer getKeyBits() { + return keyBits; + } + + public Long getHsmProfileId() { + return hsmProfileId; + } + + @Override + public void execute() { + try { + kmsManager.rotateKMSKey(this); + SuccessResponse response = new SuccessResponse(); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to rotate KMS key: " + e.getMessage()); + } + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + KMSKey key = _entityMgr.findById(KMSKey.class, id); + if (key != null) { + return key.getAccountId(); + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return com.cloud.event.EventTypes.EVENT_KMS_KEY_ROTATE; + } + + @Override + public String getEventDescription() { + return "Rotating KMS key: " + _uuidMgr.getUuid(KMSKey.class, id); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KmsKey; + } + + @Override + public Long getApiResourceId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java new file mode 100644 index 000000000000..77654aa13ac3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.command.user.kms; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "updateKMSKey", + description = "Updates KMS key name, description, or enabled status", + responseObject = KMSKeyResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class UpdateKMSKeyCmd extends BaseCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, + required = true, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "The UUID of the KMS key to update") + private Long id; + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + description = "New name for the key") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, + type = CommandType.STRING, + description = "New description for the key") + private String description; + + @Parameter(name = ApiConstants.ENABLED, + type = CommandType.BOOLEAN, + description = "whether the key should be enabled") + private Boolean enabled; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Boolean getEnabled() { + return enabled; + } + + @Override + public void execute() { + try { + KMSKeyResponse response = kmsManager.updateKMSKey(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to update KMS key: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.KmsKey; + } + + @Override + public Long getApiResourceId() { + return getId(); + } + + public Long getId() { + return id; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java new file mode 100644 index 000000000000..496ddee89b27 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java @@ -0,0 +1,158 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.kms.hsm; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.StringUtils; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.HSMProfile; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; +import java.util.Map; + +@APICommand(name = "createHSMProfile", description = "Creates a new HSM profile", responseObject = HSMProfileResponse.class, + requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0", + authorized = { RoleType.Admin }) +public class CreateHSMProfileCmd extends BaseCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "the name of the HSM profile") + private String name; + + @Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, + description = "the protocol of the HSM profile (PKCS11, KMIP, etc.). Default is 'pkcs11'") + private String protocol; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, + description = "the zone ID where the HSM profile is available. If null, global scope (for admin only)") + private Long zoneId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "the domain ID where the HSM profile is available. If specified without an account, " + + "the profile is domain-scoped and usable by all accounts directly in that domain.") + private Long domainId; + + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, + description = "the account name of the HSM profile owner. Must be used with domainId. " + + "Leave empty (with a domainId) to create a domain-scoped profile shared by all " + + "accounts in that domain.") + private String accountName; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "the ID of the project to add the HSM profile for") + private Long projectId; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, + description = "whether this is a public HSM profile available to all users globally (root admin only). " + + "Default is false") + private Boolean isPublic; + + @Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, description = "the vendor name of the HSM") + private String vendorName; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, + description = "HSM configuration details (protocol specific)") + private Map details; + + public String getName() { + return name; + } + + public String getProtocol() { + if (StringUtils.isBlank(protocol)) { + return "pkcs11"; + } + return protocol; + } + + public Long getZoneId() { + return zoneId; + } + + public Long getDomainId() { + return domainId; + } + + public String getAccountName() { + return accountName; + } + + public Long getProjectId() { + return projectId; + } + + public Boolean getIsPublic() { + return isPublic != null && isPublic; + } + + public String getVendorName() { + return vendorName; + } + + public Map getDetails() { + return convertDetailsToMap(details); + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + HSMProfile profile = kmsManager.addHSMProfile(this); + HSMProfileResponse response = kmsManager.createHSMProfileResponse(profile); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true); + if (accountId != null) { + return accountId; + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java new file mode 100644 index 000000000000..575a4447a69a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.kms.hsm; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.HSMProfile; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "deleteHSMProfile", description = "Deletes an HSM profile", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = { RoleType.Admin }) +public class DeleteHSMProfileCmd extends BaseCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, + description = "the ID of the HSM profile") + private Long id; + + public Long getId() { + return id; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = kmsManager.deleteHSMProfile(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete HSM profile"); + } + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + HSMProfile profile = _entityMgr.findById(HSMProfile.class, id); + if (profile != null && profile.getAccountId() > 0) { + return profile.getAccountId(); + } + return CallContext.current().getCallingAccount().getId(); + } + + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } + + @Override + public Long getApiResourceId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/ListHSMProfilesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/ListHSMProfilesCmd.java new file mode 100644 index 000000000000..dc5bb589c42c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/ListHSMProfilesCmd.java @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.kms.hsm; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "listHSMProfiles", description = "Lists HSM profiles", responseObject = HSMProfileResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListHSMProfilesCmd extends BaseListProjectAndAccountResourcesCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, + description = "the HSM profile ID") + private Long id; + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, + description = "the zone ID") + private Long zoneId; + + @Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "the protocol of the HSM profile") + private String protocol; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "list only enabled profiles") + private Boolean enabled; + + @Parameter(name = ApiConstants.IS_PUBLIC, + type = CommandType.BOOLEAN, + description = "when true, non-admin users see only system (global) profiles") + private Boolean isPublic; + + public Long getId() { + return id; + } + + public Long getZoneId() { + return zoneId; + } + + public String getProtocol() { + return protocol; + } + + public Boolean getEnabled() { + return enabled; + } + + public Boolean getIsPublic() { + return isPublic; + } + + @Override + public void execute() { + ListResponse response = kmsManager.listHSMProfiles(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java new file mode 100644 index 000000000000..fea7807d616d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java @@ -0,0 +1,104 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.user.kms.hsm; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.kms.HSMProfile; +import org.apache.cloudstack.kms.KMSManager; + +import javax.inject.Inject; + +@APICommand(name = "updateHSMProfile", description = "Updates an HSM profile", + responseObject = HSMProfileResponse.class, + requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0", + authorized = { RoleType.Admin }) +public class UpdateHSMProfileCmd extends BaseCmd { + + @Inject + private KMSManager kmsManager; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, + description = "the ID of the HSM profile") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the name of the HSM profile") + private String name; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, + description = "whether the HSM profile is enabled") + private Boolean enabled; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Boolean getEnabled() { + return enabled; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + HSMProfile profile = kmsManager.updateHSMProfile(this); + HSMProfileResponse response = kmsManager.createHSMProfileResponse(profile); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (KMSException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + HSMProfile profile = _entityMgr.findById(HSMProfile.class, id); + if (profile != null && profile.getAccountId() > 0) { + return profile.getAccountId(); + } + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } + + @Override + public Long getApiResourceId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java index 87b193e2513f..c9b31dc84271 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java @@ -83,7 +83,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Assigning a certificate to a load balancer"; + return "Assigning certificate with ID: " + getResourceUuid(ApiConstants.CERTIFICATE_ID) + " to load balancer with ID: " + getResourceUuid(ApiConstants.LBID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java index f7962dab1379..cc7cd2382b75 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignToLoadBalancerRuleCmd.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Map; +import com.cloud.network.Network; +import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.APICommand; @@ -72,7 +74,7 @@ public class AssignToLoadBalancerRuleCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID_IP, type = CommandType.MAP, - description = "VM ID and IP map, vmidipmap[0].vmid=1 vmidipmap[0].vmip=10.1.1.75", + description = "VM ID and IP map, vmidipmap[0].vmid=1 vmidipmap[0].vmip=10.1.1.75. (Optional, for VPC Conserve Mode) Pass vmnetworkid. Example: vmidipmap[0].vmnetworkid=NETWORK_TIER_UUID", since = "4.4") private Map vmIdIpMap; @@ -112,12 +114,13 @@ public String getEventType() { @Override public String getEventDescription() { - return "applying Instances for load balancer: " + getLoadBalancerId() + " (ids: " + StringUtils.join(getVirtualMachineIds(), ",") + ")"; + return "Applying Instances for load balancer with ID: " + getResourceUuid(ApiConstants.ID) + " (Instances IDs: " + StringUtils.join(getVirtualMachineIds(), ",") + ")"; } - public Map> getVmIdIpListMap() { - Map> vmIdIpsMap = new HashMap>(); + public Pair>, Map> getVmIdIpListMapAndVmIdNetworkMap() { + Map> vmIdIpsMap = new HashMap<>(); + Map vmIdNetworkMap = new HashMap<>(); if (vmIdIpMap != null && !vmIdIpMap.isEmpty()) { Collection idIpsCollection = vmIdIpMap.values(); Iterator iter = idIpsCollection.iterator(); @@ -125,6 +128,7 @@ public Map> getVmIdIpListMap() { HashMap idIpsMap = (HashMap)iter.next(); String vmId = idIpsMap.get("vmid"); String vmIp = idIpsMap.get("vmip"); + String vmNetworkUuid = idIpsMap.get("vmnetworkid"); VirtualMachine lbvm = _entityMgr.findByUuid(VirtualMachine.class, vmId); if (lbvm == null) { @@ -145,25 +149,35 @@ public Map> getVmIdIpListMap() { ipsList = new ArrayList(); } ipsList.add(vmIp); + + if (vmNetworkUuid != null) { + Network vmNetwork = _entityMgr.findByUuid(Network.class, vmNetworkUuid); + if (vmNetwork == null) { + throw new InvalidParameterValueException("Unable to find Network ID: " + vmNetworkUuid); + } + vmIdNetworkMap.put(longVmId, vmNetwork.getId()); + } vmIdIpsMap.put(longVmId, ipsList); } } - return vmIdIpsMap; + return new Pair<>(vmIdIpsMap, vmIdNetworkMap); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer Id: " + getLoadBalancerId() + " Instance IDs: " + StringUtils.join(getVirtualMachineIds(), ",")); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID) + " Instances IDs: " + StringUtils.join(getVirtualMachineIds(), ",")); - Map> vmIdIpsMap = getVmIdIpListMap(); + Pair>, Map> mapsPair = getVmIdIpListMapAndVmIdNetworkMap(); + Map> vmIdIpsMap = mapsPair.first(); + Map vmIdNetworkMap = mapsPair.second(); boolean result = false; try { - result = _lbService.assignToLoadBalancer(getLoadBalancerId(), virtualMachineIds, vmIdIpsMap, false); + result = _lbService.assignToLoadBalancer(getLoadBalancerId(), virtualMachineIds, vmIdIpsMap, vmIdNetworkMap, false); }catch (CloudRuntimeException ex) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to assign load balancer rule"); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to assign load balancer rule due to: " + ex.getMessage()); } if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateApplicationLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateApplicationLoadBalancerCmd.java index b244375d64b9..ae9eb31a2292 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateApplicationLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateApplicationLoadBalancerCmd.java @@ -193,7 +193,7 @@ public long getEntityOwnerId() { public void execute() throws ResourceAllocationException, ResourceUnavailableException { ApplicationLoadBalancerRule rule = null; try { - CallContext.current().setEventDetails("Load Balancer Id: " + getEntityId()); + CallContext.current().setEventDetails("Load Balancer ID: " + getEntityUuid()); // State might be different after the rule is applied, so get new object here rule = _entityMgr.findById(ApplicationLoadBalancerRule.class, getEntityId()); ApplicationLoadBalancerResponse lbResponse = _responseGenerator.createLoadBalancerContainerReponse(rule, _lbService.getLbInstances(getEntityId())); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBHealthCheckPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBHealthCheckPolicyCmd.java index c4dfcad7918a..7b5cda13f1a5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBHealthCheckPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBHealthCheckPolicyCmd.java @@ -155,7 +155,7 @@ public void execute() throws ResourceAllocationException, ResourceUnavailableExc boolean success = false; try { - CallContext.current().setEventDetails("Load balancer health check policy ID : " + getEntityId()); + CallContext.current().setEventDetails("Load balancer health check policy ID : " + getEntityUuid()); success = _lbService.applyLBHealthCheckPolicy(this); if (success) { // State might be different after the rule is applied, so get new object here diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBStickinessPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBStickinessPolicyCmd.java index b336b84517f4..e816e0f95ebb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBStickinessPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLBStickinessPolicyCmd.java @@ -138,7 +138,7 @@ public void execute() throws ResourceAllocationException, ResourceUnavailableExc boolean success = false; try { - CallContext.current().setEventDetails("Rule Id: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = _lbService.applyLBStickinessPolicy(this); if (success) { // State might be different after the rule is applied, so get new object here diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java index c39b8b9c6ec0..bd72f248364e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java @@ -268,7 +268,7 @@ public void execute() throws ResourceAllocationException, ResourceUnavailableExc boolean success = true; LoadBalancer rule = null; try { - CallContext.current().setEventDetails("Rule Id: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); if (getOpenFirewall()) { success = success && _firewallService.applyIngressFirewallRules(getSourceIpAddressId(), callerContext.getCallingAccount()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteApplicationLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteApplicationLoadBalancerCmd.java index f2064d42ca4f..8cfd1876325a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteApplicationLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteApplicationLoadBalancerCmd.java @@ -71,12 +71,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting load balancer: " + getId(); + return "Deleting load balancer with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer ID: " + getId()); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _appLbService.deleteApplicationLoadBalancer(getId()); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBHealthCheckPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBHealthCheckPolicyCmd.java index 27a92bb25fc6..c01c5a4ca01e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBHealthCheckPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBHealthCheckPolicyCmd.java @@ -76,12 +76,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting load balancer health check policy: " + getId(); + return "Deleting load balancer health check policy with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer health check policy Id: " + getId()); + CallContext.current().setEventDetails("Load balancer health check policy ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _lbService.deleteLBHealthCheckPolicy(getId(), true); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBStickinessPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBStickinessPolicyCmd.java index cc83835cd0e7..f26382478f4e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBStickinessPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLBStickinessPolicyCmd.java @@ -82,12 +82,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting load balancer stickiness policy: " + getId(); + return "Deleting load balancer stickiness policy with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer stickiness policy ID: " + getId()); + CallContext.current().setEventDetails("Load balancer stickiness policy ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _lbService.deleteLBStickinessPolicy(getId(), true); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLoadBalancerRuleCmd.java index fee9067d6950..a41808ced397 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/DeleteLoadBalancerRuleCmd.java @@ -76,12 +76,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting load balancer: " + getId(); + return "Deleting load balancer with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer ID: " + getId()); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _firewallService.revokeRelatedFirewallRule(id, true); result = result && _lbService.deleteLoadBalancerRule(id, true); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java index 0fccddf68445..010a5ad6022d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java @@ -67,7 +67,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Removing a certificate from a load balancer with ID " + getLbRuleId(); + return "Removing certificate from load balancer with ID " + getResourceUuid(ApiConstants.LBID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveFromLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveFromLoadBalancerRuleCmd.java index 713879c8c785..ffcafd47822c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveFromLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveFromLoadBalancerRuleCmd.java @@ -143,12 +143,12 @@ public Map> getVmIdIpListMap() { @Override public String getEventDescription() { - return "removing Instances from load balancer: " + getId() + " (ids: " + StringUtils.join(getVirtualMachineIds(), ",") + ")"; + return "Removing Instances from load balancer with ID: " + getResourceUuid(ApiConstants.ID) + " (instances IDs: " + StringUtils.join(getVirtualMachineIds(), ",") + ")"; } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer Id: " + getId() + " Instance IDs: " + StringUtils.join(getVirtualMachineIds(), ",")); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID) + " Instances IDs: " + StringUtils.join(getVirtualMachineIds(), ",")); Map> vmIdIpsMap = getVmIdIpListMap(); try { boolean result = _lbService.removeFromLoadBalancer(id, virtualMachineIds, vmIdIpsMap, false); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateApplicationLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateApplicationLoadBalancerCmd.java index 19a366732d54..c2075c2c79e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateApplicationLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateApplicationLoadBalancerCmd.java @@ -72,7 +72,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "updating load balancer: " + getId(); + return "Updating load balancer with ID: " + getResourceUuid(ApiConstants.ID); } @@ -81,7 +81,7 @@ public String getEventDescription() { ///////////////////////////////////////////////////// @Override public void execute() { - CallContext.current().setEventDetails("Load balancer ID: " + getId()); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID)); ApplicationLoadBalancerRule rule = _appLbService.updateApplicationLoadBalancer(getId(), this.getCustomId(), getDisplay()); ApplicationLoadBalancerResponse lbResponse = _responseGenerator.createLoadBalancerContainerReponse(rule, _lbService.getLbInstances(getId())); setResponseObject(lbResponse); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateLoadBalancerRuleCmd.java index 5e6a877954ff..0ac99f1c760c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/UpdateLoadBalancerRuleCmd.java @@ -119,12 +119,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "updating load balancer rule"; + return "Updating load balancer rule with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Load balancer ID: " + getId()); + CallContext.current().setEventDetails("Load balancer ID: " + getResourceUuid(ApiConstants.ID)); LoadBalancer result = _lbService.updateLoadBalancerRule(this); if (result != null) { LoadBalancerResponse response = _responseGenerator.createLoadBalancerResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java index 596d5952706a..98487ddeb19a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/CreateIpForwardingRuleCmd.java @@ -108,7 +108,7 @@ public void execute() throws ResourceUnavailableException { boolean result = true; FirewallRule rule = null; try { - CallContext.current().setEventDetails("Rule ID: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); if (getOpenFirewall()) { result = result && _firewallService.applyIngressFirewallRules(ipAddressId, CallContext.current().getCallingAccount()); @@ -229,8 +229,13 @@ public FirewallRule.State getState() { } @Override - public long getNetworkId() { - return -1; + public Long getNetworkId() { + return -1L; + } + + @Override + public Long getVpcId() { + return null; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DeleteIpForwardingRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DeleteIpForwardingRuleCmd.java index 6ca48bf36c43..ef9f428f8c8c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DeleteIpForwardingRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DeleteIpForwardingRuleCmd.java @@ -63,7 +63,7 @@ public Long getId() { @Override public void execute() { - CallContext.current().setEventDetails("Rule ID: " + id); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _firewallService.revokeRelatedFirewallRule(id, true); result = result && _rulesService.revokeStaticNatRule(id, true); @@ -95,7 +95,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting an IP forwarding 1:1 NAT rule ID:" + id); + return "Deleting IP forwarding 1:1 NAT rule with ID:" + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DisableStaticNatCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DisableStaticNatCmd.java index f3d03b8beb37..d80d63541c0f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DisableStaticNatCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/nat/DisableStaticNatCmd.java @@ -67,7 +67,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Disabling static NAT for IP ID=" + ipAddressId); + return ("Disabling static NAT for IP with ID: " + getResourceUuid(ApiConstants.IP_ADDRESS_ID)); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java index 087e31c08829..1776436b31a3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkACLCmd.java @@ -274,7 +274,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; NetworkACLItem rule = _networkACLService.getNetworkACLItem(getEntityId()); try { - CallContext.current().setEventDetails("Rule ID: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = _networkACLService.applyNetworkACL(rule.getAclId()); // State is different after the rule is applied, so get new object here diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java index cbf6df081b3b..79fb5f6d01cf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/CreateNetworkCmd.java @@ -199,6 +199,11 @@ public class CreateNetworkCmd extends BaseCmd implements UserCmd { @Parameter(name=ApiConstants.AS_NUMBER, type=CommandType.LONG, since = "4.20.0", description="the AS Number of the network") private Long asNumber; + @Parameter(name = ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + type = CommandType.BOOLEAN, since = "4.23.0", authorized = {RoleType.Admin}) + private Boolean keepMacAddressOnPublicNic; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -286,6 +291,10 @@ public String getSourceNatIP() { return sourceNatIP; } + public Boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + @Override public boolean isDisplay() { if(displayNetwork == null) @@ -410,6 +419,27 @@ public Long getAsNumber() { ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// + + public CreateNetworkCmd() { + } + + public CreateNetworkCmd(long networkOfferingId, String name, String displayText, String gateway, String netmask, String startIp, String endIp, long domainId, + String accountName, long zoneId, String aclType, boolean subdomainAccess, boolean displayNetwork) { + this.networkOfferingId = networkOfferingId; + this.name = name; + this.displayText = displayText; + this.gateway = gateway; + this.netmask = netmask; + this.startIp = startIp; + this.endIp = endIp; + this.domainId = domainId; + this.accountName = accountName; + this.zoneId = zoneId; + this.aclType = aclType; + this.subdomainAccess = subdomainAccess; + this.displayNetwork = displayNetwork; + } + @Override public String getCommandName() { return s_name; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLCmd.java index 76bbd127066c..a8506e06c4a3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLCmd.java @@ -61,7 +61,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting Network ACL ID=" + id); + return "Deleting Network ACL with ID:" + getResourceUuid(ApiConstants.ID); } @Override @@ -82,7 +82,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Network ACL item ID: " + id); + CallContext.current().setEventDetails("Network ACL item ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkACLService.revokeNetworkACLItem(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLListCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLListCmd.java index 0352a5756b18..3e3894a26864 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLListCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkACLListCmd.java @@ -61,7 +61,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting network ACL ID=" + id); + return ("Deleting network ACL with ID: " + getResourceUuid(ApiConstants.ID)); } @Override @@ -82,7 +82,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Network ACL ID: " + id); + CallContext.current().setEventDetails("Network ACL ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkACLService.deleteNetworkACL(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkCmd.java index 7063be7ee180..0543794e8bf5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/DeleteNetworkCmd.java @@ -66,7 +66,7 @@ public boolean isForced() { @Override public void execute() { - CallContext.current().setEventDetails("Network Id: " + id); + CallContext.current().setEventDetails("Network ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _networkService.deleteNetwork(id, isForced()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -93,7 +93,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting network: " + id; + return "Deleting network with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ReplaceNetworkACLListCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ReplaceNetworkACLListCmd.java index faddd340f83a..0b210b6b95d8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ReplaceNetworkACLListCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ReplaceNetworkACLListCmd.java @@ -76,7 +76,13 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Associating network ACL ID=" + aclId + " with network ID=" + networkId); + String description = "Associating Network ACL with ID:" + getResourceUuid(ApiConstants.ACL_ID); + + if (getNetworkId() != null) { + description += " to Network with ID:" + getResourceUuid(ApiConstants.NETWORK_ID); + } + + return description; } @Override @@ -95,7 +101,7 @@ public void execute() throws ResourceUnavailableException { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Network ID and private gateway can't be passed at the same time"); } - CallContext.current().setEventDetails("Network ACL ID: " + aclId); + CallContext.current().setEventDetails("Network ACL ID: " + getResourceUuid(ApiConstants.ACL_ID)); boolean result = false; if (getPrivateGatewayId() != null) { result = _networkACLService.replaceNetworkACLonPrivateGw(aclId, privateGatewayId); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/RestartNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/RestartNetworkCmd.java index c04a50cb05d8..2742e5ef6d18 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/RestartNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/RestartNetworkCmd.java @@ -115,7 +115,7 @@ public Long getSyncObjId() { @Override public String getEventDescription() { - return "Restarting network: " + getNetworkId(); + return "Restarting Network with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkACLItemCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkACLItemCmd.java index 9e7e1b5b8542..f1ba3a55a96c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkACLItemCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkACLItemCmd.java @@ -176,7 +176,7 @@ public String getReason() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + getId()); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); NetworkACLItem aclItem = _networkACLService.updateNetworkACLItem(this); NetworkACLItemResponse aclResponse = _responseGenerator.createNetworkACLItemResponse(aclItem); setResponseObject(aclResponse); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkCmd.java index 2e638f1e2f76..7b6841d60976 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/UpdateNetworkCmd.java @@ -105,6 +105,11 @@ public class UpdateNetworkCmd extends BaseAsyncCustomIdCmd implements UserCmd { @Parameter(name = ApiConstants.SOURCE_NAT_IP, type = CommandType.STRING, description = "IPV4 address to be assigned to the public interface of the network router. This address must already be acquired for this network", since = "4.19") private String sourceNatIP; + @Parameter(name = ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + type = CommandType.BOOLEAN, since = "4.23.0", authorized = {RoleType.Admin}) + private Boolean keepMacAddressOnPublicNic; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -186,6 +191,10 @@ public String getSourceNatIP() { return sourceNatIP; } + public Boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/CreateRoutingFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/CreateRoutingFirewallRuleCmd.java index ad52916c7a92..85166f5ab84a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/CreateRoutingFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/CreateRoutingFirewallRuleCmd.java @@ -238,7 +238,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; FirewallRule rule = _firewallService.getFirewallRule(getEntityId()); try { - CallContext.current().setEventDetails("Rule ID: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); success = routedIpv4Manager.applyRoutingFirewallRule(rule.getId()); // State is different after the rule is applied, so get new object here diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmd.java index 16696f5f71b7..646b704e088c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmd.java @@ -67,7 +67,7 @@ public String getEventType() { @Override public String getEventDescription() { - return String.format("Deleting ipv4 routing firewall rule ID=%s", id); + return String.format("Deleting IPv4 routing firewall rule with ID: %s", getResourceUuid(ApiConstants.ID)); } @Override @@ -82,7 +82,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Routing firewall rule ID: " + id); + CallContext.current().setEventDetails("Routing firewall rule with ID: " + getResourceUuid(ApiConstants.ID)); boolean result = routedIpv4Manager.revokeRoutingFirewallRule(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/UpdateRoutingFirewallRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/UpdateRoutingFirewallRuleCmd.java index c6f6034b1ba1..3c3a07ceece1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/UpdateRoutingFirewallRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/routing/UpdateRoutingFirewallRuleCmd.java @@ -95,7 +95,7 @@ public String getEventDescription() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Rule Id: " + getId()); + CallContext.current().setEventDetails("Rule ID: " + getResourceUuid(ApiConstants.ID)); FirewallRule rule = routedIpv4Manager.updateRoutingFirewallRule(this); FirewallResponse ruleResponse = _responseGenerator.createFirewallResponse(rule); setResponseObject(ruleResponse); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java index 5c5c8776bce3..cd6baaf827bb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java @@ -193,6 +193,22 @@ public Boolean getGpuEnabled() { return gpuEnabled; } + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public void setCpuNumber(Integer cpuNumber) { + this.cpuNumber = cpuNumber; + } + + public void setMemory(Integer memory) { + this.memory = memory; + } + + public void setEncryptRoot(Boolean encryptRoot) { + this.encryptRoot = encryptRoot; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/ActivateProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/ActivateProjectCmd.java index 228afb5a1862..c6717ac659a4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/ActivateProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/ActivateProjectCmd.java @@ -80,7 +80,7 @@ public List getEntityOwnerIds() { @Override public void execute() { - CallContext.current().setEventDetails("Project id: " + getId()); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.ID)); Project project = _projectService.activateProject(getId()); if (project != null) { ProjectResponse response = _responseGenerator.createProjectResponse(project); @@ -98,6 +98,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Activating project: " + id; + return "Activating project with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectCmd.java index d4e2b8f56f91..c2a0c132448b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectCmd.java @@ -66,7 +66,7 @@ public Boolean isCleanup() { @Override public void execute() { - CallContext.current().setEventDetails("Project Id: " + id); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _projectService.deleteProject(id, isCleanup()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -83,7 +83,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting project: " + id; + return "Deleting project with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectInvitationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectInvitationCmd.java index 7fe2c3c4abc6..b1d129b8af77 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectInvitationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/DeleteProjectInvitationCmd.java @@ -59,7 +59,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Project invitation id " + id); + CallContext.current().setEventDetails("Project invitation ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _projectService.deleteProjectInvitation(id); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -76,7 +76,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Project invitatino id " + id + " is being removed"; + return "Removing project invitation with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/SuspendProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/SuspendProjectCmd.java index 7850e7bf694b..f67d0d55587d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/SuspendProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/SuspendProjectCmd.java @@ -60,7 +60,7 @@ public Long geId() { @Override public void execute() throws ConcurrentOperationException, ResourceUnavailableException { - CallContext.current().setEventDetails("Project Id: " + id); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.ID)); Project project = _projectService.suspendProject(id); if (project != null) { ProjectResponse response = _responseGenerator.createProjectResponse(project); @@ -78,7 +78,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Suspending project: " + id; + return "Suspending project with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java index e38f9417e8a8..2d9bb92e4a36 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java @@ -133,7 +133,7 @@ public List getEntityOwnerIds() { @Override public void execute() throws ResourceAllocationException { - CallContext.current().setEventDetails("Project id: " + getId()); + CallContext.current().setEventDetails("Project ID: " + getResourceUuid(ApiConstants.ID)); if (getAccountName() != null && getUserId() != null) { throw new InvalidParameterValueException("Account name and User ID are mutually exclusive. Provide either Account name" + "to update Account or user ID to update the user of the project"); @@ -161,6 +161,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating project: " + id; + return "Updating project with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectInvitationCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectInvitationCmd.java index a8a6fb802fbf..34918de7339f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectInvitationCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectInvitationCmd.java @@ -92,7 +92,7 @@ public long getEntityOwnerId() { @Override public void execute() { - String eventDetails = "Project id: " + projectId + ";"; + String eventDetails = "Project id: " + getResourceUuid(ApiConstants.PROJECT_ID) + ";"; if (accountName != null) { eventDetails += " accountName: " + accountName + ";"; } else if (userId != null) { @@ -116,6 +116,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating project invitation for projectId " + projectId; + return "Updating project invitation for project with ID: " + getResourceUuid(ApiConstants.PROJECT_ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/AssignToGlobalLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/AssignToGlobalLoadBalancerRuleCmd.java index 1cda9ab2757d..8bb38d97c134 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/AssignToGlobalLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/AssignToGlobalLoadBalancerRuleCmd.java @@ -145,13 +145,13 @@ public String getEventType() { @Override public String getEventDescription() { - return "assign load balancer rules " + StringUtils.join(getLoadBalancerRulesIds(), ",") + " to global load balancer rule " + getGlobalLoadBalancerRuleId(); + return "Assigning load balancer rules " + StringUtils.join(getLoadBalancerRulesIds(), ",") + " to global load balancer rule " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { CallContext.current().setEventDetails( - "Global Load balancer rule Id: " + getGlobalLoadBalancerRuleId() + " VmIds: " + StringUtils.join(getLoadBalancerRulesIds(), ",")); + "Global Load balancer rule ID: " + getResourceUuid(ApiConstants.ID) + " Instances IDs: " + StringUtils.join(getLoadBalancerRulesIds(), ",")); boolean result = _gslbService.assignToGlobalLoadBalancerRule(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/CreateGlobalLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/CreateGlobalLoadBalancerRuleCmd.java index 20b227b831c1..2ecd8ef22e65 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/CreateGlobalLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/CreateGlobalLoadBalancerRuleCmd.java @@ -153,7 +153,7 @@ public void create() { GlobalLoadBalancerRule gslbRule = _gslbService.createGlobalLoadBalancerRule(this); this.setEntityId(gslbRule.getId()); this.setEntityUuid(gslbRule.getUuid()); - CallContext.current().setEventDetails("Rule Id: " + getEntityId()); + CallContext.current().setEventDetails("Rule ID: " + getEntityUuid()); } catch (Exception ex) { logger.warn("Exception: ", ex); throw new ServerApiException(ApiErrorCode.PARAM_ERROR, ex.getMessage()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/DeleteGlobalLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/DeleteGlobalLoadBalancerRuleCmd.java index 6053a11cf711..b44b547463e5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/DeleteGlobalLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/DeleteGlobalLoadBalancerRuleCmd.java @@ -85,12 +85,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "deleting global load balancer rule: " + getGlobalLoadBalancerId(); + return "Deleting global load balancer rule with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Deleting global Load balancer rule Id: " + getGlobalLoadBalancerId()); + CallContext.current().setEventDetails("Deleting global Load balancer rule with ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _gslbService.deleteGlobalLoadBalancerRule(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/RemoveFromGlobalLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/RemoveFromGlobalLoadBalancerRuleCmd.java index eb72cad86f29..a0ec9a1296ab 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/RemoveFromGlobalLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/RemoveFromGlobalLoadBalancerRuleCmd.java @@ -108,13 +108,13 @@ public String getEventType() { @Override public String getEventDescription() { - return "removing load balancer rules:" + StringUtils.join(getLoadBalancerRulesIds(), ",") + " from global load balancer: " + getGlobalLoadBalancerRuleId(); + return "Removing load balancer rules:" + StringUtils.join(getLoadBalancerRulesIds(), ",") + " from global load balancer: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { CallContext.current().setEventDetails( - "Global Load balancer rule Id: " + getGlobalLoadBalancerRuleId() + " VmIds: " + StringUtils.join(getLoadBalancerRulesIds(), ",")); + "Global Load balancer rule Id: " + getResourceUuid(ApiConstants.ID) + " VmIds: " + StringUtils.join(getLoadBalancerRulesIds(), ",")); boolean result = _gslbService.removeFromGlobalLoadBalancerRule(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/UpdateGlobalLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/UpdateGlobalLoadBalancerRuleCmd.java index 7ccf62a293af..a56672e29cac 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/UpdateGlobalLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/region/ha/gslb/UpdateGlobalLoadBalancerRuleCmd.java @@ -107,7 +107,7 @@ public long getEntityOwnerId() { @Override public void execute() { - org.apache.cloudstack.context.CallContext.current().setEventDetails("Global Load balancer Id: " + getId()); + org.apache.cloudstack.context.CallContext.current().setEventDetails("Global Load balancer ID: " + getResourceUuid(ApiConstants.ID)); GlobalLoadBalancerRule gslbRule = _gslbService.updateGlobalLoadBalancerRule(this); if (gslbRule != null) { GlobalLoadBalancerResponse response = _responseGenerator.createGlobalLoadBalancerResponse(gslbRule); @@ -125,6 +125,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "updating global load balancer rule"; + return "Updating global load balancer rule with ID: " + getResourceUuid(ApiConstants.ID); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/CreateResourceScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/CreateResourceScheduleCmd.java new file mode 100644 index 000000000000..f6268106c317 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/CreateResourceScheduleCmd.java @@ -0,0 +1,132 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.schedule; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.schedule.ResourceScheduleManager; +import org.apache.commons.lang3.EnumUtils; + +import javax.inject.Inject; +import java.util.Date; +import java.util.Map; + +@APICommand(name = "createResourceSchedule", description = "Create Resource Schedule", responseObject = ResourceScheduleResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateResourceScheduleCmd extends BaseCmd { + + @Inject + ResourceScheduleManager resourceScheduleManager; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedule is to be defined") + private String resourceId; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the schedule") + private String description; + + @Parameter(name = ApiConstants.SCHEDULE, type = CommandType.STRING, required = true, description = "Schedule for action on resource in cron format. e.g. '0 15 10 * *' for 'at 15:00 on 10th day of every month'") + private String schedule; + + @Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, required = true, description = "Specifies a timezone for this command. For more information on the timezone parameter, see TimeZone Format.") + private String timeZone; + + @Parameter(name = ApiConstants.ACTION, type = CommandType.STRING, required = true, description = "Action to take on the resource.") + private String action; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active. Defaults to current date plus 1 minute. (Format \"yyyy-MM-dd hh:mm:ss\")") + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive. (Format \"yyyy-MM-dd hh:mm:ss\")") + private Date endDate; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Enable schedule. Defaults to true") + private Boolean enabled; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = false, description = "Map of (key/value pairs) details for the schedule.") + private Map details; + + public ApiCommandResourceType getResourceType() { + ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType); + if (type == null) { + throw new InvalidParameterValueException("Unknown resource type: " + resourceType); + } + return type; + } + + public String getResourceId() { + return resourceId; + } + + public String getDescription() { + return description; + } + + public String getSchedule() { + return schedule; + } + + public String getTimeZone() { + return timeZone; + } + + public String getAction() { + return action; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Boolean getEnabled() { + if (enabled == null) { + enabled = true; + } + return enabled; + } + + public Map getDetails() { + return convertDetailsToMap(details); + } + + @Override + public void execute() { + ResourceScheduleResponse response = resourceScheduleManager.createSchedule(getResourceType(), getResourceId(), + getDescription(), getSchedule(), getTimeZone(), getAction(), getStartDate(), getEndDate(), getEnabled(), getDetails()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/DeleteResourceScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/DeleteResourceScheduleCmd.java new file mode 100644 index 000000000000..fe9a695df637 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/DeleteResourceScheduleCmd.java @@ -0,0 +1,86 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.schedule; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.schedule.ResourceScheduleManager; +import org.apache.commons.lang3.EnumUtils; + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = "deleteResourceSchedule", description = "Delete Resource Schedule", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteResourceScheduleCmd extends BaseCmd { + + @Inject + ResourceScheduleManager resourceScheduleManager; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedules are to be deleted") + private String resourceId; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "ID of the schedule to be deleted") + private Long id; + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "comma separated list of schedule ids to be deleted") + private List ids; + + public ApiCommandResourceType getResourceType() { + ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType); + if (type == null) { + throw new InvalidParameterValueException("Unknown resource type: " + resourceType); + } + return type; + } + + public String getResourceId() { + return resourceId; + } + + public Long getId() { + return id; + } + + public List getIds() { + return ids; + } + + @Override + public void execute() { + resourceScheduleManager.removeSchedule(getResourceType(), getResourceId(), getId(), getIds()); + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/ListResourceScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/ListResourceScheduleCmd.java new file mode 100644 index 000000000000..1c8869eff9c3 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/ListResourceScheduleCmd.java @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.schedule; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; +import org.apache.cloudstack.schedule.ResourceScheduleManager; +import org.apache.commons.lang3.EnumUtils; + +import javax.inject.Inject; +import java.util.List; + +@APICommand(name = "listResourceSchedule", description = "List Resource Schedules", responseObject = ResourceScheduleResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListResourceScheduleCmd extends BaseListCmd { + + @Inject + ResourceScheduleManager resourceScheduleManager; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "ID of the schedule") + private Long id; + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "comma separated list of schedule ids") + private List ids; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource.") + private String resourceType; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedules are to be listed.") + private String resourceId; + + @Parameter(name = ApiConstants.ACTION, type = CommandType.STRING, required = false, description = "Action to take on the resource.") + private String action; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Filter by enabled status.") + private Boolean enabled; + + public Long getId() { + return id; + } + + public List getIds() { + return ids; + } + + public ApiCommandResourceType getResourceType() { + ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType); + if (type == null) { + throw new InvalidParameterValueException("Unknown resource type: " + resourceType); + } + return type; + } + + public String getResourceId() { + return resourceId; + } + + public String getAction() { + return action; + } + + public Boolean getEnabled() { + return enabled; + } + + @Override + public void execute() { + ListResponse response = resourceScheduleManager.listSchedule( + getId(), getIds(), getResourceType(), getResourceId(), getAction(), getEnabled(), + getStartIndex(), getPageSizeVal() + ); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/UpdateResourceScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/UpdateResourceScheduleCmd.java new file mode 100644 index 000000000000..5422588853e9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/schedule/UpdateResourceScheduleCmd.java @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.schedule; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.schedule.ResourceScheduleManager; + +import javax.inject.Inject; +import java.util.Date; +import java.util.Map; + +@APICommand(name = "updateResourceSchedule", description = "Update Resource Schedule", responseObject = ResourceScheduleResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateResourceScheduleCmd extends BaseCmd { + + @Inject + ResourceScheduleManager resourceScheduleManager; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = true, description = "ID of the schedule to be updated") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the schedule") + private String description; + + @Parameter(name = ApiConstants.SCHEDULE, type = CommandType.STRING, required = false, description = "Schedule for action on resource in cron format.") + private String schedule; + + @Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, required = false, description = "Specifies a timezone for this command.") + private String timeZone; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active.") + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive.") + private Date endDate; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Enable or disable the schedule.") + private Boolean enabled; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = false, description = "Map of (key/value pairs) details for the schedule.") + private Map details; + + public Long getId() { + return id; + } + + public String getDescription() { + return description; + } + + public String getSchedule() { + return schedule; + } + + public String getTimeZone() { + return timeZone; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Boolean getEnabled() { + return enabled; + } + + public Map getDetails() { + return convertDetailsToMap(details); + } + + @Override + public void execute() { + ResourceScheduleResponse response = resourceScheduleManager.updateSchedule(getId(), getDescription(), getSchedule(), + getTimeZone(), getStartDate(), getEndDate(), getEnabled(), getDetails()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupEgressCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupEgressCmd.java index bf435406174c..91f2b7ad999a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupEgressCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupEgressCmd.java @@ -82,7 +82,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "revoking egress rule id: " + getId(); + return "Revoking egress rule with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupIngressCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupIngressCmd.java index c426647fe36c..2d7e591214df 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupIngressCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/securitygroup/RevokeSecurityGroupIngressCmd.java @@ -83,7 +83,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "revoking ingress rule id: " + getId(); + return "Revoking ingress rule with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ArchiveSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ArchiveSnapshotCmd.java index c6ed36ccef53..cae2a32a09fd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ArchiveSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ArchiveSnapshotCmd.java @@ -54,12 +54,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Archiving Snapshot " + id + " to secondary storage"; + return "Archiving Snapshot with ID: " + getResourceUuid(ApiConstants.ID) + " to secondary storage"; } @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { - CallContext.current().setEventDetails("Snapshot Id: " + this._uuidMgr.getUuid(Snapshot.class,getId())); + CallContext.current().setEventDetails("Snapshot ID: " + getResourceUuid(ApiConstants.ID)); Snapshot snapshot = _snapshotService.archiveSnapshot(getId()); if (snapshot != null) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java index 519f9876b960..c67439a2ef7c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CopySnapshotCmd.java @@ -144,18 +144,22 @@ public String getEventType() { @Override public String getEventDescription() { - StringBuilder descBuilder = new StringBuilder(); + StringBuilder descBuilder = new StringBuilder("Copying snapshot with ID: " + getResourceUuid(ApiConstants.ID)); + if (getDestinationZoneIds() != null) { + descBuilder.append(" to zones: ["); + for (Long destId : getDestinationZoneIds()) { - descBuilder.append(", "); descBuilder.append(_uuidMgr.getUuid(DataCenter.class, destId)); + descBuilder.append(", "); } - if (descBuilder.length() > 0) { - descBuilder.deleteCharAt(0); - } + + descBuilder.deleteCharAt(descBuilder.length() - 1); + + descBuilder.append("]"); } - return "copying snapshot: " + _uuidMgr.getUuid(Snapshot.class, getId()) + ((descBuilder.length() > 0) ? " to zones: " + descBuilder.toString() : ""); + return descBuilder.toString(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java index b2b1da91abea..d03df501847a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotCmd.java @@ -229,7 +229,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating Snapshot for volume: " + getVolumeUuid(); + return "Creating Snapshot for volume: " + getResourceUuid(ApiConstants.VOLUME_ID); } @Override @@ -244,7 +244,7 @@ public void create() throws ResourceAllocationException { setEntityId(snapshot.getId()); setEntityUuid(snapshot.getUuid()); } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Snapshot for volume" + getVolumeUuid()); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Snapshot for volume" + getResourceUuid(ApiConstants.VOLUME_ID)); } } @@ -260,14 +260,14 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Snapshot from volume [%s] was not found in database.", getVolumeUuid())); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Snapshot from volume [%s] was not found in database.", getResourceUuid(ApiConstants.VOLUME_ID))); } } catch (Exception e) { if (e.getCause() instanceof UnsupportedOperationException) { throw new ServerApiException(ApiErrorCode.UNSUPPORTED_ACTION_ERROR, String.format("Failed to create Snapshot due to unsupported operation: %s", e.getCause().getMessage())); } - String errorMessage = "Failed to create Snapshot due to an internal error creating Snapshot for volume " + getVolumeUuid(); + String errorMessage = "Failed to create Snapshot due to an internal error creating Snapshot for volume " + getResourceUuid(ApiConstants.VOLUME_ID); logger.error(errorMessage, e); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, errorMessage); } @@ -312,8 +312,4 @@ public Boolean getAsyncBackup() { return asyncBackup; } } - - protected String getVolumeUuid() { - return _uuidMgr.getUuid(Volume.class, getVolumeId()); - } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java index 0dd275cb4ae2..6fb5fb0463ab 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java @@ -143,7 +143,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating Snapshot from Instance Snapshot : " + this._uuidMgr.getUuid(VMSnapshot.class, getVMSnapshotId()); + return "Creating Snapshot from Instance Snapshot : " + getResourceUuid(ApiConstants.VOLUME_ID); } @Override @@ -166,7 +166,7 @@ public void create() throws ResourceAllocationException { public void execute() { VMSnapshot vmSnapshot = _vmSnapshotService.getVMSnapshotById(getVMSnapshotId()); logger.info("CreateSnapshotFromVMSnapshotCmd with {} and Snapshot [ID: {}, UUID: {}]", vmSnapshot, getEntityId(), getEntityUuid()); - CallContext.current().setEventDetails("Instance Snapshot Id: " + vmSnapshot.getUuid()); + CallContext.current().setEventDetails("Instance Snapshot ID: " + vmSnapshot.getUuid()); Snapshot snapshot = null; try { snapshot = _snapshotService.backupSnapshotFromVmSnapshot(getEntityId(), getVmId(), getVolumeId(), getVMSnapshotId()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/DeleteSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/DeleteSnapshotCmd.java index 894777375819..b4eaceb61ba6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/DeleteSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/DeleteSnapshotCmd.java @@ -85,7 +85,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting Snapshot: " + this._uuidMgr.getUuid(Snapshot.class, getId()); + return "Deleting Snapshot with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -100,7 +100,7 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Snapshot Id: " + this._uuidMgr.getUuid(Snapshot.class, getId())); + CallContext.current().setEventDetails("Snapshot ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _snapshotService.deleteSnapshot(getId(), getZoneId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java index 3f0f82ea4e3b..dacdd20b3969 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/ExtractSnapshotCmd.java @@ -96,12 +96,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Snapshot extraction job"; + return "Starting Snapshot extraction for Snapshot with ID: " + getResourceUuid(ApiConstants.ID); } @Override public void execute() { - CallContext.current().setEventDetails("Snapshot ID: " + this._uuidMgr.getUuid(Snapshot.class, getId())); + CallContext.current().setEventDetails("Snapshot ID: " + getResourceUuid(ApiConstants.ID)); String uploadUrl = _snapshotService.extractSnapshot(this); logger.info("Extract URL [{}] of snapshot [{}].", uploadUrl, id); if (uploadUrl != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/RevertSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/RevertSnapshotCmd.java index 7cee7e71cf37..59881dfdabe2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/RevertSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/RevertSnapshotCmd.java @@ -74,7 +74,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "revert Snapshot: " + this._uuidMgr.getUuid(Snapshot.class, getId()); + return "Reverting Snapshot with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -89,7 +89,7 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Snapshot Id: " + this._uuidMgr.getUuid(Snapshot.class, getId())); + CallContext.current().setEventDetails("Snapshot ID: " + getResourceUuid(ApiConstants.ID)); Snapshot snapshot = _snapshotService.revertSnapshot(getId()); if (snapshot != null) { SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/UpdateSnapshotPolicyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/UpdateSnapshotPolicyCmd.java index ba98956644cb..84f8d0b3c390 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/UpdateSnapshotPolicyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/UpdateSnapshotPolicyCmd.java @@ -104,7 +104,7 @@ public String getEventDescription() { @Override public void execute() { - CallContext.current().setEventDetails("SnapshotPolicy ID: " + getId()); + CallContext.current().setEventDetails("Snapshot policy ID: " + getResourceUuid(ApiConstants.ID)); SnapshotPolicy result = _snapshotService.updateSnapshotPolicy(this); if (result != null) { SnapshotPolicyResponse response = _responseGenerator.createSnapshotPolicyResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSDiskOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSDiskOfferingCmd.java index b078ce4aae95..24290bc345e1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSDiskOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSDiskOfferingCmd.java @@ -117,7 +117,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Changing disk offering for the Shared FileSystem " + id; + return "Changing disk offering for the Shared FileSystem with ID:" + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSServiceOfferingCmd.java index 70fb543d64c3..1ac0f27067b4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ChangeSharedFSServiceOfferingCmd.java @@ -96,7 +96,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Changing service offering for the Shared FileSystem " + id; + return "Changing service offering for the Shared FileSystem with ID:" + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/DestroySharedFSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/DestroySharedFSCmd.java index 09fae53f1284..35f16a4dc2a0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/DestroySharedFSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/DestroySharedFSCmd.java @@ -95,7 +95,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Destroying Shared FileSystem " + id; + return "Destroying Shared FileSystem with ID:" + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ExpungeSharedFSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ExpungeSharedFSCmd.java index 39b99218b667..8960aa3e4d40 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ExpungeSharedFSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/ExpungeSharedFSCmd.java @@ -74,7 +74,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Expunging Shared FileSystem " + id; + return "Expunging Shared FileSystem with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/RestartSharedFSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/RestartSharedFSCmd.java index 576c472b6eb2..75565796caa4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/RestartSharedFSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/RestartSharedFSCmd.java @@ -94,7 +94,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Restarting Shared FileSystem " + id; + return "Restarting Shared FileSystem with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StartSharedFSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StartSharedFSCmd.java index bd384aceef73..d7440b532b31 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StartSharedFSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StartSharedFSCmd.java @@ -84,7 +84,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Starting Shared FileSystem " + id; + return "Starting Shared FileSystem with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StopSharedFSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StopSharedFSCmd.java index d6e0737144a5..3800b16289e7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StopSharedFSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/storage/sharedfs/StopSharedFSCmd.java @@ -92,7 +92,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Stopping Shared FileSystem " + id; + return "Stopping Shared FileSystem with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java index 66a20fac8606..02601b2257f1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java @@ -136,19 +136,21 @@ public String getEventType() { @Override public String getEventDescription() { - StringBuilder descBuilder = new StringBuilder(); - if (getDestinationZoneIds() != null) { + String description = "Copying Template: " + getResourceUuid(ApiConstants.ID); + + if (getSourceZoneId() != null) { + description += " from zone: " + getResourceUuid(ApiConstants.SOURCE_ZONE_ID); + } + if (getDestinationZoneIds() != null) { + description += " to zones: "; for (Long destId : getDestinationZoneIds()) { - descBuilder.append(", "); - descBuilder.append(this._uuidMgr.getUuid(DataCenter.class, destId)); - } - if (descBuilder.length() > 0) { - descBuilder.deleteCharAt(0); + description += this._uuidMgr.getUuid(DataCenter.class, destId); + description += ", "; } } - return "Copying Template: " + this._uuidMgr.getUuid(VirtualMachineTemplate.class, getId()) +((getSourceZoneId() != null) ? " from zone: " + this._uuidMgr.getUuid(DataCenter.class, getSourceZoneId()) : "") + ((descBuilder.length() > 0) ? " to zones: " + descBuilder.toString() : ""); + return description; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java index 76fadb7853ba..b5e41ff449ca 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java @@ -301,7 +301,7 @@ public void create() throws ResourceAllocationException { @Override public void execute() { CallContext.current().setEventDetails( - "Template Id: " + getEntityUuid() + ((getSnapshotId() == null) ? " from volume Id: " + this._uuidMgr.getUuid(Volume.class, getVolumeId()) : " from Snapshot Id: " + this._uuidMgr.getUuid(Snapshot.class, getSnapshotId()))); + "Template ID: " + getEntityUuid() + ((getSnapshotId() == null) ? " from volume with ID: " + getResourceUuid(ApiConstants.VOLUME_ID) : " from Snapshot with ID: " + getResourceUuid(ApiConstants.SNAPSHOT_ID))); VirtualMachineTemplate template = _templateService.createPrivateTemplate(this); if (template != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteTemplateCmd.java index fef70d188e59..3c7b1e2708b8 100755 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteTemplateCmd.java @@ -98,7 +98,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting Template " + this._uuidMgr.getUuid(VirtualMachineTemplate.class, getId()); + return "Deleting Template with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -113,7 +113,7 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Template Id: " + this._uuidMgr.getUuid(VirtualMachineTemplate.class, getId())); + CallContext.current().setEventDetails("Template ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _templateService.deleteTemplate(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java index b0215b12ef2e..d3f039ce38de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ExtractTemplateCmd.java @@ -28,7 +28,6 @@ import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; -import com.cloud.dc.DataCenter; import com.cloud.event.EventTypes; import com.cloud.exception.InternalErrorException; import com.cloud.template.VirtualMachineTemplate; @@ -101,15 +100,14 @@ public String getEventType() { @Override public String getEventDescription() { - String templateId = this._uuidMgr.getUuid(VirtualMachineTemplate.class, getId()); - String baseDescription = String.format("Extracting Template: %s", templateId); + String description = "Extracting Template with ID: " + getResourceUuid(ApiConstants.ID); Long zoneId = getZoneId(); - if (zoneId == null) { - return baseDescription; + if (zoneId != null) { + description += "from zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } - return String.format("%s from zone: %s", baseDescription, this._uuidMgr.getUuid(DataCenter.class, zoneId)); + return description; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java index 3882b157e6e8..9d96f104885e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java @@ -56,6 +56,9 @@ public class AddIpToVmNicCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, required = false, description = "Secondary IP Address") private String ipAddr; + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the secondary IP address", length = 2048) + private String description; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -89,7 +92,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Associating IP to NIC id=" + this._uuidMgr.getUuid(Nic.class, getNicId()) + " belonging to Network id=" + this._uuidMgr.getUuid(Network.class, getNetworkId()); + return "Associating secondary IP address to NIC with ID: " + getResourceUuid(ApiConstants.NIC_ID) + " belonging to Network with ID: " + this._uuidMgr.getUuid(Network.class, getNetworkId()); } ///////////////////////////////////////////////////// @@ -108,11 +111,11 @@ public static String getResultObjectName() { @Override public void execute() throws ResourceUnavailableException, ResourceAllocationException, ConcurrentOperationException, InsufficientCapacityException { - CallContext.current().setEventDetails("Nic Id: " + this._uuidMgr.getUuid(Nic.class, getNicId())); + CallContext.current().setEventDetails("Nic ID: " + getResourceUuid(ApiConstants.NIC_ID)); NicSecondaryIp result = _entityMgr.findById(NicSecondaryIp.class, getEntityId()); if (result != null) { - CallContext.current().setEventDetails("secondary Ip Id: " + getEntityUuid()); + CallContext.current().setEventDetails("Secondary IP address ID: " + getEntityUuid()); boolean success = false; success = _networkService.configureNicSecondaryIp(result, isZoneSGEnabled()); @@ -160,7 +163,7 @@ public void create() throws ResourceAllocationException { } try { - result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair); + result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair, description); if (result != null) { setEntityId(result.getId()); setEntityUuid(result.getUuid()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java index cf91e15601ba..f6ef955956f6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java @@ -40,7 +40,6 @@ import com.cloud.event.EventTypes; import com.cloud.exception.InvalidParameterValueException; -import com.cloud.network.Network; import com.cloud.user.Account; import com.cloud.uservm.UserVm; import com.cloud.utils.net.Dhcp; @@ -101,6 +100,26 @@ public String getMacAddress() { return NetUtils.standardizeMacAddress(macaddr); } + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public void setNetworkId(Long netId) { + this.netId = netId; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public void setMacAddress(String macaddr) { + this.macaddr = macaddr; + } + + public void setDhcpOptions(Map dhcpOptions) { + this.dhcpOptions = dhcpOptions; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -121,7 +140,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Adding Network " + this._uuidMgr.getUuid(Network.class, getNetworkId()) + " to User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()); + return "Adding NIC on Network " + getResourceUuid(ApiConstants.NETWORK_ID) + " to User Instance: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -167,7 +186,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()) + " Network Id: " + this._uuidMgr.getUuid(Network.class, getNetworkId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " Network ID: " + getResourceUuid(ApiConstants.NETWORK_ID)); UserVm result = _userVmService.addNicToVirtualMachine(this); ArrayList dc = new ArrayList(); dc.add(VMDetails.valueOf("nics")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java index 07c11b21107a..38e9921853a3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java @@ -40,12 +40,14 @@ import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.UserDataResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.kms.KMSKey; import org.apache.cloudstack.vm.lease.VMLeaseManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -61,10 +63,10 @@ import com.cloud.network.Network.IpAddresses; import com.cloud.offering.DiskOffering; import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.net.Dhcp; import com.cloud.utils.net.NetUtils; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.VmDiskInfo; -import com.cloud.utils.net.Dhcp; public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityGroupAction, UserCmd { @@ -75,13 +77,13 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme ///////////////////////////////////////////////////// @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, description = "availability zone for the virtual machine") - private Long zoneId; + protected Long zoneId; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "host name for the virtual machine", validations = {ApiArgValidator.RFCComplianceDomainName}) - private String name; + protected String name; @Parameter(name = ApiConstants.DISPLAY_NAME, type = CommandType.STRING, description = "an optional user generated name for the virtual machine") - private String displayName; + protected String displayName; @Parameter(name=ApiConstants.PASSWORD, type=CommandType.STRING, description="The password of the virtual machine. If null, a random password will be generated for the VM.", since="4.19.0.0") @@ -89,21 +91,21 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme //Owner information @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the virtual machine. Must be used with domainId.") - private String accountName; + protected String accountName; @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "an optional domainId for the virtual machine. If the account parameter is used, domainId must also be used. If account is NOT provided then virtual machine will be assigned to the caller account and domain.") - private Long domainId; + protected Long domainId; //Network information //@ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.NETWORK_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = NetworkResponse.class, description = "list of network ids used by virtual machine. Can't be specified with ipToNetworkList parameter") - private List networkIds; + protected List networkIds; @Parameter(name = ApiConstants.BOOT_TYPE, type = CommandType.STRING, required = false, description = "Guest VM Boot option either custom[UEFI] or default boot [BIOS]. Not applicable with VMware if the template is marked as deploy-as-is, as we honour what is defined in the template.", since = "4.14.0.0") - private String bootType; + protected String bootType; @Parameter(name = ApiConstants.BOOT_MODE, type = CommandType.STRING, required = false, description = "Boot Mode [Legacy] or [Secure] Applicable when Boot Type Selected is UEFI, otherwise Legacy only for BIOS. Not applicable with VMware if the template is marked as deploy-as-is, as we honour what is defined in the template.", since = "4.14.0.0") - private String bootMode; + protected String bootMode; @Parameter(name = ApiConstants.BOOT_INTO_SETUP, type = CommandType.BOOLEAN, required = false, description = "Boot into hardware setup or not (ignored if startVm = false, only valid for vmware)", since = "4.15.0.0") private Boolean bootIntoSetup; @@ -126,11 +128,19 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme since = "4.4") private Long rootdisksize; + @ACL + @Parameter(name = ApiConstants.ROOT_DISK_KMS_KEY_ID, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "ID of the KMS Key to use for root disk encryption", + since = "4.23.0") + private Long rootDiskKmsKeyId; + @Parameter(name = ApiConstants.DATADISKS_DETAILS, type = CommandType.MAP, since = "4.21.0", description = "Disk offering details for creating multiple data volumes. Mutually exclusive with diskOfferingId." + - " Example: datadisksdetails[0].diskofferingid=a2a73a84-19db-4852-8930-dfddef053341&datadisksdetails[0].size=10&datadisksdetails[0].miniops=100&datadisksdetails[0].maxiops=200") + " Example: datadisksdetails[0].diskofferingid=a2a73a84-19db-4852-8930-dfddef053341&datadisksdetails[0].size=10&datadisksdetails[0].miniops=100&datadisksdetails[0].maxiops=200&datadisksdetails[0].kmskeyid=") private Map dataDisksDetails; @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "an optional group for the virtual machine") @@ -138,7 +148,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @Parameter(name = ApiConstants.HYPERVISOR, type = CommandType.STRING, description = "the hypervisor on which to deploy the virtual machine. " + "The parameter is required and respected only when hypervisor info is not set on the ISO/Template passed to the call") - private String hypervisor; + protected String hypervisor; @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + @@ -147,10 +157,10 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576) - private String userData; + protected String userData; @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata", since = "4.18") - private Long userdataId; + protected Long userdataId; @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18") private Map userdataDetails; @@ -160,7 +170,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private String sshKeyPairName; @Parameter(name = ApiConstants.SSH_KEYPAIRS, type = CommandType.LIST, collectionType = CommandType.STRING, since="4.17", description = "names of the ssh key pairs used to login to the virtual machine") - private List sshKeyPairNames; + protected List sshKeyPairNames; @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "destination Host ID to deploy the VM to - parameter available for root admin only") private Long hostId; @@ -168,7 +178,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @ACL @Parameter(name = ApiConstants.SECURITY_GROUP_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = SecurityGroupResponse.class, description = "comma separated list of security groups id that going to be applied to the virtual machine. " + "Should be passed only when vm is created from a zone with Basic Network support." + " Mutually exclusive with securitygroupnames parameter") - private List securityGroupIdList; + protected List securityGroupIdList; @ACL @Parameter(name = ApiConstants.SECURITY_GROUP_NAMES, type = CommandType.LIST, collectionType = CommandType.STRING, entityType = SecurityGroupResponse.class, description = "comma separated list of security groups names that going to be applied to the virtual machine." @@ -188,11 +198,11 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @Parameter(name = ApiConstants.MAC_ADDRESS, type = CommandType.STRING, description = "the mac address for default vm's network") private String macAddress; - @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") - private String keyboard; + @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,es-latam,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") + protected String keyboard; @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Deploy vm for the project") - private Long projectId; + protected Long projectId; @Parameter(name = ApiConstants.START_VM, type = CommandType.BOOLEAN, description = "true if start vm after creating; defaulted to true if not specified") private Boolean startVm; @@ -200,7 +210,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @ACL @Parameter(name = ApiConstants.AFFINITY_GROUP_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = AffinityGroupResponse.class, description = "comma separated list of affinity groups id that are going to be applied to the virtual machine." + " Mutually exclusive with affinitygroupnames parameter") - private List affinityGroupIdList; + protected List affinityGroupIdList; @ACL @Parameter(name = ApiConstants.AFFINITY_GROUP_NAMES, type = CommandType.LIST, collectionType = CommandType.STRING, entityType = AffinityGroupResponse.class, description = "comma separated list of affinity groups names that are going to be applied to the virtual machine." @@ -208,10 +218,10 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private List affinityGroupNameList; @Parameter(name = ApiConstants.DISPLAY_VM, type = CommandType.BOOLEAN, since = "4.2", description = "an optional field, whether to the display the vm to the end user or not.", authorized = {RoleType.Admin}) - private Boolean displayVm; + protected Boolean displayVm; @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.3", description = "used to specify the custom parameters. 'extraconfig' is not allowed to be passed in details") - private Map details; + protected Map details; @Parameter(name = ApiConstants.DEPLOYMENT_PLANNER, type = CommandType.STRING, description = "Deployment planner to use for vm allocation. Available to ROOT admin only", since = "4.4", authorized = { RoleType.Admin }) private String deploymentPlanner; @@ -225,7 +235,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private Map dataDiskTemplateToDiskOfferingList; @Parameter(name = ApiConstants.EXTRA_CONFIG, type = CommandType.STRING, since = "4.12", description = "an optional URL encoded string that can be passed to the virtual machine upon successful deployment", length = 5120) - private String extraConfig; + protected String extraConfig; @Parameter(name = ApiConstants.COPY_IMAGE_TAGS, type = CommandType.BOOLEAN, since = "4.13", description = "if true the image tags (if any) will be copied to the VM, default value is false") private Boolean copyImageTags; @@ -300,6 +310,10 @@ public Long getDiskOfferingId() { return diskOfferingId; } + public Long getRootDiskKmsKeyId() { + return rootDiskKmsKeyId; + } + public String getDeploymentPlanner() { return deploymentPlanner; } @@ -581,7 +595,19 @@ public List getDataDiskInfoList() { minIops = Long.parseLong(dataDisk.get(ApiConstants.MIN_IOPS)); maxIops = Long.parseLong(dataDisk.get(ApiConstants.MAX_IOPS)); } - VmDiskInfo vmDiskInfo = new VmDiskInfo(diskOffering, size, minIops, maxIops, deviceId); + + // Extract KMS key ID if provided + Long kmsKeyId = null; + String kmsKeyUuid = dataDisk.get(ApiConstants.KMS_KEY_ID); + if (kmsKeyUuid != null) { + KMSKey kmsKey = _entityMgr.findByUuid(org.apache.cloudstack.kms.KMSKey.class, kmsKeyUuid); + if (kmsKey == null) { + throw new InvalidParameterValueException("Unable to find KMS key " + kmsKeyUuid); + } + kmsKeyId = kmsKey.getId(); + } + + VmDiskInfo vmDiskInfo = new VmDiskInfo(diskOffering, size, minIops, maxIops, deviceId, kmsKeyId); vmDiskInfoList.add(vmDiskInfo); } this.dataDiskInfoList = vmDiskInfoList; @@ -798,6 +824,11 @@ public IoDriverPolicy getIoDriverPolicy() { } return null; } + + public String getInstanceType() { + return null; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -828,15 +859,15 @@ public String getCreateEventType() { @Override public String getCreateEventDescription() { - return "creating Vm"; + return "Creating Instance"; } @Override public String getEventDescription() { if(getStartVm()) { - return "starting Vm. Vm Id: " + getEntityUuid(); + return "Starting Instance with ID: " + getEntityUuid(); } - return "deploying Vm. Vm Id: " + getEntityUuid(); + return "Deploying Instance with ID: " + getEntityUuid(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java index e17ba9c2d705..7743c031006b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMFromBackupCmd.java @@ -36,6 +36,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.uservm.UserVm; import com.cloud.vm.VirtualMachine; +import org.apache.commons.lang3.ObjectUtils; @APICommand(name = "createVMFromBackup", description = "Creates and automatically starts a VM from a backup.", @@ -51,6 +52,7 @@ public class CreateVMFromBackupCmd extends BaseDeployVMCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// + @ACL @Parameter(name = ApiConstants.BACKUP_ID, type = CommandType.UUID, entityType = BackupResponse.class, @@ -69,6 +71,10 @@ public class CreateVMFromBackupCmd extends BaseDeployVMCmd { @Parameter(name = ApiConstants.PRESERVE_IP, type = CommandType.BOOLEAN, description = "Use the same IP/MAC addresses as stored in the backup metadata. Works only if the original Instance is deleted and the IP/MAC address is available.") private Boolean preserveIp; + @Parameter(name = ApiConstants.QUICK_RESTORE, type = CommandType.BOOLEAN, entityType = BackupResponse.class, description = "Whether to use the quick restore process or not. " + + "Currently this parameter is only supported by the KBOSS provider.", since = "4.23.0") + private Boolean quickRestore; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -89,6 +95,10 @@ public boolean getPreserveIp() { return (preserveIp != null) ? preserveIp : false; } + public Boolean getQuickRestore() { + return ObjectUtils.defaultIfNull(this.quickRestore, false); + } + @Override public void create() { UserVm vm; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmd.java index 7e9bdd942ed7..1fce8caf8e1a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmd.java @@ -22,23 +22,26 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import javax.inject.Inject; import java.util.Date; +@Deprecated @APICommand(name = "createVMSchedule", description = "Create Instance Schedule", responseObject = VMScheduleResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateVMScheduleCmd extends BaseCmd { @Inject - VMScheduleManager vmScheduleManager; + ResourceScheduleManager resourceScheduleManager; @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, @@ -75,14 +78,14 @@ public class CreateVMScheduleCmd extends BaseCmd { type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active. Defaults to current date plus 1 minute." - + "Use format \"yyyy-MM-dd hh:mm:ss\")") + + "(Format \"yyyy-MM-dd hh:mm:ss\")") private Date startDate; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive" - + "Use format \"yyyy-MM-dd hh:mm:ss\")") + + "(Format \"yyyy-MM-dd hh:mm:ss\")") private Date endDate; @Parameter(name = ApiConstants.ENABLED, @@ -91,9 +94,9 @@ public class CreateVMScheduleCmd extends BaseCmd { description = "Enable Instance schedule. Defaults to true") private Boolean enabled; - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// public Long getVmId() { return vmId; @@ -130,13 +133,19 @@ public Boolean getEnabled() { return enabled; } - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// @Override public void execute() { - VMScheduleResponse response = vmScheduleManager.createSchedule(this); + String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null; + + ResourceScheduleResponse scheduleResponse = resourceScheduleManager.createSchedule( + ApiCommandResourceType.VirtualMachine, + resourceIdStr, getDescription(), getSchedule(), getTimeZone(), getAction(), + getStartDate(), getEndDate(), getEnabled(), null); + VMScheduleResponse response = new VMScheduleResponse(scheduleResponse); response.setResponseName(getCommandName()); setResponseObject(response); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmd.java index f34d07b045d9..f40db4ee0487 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmd.java @@ -22,6 +22,7 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; @@ -30,19 +31,19 @@ import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import javax.inject.Inject; import java.util.Collections; import java.util.List; +@Deprecated @APICommand(name = "deleteVMSchedule", description = "Delete Instance Schedule.", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteVMScheduleCmd extends BaseCmd { @Inject - VMScheduleManager vmScheduleManager; + ResourceScheduleManager resourceScheduleManager; @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, @@ -50,12 +51,14 @@ public class DeleteVMScheduleCmd extends BaseCmd { required = true, description = "ID of Instance") private Long vmId; + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VMScheduleResponse.class, required = false, description = "ID of Instance schedule") private Long id; + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, @@ -64,9 +67,9 @@ public class DeleteVMScheduleCmd extends BaseCmd { description = "IDs of Instance schedule") private List ids; - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// public Long getId() { return id; @@ -83,18 +86,21 @@ public Long getVmId() { return vmId; } - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// @Override public void execute() { - long rowsRemoved = vmScheduleManager.removeSchedule(this); + String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null; + long rowsRemoved = resourceScheduleManager.removeSchedule( + ApiCommandResourceType.VirtualMachine, + resourceIdStr, getId(), getIds()); if (rowsRemoved > 0) { final SuccessResponse response = new SuccessResponse(); response.setResponseName(getCommandName()); - response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase()); + response.setObjectName("vmschedule"); setResponseObject(response); } else { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete Instance Schedules"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 528a8b0c7359..0a943fab118d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -16,10 +16,11 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Stream; -import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -40,6 +41,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.uservm.UserVm; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; @APICommand(name = "deployVirtualMachine", description = "Creates and automatically starts an Instance based on a service offering, disk offering, and Template.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, @@ -88,11 +90,116 @@ public boolean isVolumeOrSnapshotProvided() { return volumeId != null || snapshotId != null; } + public boolean isBlankInstance() { + return false; + } + + + + ///////////////////////////////////////////////////// + ////////////////// Setters ////////////////////////// + ///////////////////////////////////////////////////// + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public void setName(String name) { + this.name = name; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setNetworkIds(List networkIds) { + this.networkIds = networkIds; + } + + public void setBootType(String bootType) { + this.bootType = bootType; + } + + public void setBootMode(String bootMode) { + this.bootMode = bootMode; + } + + public void setHypervisor(String hypervisor) { + this.hypervisor = hypervisor; + } + + public void setUserData(String userData) { + this.userData = userData; + } + + public void setKeyboard(String keyboard) { + this.keyboard = keyboard; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + public void setDisplayVm(Boolean displayVm) { + this.displayVm = displayVm; + } + + public void setUserDataId(Long userDataId) { + this.userdataId = userDataId; + } + + public void setAffinityGroupIds(List ids) { + this.affinityGroupIdList = ids; + } + + public void setDetails(Map details) { + this.details = details; + } + + public void setExtraConfig(String extraConfig) { + this.extraConfig = extraConfig; + } + + public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) { + this.dynamicScalingEnabled = dynamicScalingEnabled; + } + + public void setServiceOfferingId(Long serviceOfferingId) { + this.serviceOfferingId = serviceOfferingId; + } + + public void setTemplateId(Long templateId) { + this.templateId = templateId; + } + + public void setVolumeId(Long volumeId) { + this.volumeId = volumeId; + } + + public void setSnapshotId(Long snapshotId) { + this.snapshotId = snapshotId; + } + + public void setSshKeyPairNames(List sshKeyPairNames) { + this.sshKeyPairNames = sshKeyPairNames; + } + + public void setSecurityGroupList(List securityGroupIdList) { + this.securityGroupIdList = securityGroupIdList; + } + @Override public void execute() { UserVm result; - CallContext.current().setEventDetails("Instance Id: " + getEntityUuid()); + CallContext.current().setEventDetails("Instance ID: " + getEntityUuid()); if (getStartVm()) { try { result = _userVmService.startVirtualMachine(this); @@ -132,7 +239,7 @@ public void execute() { @Override public void create() throws ResourceAllocationException { - if (Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) { + if (!isBlankInstance() && Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) { throw new CloudRuntimeException("Please provide only one of the following parameters - template ID, volume ID or snapshot ID"); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 1ca73c0cb3cd..0a3e510d3d53 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -90,10 +90,22 @@ public List getVolumeIds() { return volumeIds; } + public boolean isForced() { + return false; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// + public DestroyVMCmd() { + } + + public DestroyVMCmd(Long id, Boolean expunge) { + this.id = id; + this.expunge = expunge; + } + @Override public String getCommandName() { return s_name; @@ -116,7 +128,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "destroying Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Destroying Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -131,8 +143,8 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, ConcurrentOperationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); - UserVm result = _userVmService.destroyVm(this); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); + UserVm result = _userVmService.destroyVm(this, true); UserVmResponse response = new UserVmResponse(); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmd.java index be94315abe76..c7a9a75f2eac 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmd.java @@ -20,23 +20,27 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +@Deprecated @APICommand(name = "listVMSchedule", description = "List Instance Schedules.", responseObject = VMScheduleResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListVMScheduleCmd extends BaseListCmd { @Inject - VMScheduleManager vmScheduleManager; + ResourceScheduleManager resourceScheduleManager; @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, @@ -61,12 +65,12 @@ public class ListVMScheduleCmd extends BaseListCmd { @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, - description = "ID of Instance schedule") + description = "Filter by enabled status") private Boolean enabled; - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// public Long getVmId() { return vmId; @@ -84,14 +88,26 @@ public Boolean getEnabled() { return enabled; } - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// @Override public void execute() { - ListResponse response = vmScheduleManager.listSchedule(this); + String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null; + + ListResponse scheduleResponse = resourceScheduleManager.listSchedule( + getId(), null, ApiCommandResourceType.VirtualMachine, resourceIdStr, getAction(), getEnabled(), + getStartIndex(), getPageSizeVal() + ); + + List vmScheduleResponses = new ArrayList<>(); + for (ResourceScheduleResponse resourceScheduleResponse : scheduleResponse.getResponses()) { + vmScheduleResponses.add(new VMScheduleResponse(resourceScheduleResponse)); + } + ListResponse response = new ListResponse<>(); + response.setResponses(vmScheduleResponses, scheduleResponse.getCount()); response.setResponseName(getCommandName()); - response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase()); + response.setObjectName("vmschedule"); setResponseObject(response); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java index 32756755f395..6f4431547848 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RebootVMCmd.java @@ -100,7 +100,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Rebooting User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Rebooting User Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -115,7 +115,7 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result; result = _userVmService.rebootVirtualMachine(this); if (result !=null){ diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java index 09c84fdbb380..f4c4d82b30d4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveIpFromVmNicCmd.java @@ -93,7 +93,7 @@ public NicSecondaryIp getIpEntry() { @Override public String getEventDescription() { - return ("Disassociating ip address with id=" + id); + return "Disassociating IP address with ID:" + getResourceUuid(ApiConstants.ID); } ///////////////////////////////////////////////////// @@ -132,7 +132,7 @@ private boolean isZoneSGEnabled() { @Override public void execute() throws InvalidParameterValueException { - CallContext.current().setEventDetails("Ip Id: " + id); + CallContext.current().setEventDetails("IP address ID: " + getResourceUuid(ApiConstants.ID)); NicSecondaryIp nicSecIp = getIpEntry(); if (nicSecIp == null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveNicFromVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveNicFromVMCmd.java index 8a891e824eea..cfbc64339909 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveNicFromVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RemoveNicFromVMCmd.java @@ -19,8 +19,6 @@ import java.util.ArrayList; import java.util.EnumSet; -import com.cloud.vm.Nic; - import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; @@ -89,7 +87,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Removing NIC " + this._uuidMgr.getUuid(Nic.class, getNicId()) + " from User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()); + return "Removing NIC with ID: " + getResourceUuid(ApiConstants.NIC_ID) + " from User Instance: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -103,7 +101,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()) + " Nic Id: " + this._uuidMgr.getUuid(Nic.class, getNicId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " NIC ID: " + getResourceUuid(ApiConstants.NIC_ID)); UserVm result = _userVmService.removeNicFromVirtualMachine(this); ArrayList dc = new ArrayList(); dc.add(VMDetails.valueOf("nics")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMPasswordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMPasswordCmd.java index 5302675fb5f1..b6179efc0d35 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMPasswordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMPasswordCmd.java @@ -101,7 +101,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "resetting password for Instance: " + getId(); + return "Resetting password for Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -124,7 +124,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE } else { logger.debug("Resetting VM [{}] password to password defined by user.", vm.getUuid()); } - CallContext.current().setEventDetails("Vm Id: " + getId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result = _userVmService.resetVMPassword(this, password); if (result != null){ UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", result).get(0); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMSSHKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMSSHKeyCmd.java index 530677edf383..73e4ec623f23 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMSSHKeyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMSSHKeyCmd.java @@ -121,7 +121,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "resetting SSHKey for Instance: " + getId(); + return "Resetting SSH key for Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -152,7 +152,7 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Vm Id: " + getId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result = _userVmService.resetVMSSHKey(this); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java index 9fb60b537c5a..8c513549506f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ResetVMUserDataCmd.java @@ -143,7 +143,7 @@ public Long getApiResourceId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException { - CallContext.current().setEventDetails("Vm Id: " + getId()); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result = _userVmService.resetVMUserData(this); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RestoreVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RestoreVMCmd.java index b92b2d1b3c16..d3459347687a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RestoreVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/RestoreVMCmd.java @@ -90,14 +90,14 @@ public String getEventType() { @Override public String getEventDescription() { - return "Restore an Instance to original Template or specific Snapshot"; + return "Restoring Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " to original Template or specific Snapshot"; } @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException { UserVm result; - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID)); result = _userVmService.restoreVM(this); if (result != null) { UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", result).get(0); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ScaleVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ScaleVMCmd.java index cd3aeefc44b2..36d0ad9c6500 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ScaleVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ScaleVMCmd.java @@ -39,7 +39,6 @@ import com.cloud.exception.ManagementServerException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.VirtualMachineMigrationException; -import com.cloud.offering.ServiceOffering; import com.cloud.user.Account; import com.cloud.uservm.UserVm; import com.cloud.vm.VirtualMachine; @@ -148,7 +147,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Upgrading Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()) + " to service offering: " + this._uuidMgr.getUuid(ServiceOffering.class, getServiceOfferingId()); + return "Upgrading Instance with ID: " + getResourceUuid(ApiConstants.ID) + " to service offering with ID: " + getResourceUuid(ApiConstants.SERVICE_OFFERING_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java index af7058d44923..40ae91d4c264 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java @@ -161,7 +161,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Starting User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Starting User Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -177,7 +177,7 @@ public Long getApiResourceId() { @Override public void execute() { try { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result; result = _userVmService.startVirtualMachine(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StopVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StopVMCmd.java index d20f27eb56db..232eeebd34be 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StopVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StopVMCmd.java @@ -96,7 +96,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Stopping User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + return "Stopping User Instance with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -115,7 +115,7 @@ public boolean isForced() { @Override public void execute() throws ServerApiException, ConcurrentOperationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); UserVm result; result = _userVmService.stopVirtualMachine(getId(), isForced()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateDefaultNicForVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateDefaultNicForVMCmd.java index 591d3871493e..011edb1a9df4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateDefaultNicForVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateDefaultNicForVMCmd.java @@ -19,8 +19,6 @@ import java.util.ArrayList; import java.util.EnumSet; -import com.cloud.vm.Nic; - import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; @@ -90,7 +88,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Updating NIC " + this._uuidMgr.getUuid(Nic.class, getNicId()) + " on User Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()); + return "Setting NIC " + getResourceUuid(ApiConstants.NIC_ID) + " as default to User Instance: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -104,7 +102,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()) + " Nic Id: " + this._uuidMgr.getUuid(Nic.class, getNicId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID) + " NIC ID: " + getResourceUuid(ApiConstants.NIC_ID)); UserVm result = _userVmService.updateDefaultNicForVirtualMachine(this); ArrayList dc = new ArrayList(); dc.add(VMDetails.valueOf("nics")); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java index 115b37d61d17..a5f06e6ecbb3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java @@ -288,6 +288,14 @@ public boolean isCleanupExtraConfig() { return Boolean.TRUE.equals(cleanupExtraConfig); } + public void setId(Long id) { + this.id = id; + } + + public void setSecurityGroupIdList(List securityGroupIdList) { + this.securityGroupIdList = securityGroupIdList; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -313,7 +321,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { - CallContext.current().setEventDetails("Instance Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + ApiConstants.ID); UserVm result = null; try { result = _userVmService.updateVirtualMachine(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmd.java index b7222944fe07..9b5b05373102 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmd.java @@ -22,22 +22,25 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceSchedule; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import javax.inject.Inject; import java.util.Date; +@Deprecated @APICommand(name = "updateVMSchedule", description = "Update Instance Schedule.", responseObject = VMScheduleResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class UpdateVMScheduleCmd extends BaseCmd { @Inject - VMScheduleManager vmScheduleManager; + ResourceScheduleManager resourceScheduleManager; @Parameter(name = ApiConstants.ID, type = CommandType.UUID, @@ -68,14 +71,14 @@ public class UpdateVMScheduleCmd extends BaseCmd { type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active" - + "Use format \"yyyy-MM-dd hh:mm:ss\")") + + "(Format \"yyyy-MM-dd hh:mm:ss\")") private Date startDate; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive" - + "Use format \"yyyy-MM-dd hh:mm:ss\")") + + "(Format \"yyyy-MM-dd hh:mm:ss\")") private Date endDate; @Parameter(name = ApiConstants.ENABLED, @@ -84,9 +87,9 @@ public class UpdateVMScheduleCmd extends BaseCmd { description = "Enable Instance schedule") private Boolean enabled; - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// public Long getId() { return id; @@ -116,24 +119,29 @@ public Boolean getEnabled() { return enabled; } - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// @Override public void execute() { - VMScheduleResponse response = vmScheduleManager.updateSchedule(this); + ResourceScheduleResponse scheduleResponse = resourceScheduleManager.updateSchedule( + getId(), getDescription(), getSchedule(), getTimeZone(), getStartDate(), getEndDate(), getEnabled(), null); + VMScheduleResponse response = new VMScheduleResponse(scheduleResponse); response.setResponseName(getCommandName()); setResponseObject(response); } @Override public long getEntityOwnerId() { - VMSchedule vmSchedule = _entityMgr.findById(VMSchedule.class, getId()); - if (vmSchedule == null) { - throw new InvalidParameterValueException(String.format("Unable to find vmSchedule by id=%d", getId())); + ResourceSchedule schedule = _entityMgr.findById(ResourceSchedule.class, getId()); + if (schedule == null || !ApiCommandResourceType.VirtualMachine.equals(schedule.getResourceType())) { + throw new InvalidParameterValueException(String.format("Unable to find VM schedule by id=%d", getId())); + } + VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, schedule.getResourceId()); + if (vm == null) { + throw new InvalidParameterValueException(String.format("Unable to find VM schedule by id=%d", getId())); } - VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, vmSchedule.getVmId()); return vm.getAccountId(); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicCmd.java new file mode 100644 index 000000000000..363273a4670d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicCmd.java @@ -0,0 +1,95 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.vm; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NicResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; + +import java.util.ArrayList; +import java.util.EnumSet; + +@APICommand(name = "updateVmNic", description = "Updates the specified VM NIC", responseObject = NicResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = { RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User }) +public class UpdateVmNicCmd extends BaseAsyncCmd { + + @ACL + @Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true, description = "NIC ID") + private Long nicId; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "If true, sets the NIC state to UP; otherwise, sets the NIC state to DOWN") + private Boolean enabled; + + public Long getNicId() { + return nicId; + } + + public Boolean isEnabled() { + return enabled; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_NIC_UPDATE; + } + + @Override + public String getEventDescription() { + return String.format("Updating NIC %s.", getResourceUuid(ApiConstants.NIC_ID)); + } + + @Override + public long getEntityOwnerId() { + UserVm vm = _responseGenerator.findUserVmByNicId(nicId); + if (vm == null) { + return Account.ACCOUNT_ID_SYSTEM; + } + return vm.getAccountId(); + } + + @Override + public void execute() { + CallContext.current().setEventDetails(String.format("NIC ID: %s", getResourceUuid(ApiConstants.NIC_ID))); + + UserVm result = _userVmService.updateVirtualMachineNic(this); + + ArrayList dc = new ArrayList<>(); + dc.add(ApiConstants.VMDetails.valueOf("nics")); + EnumSet details = EnumSet.copyOf(dc); + + if (result != null){ + UserVmResponse response = _responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Restricted, "virtualmachine", details, result).get(0); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update NIC from VM."); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicIpCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicIpCmd.java index f8ca0b7afea0..6da34c7ef0b0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicIpCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVmNicIpCmd.java @@ -53,7 +53,7 @@ public class UpdateVmNicIpCmd extends BaseAsyncCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// @Parameter(name=ApiConstants.NIC_ID, type=CommandType.UUID, entityType = NicResponse.class, required = true, - description = "The ID of the NIC to which you want to assign private IP") + description = "The ID of the NIC to which you want to assign private IP") private Long nicId; @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, required = false, @@ -123,7 +123,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Associating IP to NIC id: " + this._uuidMgr.getUuid(Network.class, getNetworkId()) + " in zone " + this._uuidMgr.getUuid(DataCenter.class, getZoneId()); + return "Associating IP to NIC with ID: " + getResourceUuid(ApiConstants.NIC_ID) + " in zone " + this._uuidMgr.getUuid(DataCenter.class, getZoneId()); } ///////////////////////////////////////////////////// @@ -139,7 +139,7 @@ public static String getResultObjectName() { public void execute() throws ResourceUnavailableException, ResourceAllocationException, ConcurrentOperationException, InsufficientCapacityException { - CallContext.current().setEventDetails("Nic Id: " + getNicId() ); + CallContext.current().setEventDetails("NIC ID: " + getResourceUuid(ApiConstants.NIC_ID)); String ip; if ((ip = getIpaddress()) != null) { if (!NetUtils.isValidIp4(ip)) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpgradeVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpgradeVMCmd.java index c45a18d2fa2b..83908802a690 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpgradeVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpgradeVMCmd.java @@ -139,7 +139,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceAllocationException { - CallContext.current().setEventDetails("Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.ID)); ServiceOffering serviceOffering = _entityMgr.findById(ServiceOffering.class, serviceOfferingId); if (serviceOffering == null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java index 6e1a7daf4c23..d3128599b616 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/CreateVMSnapshotCmd.java @@ -34,7 +34,6 @@ import com.cloud.event.EventTypes; import com.cloud.exception.ResourceAllocationException; import com.cloud.uservm.UserVm; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.snapshot.VMSnapshot; @APICommand(name = "createVMSnapshot", description = "Creates Snapshot for an Instance.", responseObject = VMSnapshotResponse.class, since = "4.2.0", entityType = {VMSnapshot.class}, @@ -105,7 +104,7 @@ public void create() throws ResourceAllocationException { @Override public String getEventDescription() { - return "Creating Snapshot for Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId()); + return "Creating Snapshot for Instance: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override @@ -115,7 +114,7 @@ public String getEventType() { @Override public void execute() { - CallContext.current().setEventDetails("VM Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVmId())); + CallContext.current().setEventDetails("Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID)); VMSnapshot result = _vmSnapshotService.createVMSnapshot(getVmId(), getEntityId(), getQuiescevm()); if (result != null) { VMSnapshotResponse response = _responseGenerator.createVMSnapshotResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/DeleteVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/DeleteVMSnapshotCmd.java index afd63e8e64be..3373ac534cc8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/DeleteVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/DeleteVMSnapshotCmd.java @@ -62,7 +62,7 @@ public long getEntityOwnerId() { @Override public void execute() { - CallContext.current().setEventDetails("vmsnapshot id: " + this._uuidMgr.getUuid(VMSnapshot.class, getId())); + CallContext.current().setEventDetails("Instance Snapshot ID: " + getResourceUuid(ApiConstants.VM_SNAPSHOT_ID)); boolean result = _vmSnapshotService.deleteVMSnapshot(getId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); @@ -74,7 +74,7 @@ public void execute() { @Override public String getEventDescription() { - return "Delete Instance Snapshot: " + this._uuidMgr.getUuid(VMSnapshot.class, getId()); + return "Deleting Instance Snapshot with ID: " + getResourceUuid(ApiConstants.VM_SNAPSHOT_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/RevertToVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/RevertToVMSnapshotCmd.java index 43f20362e98d..d44cefca5027 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/RevertToVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vmsnapshot/RevertToVMSnapshotCmd.java @@ -74,7 +74,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException, ConcurrentOperationException { - CallContext.current().setEventDetails("vmsnapshot id: " + this._uuidMgr.getUuid(VMSnapshot.class, getVmSnapShotId())); + CallContext.current().setEventDetails("Instance Snapshot ID: " + getResourceUuid(ApiConstants.VM_SNAPSHOT_ID)); UserVm result = _vmSnapshotService.revertToSnapshot(getVmSnapShotId()); if (result != null) { UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), @@ -88,7 +88,7 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacity @Override public String getEventDescription() { - return "Revert from Instance Snapshot: " + this._uuidMgr.getUuid(VMSnapshot.class, getVmSnapShotId()); + return "Reverting from Instance Snapshot with ID: " + getResourceUuid(ApiConstants.VM_SNAPSHOT_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AddResourceDetailCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AddResourceDetailCmd.java index b1e58bb6ef63..9e1c1056fdf9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AddResourceDetailCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AddResourceDetailCmd.java @@ -90,7 +90,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "adding details to the resource "; + return "Adding details to the resource "; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java index f39853512281..f50abaf73c96 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java @@ -70,6 +70,21 @@ public Long getProjectid() { return projectid; } + ///////////////////////////////////////////////////// + /////////////////// Setter/////////////////////////// + ///////////////////////////////////////////////////// + public void setVolumeId(Long volumeId) { + this.volumeId = volumeId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setProjectId(Long projectid) { + this.projectid = projectid; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AttachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AttachVolumeCmd.java index 23fbc0fa0c75..8624043afc51 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AttachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AttachVolumeCmd.java @@ -111,12 +111,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Attaching volume: " + this._uuidMgr.getUuid(Volume.class, getId()) + " to Instance: " + this._uuidMgr.getUuid(VirtualMachine.class, getVirtualMachineId()); + return "Attaching volume with ID: " + getResourceUuid(ApiConstants.ID) + " to Instance with ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId()) + " Instance Id: " + this._uuidMgr.getUuid(VirtualMachine.class, getVirtualMachineId())); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID) + " Instance ID: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID)); Volume result = _volumeService.attachVolumeToVM(this); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(getResponseView(), result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ChangeOfferingForVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ChangeOfferingForVolumeCmd.java index 77c30aa6be92..c8cda7e1c19c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ChangeOfferingForVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ChangeOfferingForVolumeCmd.java @@ -22,7 +22,6 @@ import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; -import com.cloud.offering.DiskOffering; import com.cloud.storage.Volume; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; @@ -130,12 +129,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "Changing Disk offering of Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId()) + " to " + this._uuidMgr.getUuid(DiskOffering.class, getNewDiskOfferingId()); + return "Changing disk offering of volume with ID: " + getResourceUuid(ApiConstants.ID) + " to " + getResourceUuid(ApiConstants.DISK_OFFERING_ID); } @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { - CallContext.current().setEventDetails("Volume Id: " + getId()); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); Volume result = _volumeService.changeDiskOfferingForVolume(this); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseObject.ResponseView.Restricted, result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java index 56fdf6bc126c..fdbd4a61c072 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CheckAndRepairVolumeCmd.java @@ -105,7 +105,7 @@ public String getEventType() { @Override public String getEventDescription() { - return String.format("check and repair operation on volume: %s", this._uuidMgr.getUuid(Volume.class, getId())); + return "Starting checking and repairing operation on volume: " + getResourceUuid(ApiConstants.ID); } @Override @@ -120,7 +120,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ResourceAllocationException { - CallContext.current().setEventDetails("Volume Id: " + getId()); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); Pair result = _volumeService.checkAndRepairVolume(this); Volume volume = _responseGenerator.findVolumeById(getId()); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java index 5938bdb810f5..538e263ae9de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java @@ -30,8 +30,10 @@ import org.apache.cloudstack.api.command.user.UserCmd; import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; @@ -109,6 +111,21 @@ public class CreateVolumeCmd extends BaseAsyncCreateCustomIdCmd implements UserC description = "The ID of the Instance; to be used with snapshot Id, Instance to which the volume gets attached after creation") private Long virtualMachineId; + @Parameter(name = ApiConstants.KMS_KEY_ID, + type = CommandType.UUID, + entityType = KMSKeyResponse.class, + description = "ID of the KMS Key for volume encryption (required if encryption enabled for zone)", + since = "4.23.0") + private Long kmsKeyId; + + @Parameter(name = ApiConstants.STORAGE_ID, + type = CommandType.UUID, + entityType = StoragePoolResponse.class, + description = "Storage pool ID to create the volume in. Cannot be used with the snapshotid parameter.", + authorized = {RoleType.Admin}, + since = "4.22.1") + private Long storageId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -142,6 +159,10 @@ public Long getMaxIops() { } public Long getSnapshotId() { + if (storageId != null && snapshotId != null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Snapshot ID cannot be specified with the Storage ID."); + } return snapshotId; } @@ -153,6 +174,14 @@ private Long getProjectId() { return projectId; } + public Long getStorageId() { + if (snapshotId != null && storageId != null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Storage ID cannot be specified with the Snapshot ID."); + } + return storageId; + } + public Boolean getDisplayVolume() { return displayVolume; } @@ -169,6 +198,10 @@ public Long getVirtualMachineId() { return virtualMachineId; } + public Long getKmsKeyId() { + return kmsKeyId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -203,7 +236,17 @@ public String getEventType() { @Override public String getEventDescription() { - return "Creating volume: " + getVolumeName() + ((getSnapshotId() == null) ? "" : " from Snapshot: " + this._uuidMgr.getUuid(Snapshot.class, getSnapshotId())); + String description = "Creating volume "; + + if (getVolumeName() != null) { + description += getVolumeName(); + } + + if (getSnapshotId() != null) { + description += " from Snapshot: " + getResourceUuid(ApiConstants.SNAPSHOT_ID); + } + + return description; } @Override @@ -220,7 +263,7 @@ public void create() throws ResourceAllocationException { @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + getEntityUuid() + ((getSnapshotId() == null) ? "" : " from Snapshot: " + this._uuidMgr.getUuid(Snapshot.class, getSnapshotId()))); + CallContext.current().setEventDetails("Volume ID: " + getEntityUuid() + ((getSnapshotId() == null) ? "" : " from Snapshot with ID: " + getResourceUuid(ApiConstants.SNAPSHOT_ID))); Volume volume = _volumeService.createVolume(this); if (volume != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(getResponseView(), volume); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java index e21103654c81..ab6cda651b06 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DeleteVolumeCmd.java @@ -84,8 +84,8 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() throws ConcurrentOperationException { - CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId())); - Volume result = _volumeService.destroyVolume(id, CallContext.current().getCallingAccount(), true, false); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); + Volume result = _volumeService.destroyVolume(id, CallContext.current().getCallingAccount(), true, false, null); if (result != null) { SuccessResponse response = new SuccessResponse(getCommandName()); setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java index 32ddec880861..e9e388436642 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DestroyVolumeCmd.java @@ -100,7 +100,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "destroying volume: " + getId(); + return "Destroying volume with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -115,8 +115,8 @@ public Long getApiResourceId() { @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + getId()); - Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); + Volume result = _volumeService.destroyVolume(getId(), CallContext.current().getCallingAccount(), getExpunge(), false, null); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Restricted, result); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java index 9c8b8fcf6e67..f6e811da6055 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java @@ -38,7 +38,7 @@ import com.cloud.uservm.UserVm; import com.cloud.vm.VirtualMachine; -@APICommand(name = "detachVolume", description = "Detaches a disk volume from an Instance.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, +@APICommand(name = "detachVolume", description = "Detaches a disk volume from an Instance.", responseObject = VolumeResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class DetachVolumeCmd extends BaseAsyncCmd implements UserCmd { private static final String s_name = "detachvolumeresponse"; @@ -77,6 +77,10 @@ public Long getVirtualMachineId() { return virtualMachineId; } + public void setId(Long id) { + this.id = id; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -126,15 +130,19 @@ public String getEventType() { @Override public String getEventDescription() { - StringBuilder sb = new StringBuilder(); + String description = "Detaching volume"; + if (id != null) { - sb.append(": " + this._uuidMgr.getUuid(Volume.class, id)); - } else if ((deviceId != null) && (virtualMachineId != null)) { - sb.append(" with device id: " + deviceId + " from Instance: " + ((getVirtualMachineId() != null) ? this._uuidMgr.getUuid(VirtualMachine.class, getVirtualMachineId()) : "" )); + description += ": " + getResourceUuid(ApiConstants.ID); + } + + if ((deviceId != null) && (virtualMachineId != null)) { + description += " with device id: " + deviceId + " from Instance: " + getResourceUuid(ApiConstants.VIRTUAL_MACHINE_ID); } else { - sb.append(" "); + description += " "; } - return "detaching volume" + sb.toString(); + + return description; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java index 2b225f4fd347..f50d23a6b60c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ExtractVolumeCmd.java @@ -16,8 +16,6 @@ // under the License. package org.apache.cloudstack.api.command.user.volume; - -import com.cloud.dc.DataCenter; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; @@ -115,10 +113,7 @@ public String getEventType() { @Override public String getEventDescription() { - String volumeId = this._uuidMgr.getUuid(Volume.class, getId()); - String zoneId = this._uuidMgr.getUuid(DataCenter.class, getZoneId()); - - return String.format("Extracting volume: %s from zone: %s", volumeId, zoneId); + return "Extracting volume with ID: " + getResourceUuid(ApiConstants.ID) + " from zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java index a4cd299dae9c..88f87e941e78 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ListVolumesCmd.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.PodResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; @@ -90,6 +91,9 @@ public class ListVolumesCmd extends BaseListRetrieveOnlyResourceCountCmd impleme @Parameter(name = ApiConstants.DISK_OFFERING_ID, type = CommandType.UUID, entityType = DiskOfferingResponse.class, description = "List volumes by disk offering", since = "4.4") private Long diskOfferingId; + @Parameter(name = ApiConstants.KMS_KEY_ID, type = CommandType.UUID, entityType = KMSKeyResponse.class, description = "List volumes by KMS Key", since = "4.23") + private Long kmsKeyId; + @Parameter(name = ApiConstants.DISPLAY_VOLUME, type = CommandType.BOOLEAN, description = "List resources by display flag; only ROOT admin is eligible to pass this parameter", since = "4.4", authorized = { RoleType.Admin}) private Boolean display; @@ -136,6 +140,10 @@ public Long getDiskOfferingId() { return diskOfferingId; } + public Long getKmsKeyId() { + return kmsKeyId; + } + public String getType() { return type; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java index ea6890ac3e82..9927978ad55e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java @@ -30,7 +30,6 @@ import org.apache.cloudstack.api.response.VolumeResponse; import com.cloud.event.EventTypes; -import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; import com.cloud.user.Account; @@ -110,7 +109,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Attempting to migrate volume Id: " + this._uuidMgr.getUuid(Volume.class, getVolumeId()) + " to storage pool Id: " + this._uuidMgr.getUuid(StoragePool.class, getStoragePoolId()); + return "Attempting to migrate volume with ID: " + getResourceUuid(ApiConstants.VOLUME_ID) + " to storage pool: " + getResourceUuid(ApiConstants.STORAGE_ID); } public Long getNewDiskOfferingId() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java index cd5a7735e382..4d6be7270afb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/RecoverVolumeCmd.java @@ -87,7 +87,7 @@ public ApiCommandResourceType getApiResourceType() { @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + getId()); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); Volume result = _volumeService.recoverVolume(getId()); if (result != null) { VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java index 5b2b6709cf64..f8f744285c04 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java @@ -190,11 +190,13 @@ public String getEventType() { @Override public String getEventDescription() { + String baseDescription = "Resizing volume with ID: " + getResourceUuid(ApiConstants.ID); + if (getSize() != null) { - return "Volume Id: " + this._uuidMgr.getUuid(Volume.class, getEntityId()) + " to size " + getSize() + " GB"; - } else { - return "Volume Id: " + this._uuidMgr.getUuid(Volume.class, getEntityId()); + baseDescription = baseDescription + " to size " + getSize() + " GB."; } + + return baseDescription; } @Override @@ -202,9 +204,9 @@ public void execute() { Volume volume = null; try { if (size != null) { - CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getEntityId()) + " to size " + getSize() + " GB"); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID) + " to size " + getSize() + " GB"); } else { - CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getEntityId())); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); } volume = _volumeService.resizeVolume(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UpdateVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UpdateVolumeCmd.java index 0d3fc59a5285..00c50fa5ffff 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UpdateVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UpdateVolumeCmd.java @@ -178,7 +178,7 @@ public String getEventDescription() { @Override public void execute() { - CallContext.current().setEventDetails("Volume Id: " + this._uuidMgr.getUuid(Volume.class, getId())); + CallContext.current().setEventDetails("Volume ID: " + getResourceUuid(ApiConstants.ID)); Volume result = _volumeService.updateVolume(getId(), getPath(), getState(), getStorageId(), getDisplayVolume(), getDeleteProtection(), getCustomId(), getEntityOwnerId(), getChainInfo(), getName()); if (result != null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UploadVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UploadVolumeCmd.java index 81077deff654..33a9251e094f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UploadVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/UploadVolumeCmd.java @@ -32,7 +32,6 @@ import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; -import com.cloud.dc.DataCenter; import com.cloud.event.EventTypes; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; @@ -169,7 +168,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "uploading volume: " + getVolumeName() + " in the zone " + this._uuidMgr.getUuid(DataCenter.class, getZoneId()); + return "Uploading volume: " + getVolumeName() + " to zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java index 1605e4830bd1..9b5d25aa0ddb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java @@ -46,7 +46,6 @@ public class CreateStaticRouteCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.GATEWAY_ID, type = CommandType.UUID, entityType = PrivateGatewayResponse.class, - required = true, description = "The gateway ID we are creating static route for. Mutually exclusive with the nexthop parameter") private Long gatewayId; @@ -116,7 +115,7 @@ public void execute() throws ResourceUnavailableException { boolean success = false; StaticRoute route = null; try { - CallContext.current().setEventDetails("Static route Id: " + getEntityId()); + CallContext.current().setEventDetails("Static route ID: " + getEntityUuid()); success = _vpcService.applyStaticRoute(getEntityId()); // State is different after the route is applied, so retrieve the object only here route = _entityMgr.findById(StaticRoute.class, getEntityId()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateVPCCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateVPCCmd.java index 2adbbd664085..86309d7f28a5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateVPCCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateVPCCmd.java @@ -130,6 +130,11 @@ public class CreateVPCCmd extends BaseAsyncCreateCmd implements UserCmd { description="(optional) for NSX based VPCs: when set to true, use the VR IP as nameserver, otherwise use DNS1 and DNS2") private Boolean useVrIpResolver; + @Parameter(name = ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + type = CommandType.BOOLEAN, since = "4.23.0", authorized = {RoleType.Admin}) + private boolean keepMacAddressOnPublicNic = true; + // /////////////////////////////////////////////////// // ///////////////// Accessors /////////////////////// // /////////////////////////////////////////////////// @@ -214,6 +219,10 @@ public boolean getUseVrIpResolver() { return BooleanUtils.toBoolean(useVrIpResolver); } + public boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteStaticRouteCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteStaticRouteCmd.java index 532a4108076c..cf1805973b77 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteStaticRouteCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteStaticRouteCmd.java @@ -69,7 +69,7 @@ public String getEventType() { @Override public String getEventDescription() { - return ("Deleting static route id=" + id); + return "Deleting static route with ID: " + getResourceUuid(ApiConstants.ID); } @Override @@ -87,7 +87,7 @@ public long getEntityOwnerId() { @Override public void execute() throws ResourceUnavailableException { - CallContext.current().setEventDetails("Route Id: " + id); + CallContext.current().setEventDetails("Route ID: " + getResourceUuid(ApiConstants.ID)); boolean result = _vpcService.revokeStaticRoute(id); if (result) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteVPCCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteVPCCmd.java index ccaac2b1e29b..e42b4761ea8f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteVPCCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/DeleteVPCCmd.java @@ -65,7 +65,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Deleting VPC id=" + getId(); + return "Deleting VPC with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/RestartVPCCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/RestartVPCCmd.java index 9dadf061753a..f6a408a66a3d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/RestartVPCCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/RestartVPCCmd.java @@ -118,7 +118,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "restarting VPC id=" + getId(); + return "Restarting VPC with ID: " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmd.java index 88e38649802b..6cfdb895977d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmd.java @@ -69,6 +69,11 @@ public class UpdateVPCCmd extends BaseAsyncCustomIdCmd implements UserCmd { since = "4.19") private String sourceNatIP; + @Parameter(name = ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, + type = CommandType.BOOLEAN, since = "4.23.0", authorized = {RoleType.Admin}) + private Boolean keepMacAddressOnPublicNic; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -97,6 +102,10 @@ public String getSourceNatIP() { return sourceNatIP; } + public Boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -143,7 +152,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "updating VPC id=" + getId(); + return "Updating VPC " + getResourceUuid(ApiConstants.ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java index 533d2c0ab814..665f699c4ef3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java @@ -106,7 +106,7 @@ public String getEventType() { @Override public void execute() { - CallContext.current().setEventDetails("VPN gateway Id: " + getEntityId()); + CallContext.current().setEventDetails("VPN gateway ID: " + getEntityUuid()); Site2SiteVpnGateway result = _s2sVpnService.getVpnGateway(getEntityId()); if (result != null) { Site2SiteVpnGatewayResponse response = _responseGenerator.createSite2SiteVpnGatewayResponse(result); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteRemoteAccessVpnCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteRemoteAccessVpnCmd.java index 91e1cd1e56c7..b1fc331f4c3a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteRemoteAccessVpnCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteRemoteAccessVpnCmd.java @@ -76,7 +76,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Delete Remote Access VPN for Account " + getEntityOwnerId() + " for ip id=" + publicIpId; + return "Delete Remote Access VPN for Account " + getEntityOwnerId() + " for IP: " + getResourceUuid(ApiConstants.PUBLIC_IP_ID); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnConnectionCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnConnectionCmd.java index f66fe237a996..b23e6c163020 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnConnectionCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnConnectionCmd.java @@ -65,7 +65,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Delete site-to-site VPN connection for Account " + getEntityOwnerId(); + return "Deleting site-to-site VPN connection for Account " + getEntityOwnerId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnCustomerGatewayCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnCustomerGatewayCmd.java index 0d43477205ec..9057620e0ddb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnCustomerGatewayCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/DeleteVpnCustomerGatewayCmd.java @@ -72,7 +72,7 @@ public long getEntityOwnerId() { @Override public String getEventDescription() { - return "Delete site-to-site VPN customer gateway for Account " + getEntityOwnerId(); + return "Deleting site-to-site VPN customer gateway for Account " + getEntityOwnerId(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java new file mode 100644 index 000000000000..350d71b37887 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ApiKeyPairResponse.java @@ -0,0 +1,285 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.user.ApiKeyPairState; +import com.google.gson.annotations.SerializedName; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; +import org.apache.cloudstack.api.ApiConstants; + +import com.cloud.serializer.Param; +import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = ApiKeyPair.class) +public class ApiKeyPairResponse extends BaseResponseWithAnnotations { + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the API key pair") + private String name; + + @SerializedName(ApiConstants.API_KEY) + @Param(description = "The API key of the registered user.", isSensitive = true) + private String userApiKey; + + @SerializedName(ApiConstants.SECRET_KEY) + @Param(description = "The secret key of the registered user.", isSensitive = true) + private String userSecretKey; + + @SerializedName(ApiConstants.USER_ID) + @Param(description = "ID of the user that owns the keypair.") + private String userId; + + @SerializedName(ApiConstants.USERNAME) + @Param(description = "Username of the keypair's owner.") + private String username; + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the API key pair.", isSensitive = true) + private String id; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "API key pair description.") + private String description; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "API key pair start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "API key pair expiration date.") + private Date endDate; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "API key pair creation timestamp.") + private Date created; + + @SerializedName(ApiConstants.ACCOUNT_TYPE) + @Param(description = "Account type.") + private String accountType; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "Account ID.") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT_NAME) + @Param(description = "Account name.") + private String accountName; + + @SerializedName(ApiConstants.ROLE_ID) + @Param(description = "ID of the role.") + private String roleId; + + @SerializedName(ApiConstants.ROLE_TYPE) + @Param(description = "Type of the role (Admin, ResourceAdmin, DomainAdmin, User).") + private String roleType; + + @SerializedName(ApiConstants.ROLE_NAME) + @Param(description = "Name of the role.") + private String roleName; + + @SerializedName(ApiConstants.PERMISSIONS) + @Param(description = "Permissions of the API key pair.") + private List permissions; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "ID of the domain which the account belongs to.") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "Name of the domain which the account belongs to.") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "Path of the domain which the account belongs to.") + private String domainPath; + + @SerializedName(ApiConstants.STATE) + @Param(description = "State of the API key pair.") + private ApiKeyPairState state; + + public String getApiKey() { + return userApiKey; + } + + public void setApiKey(String apiKey) { + this.userApiKey = apiKey; + } + + public String getSecretKey() { + return userSecretKey; + } + + public void setSecretKey(String secretKey) { + this.userSecretKey = secretKey; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAccountType() { + return accountType; + } + + public void setAccountType(String accountType) { + this.accountType = accountType; + } + + public String getRoleId() { + return roleId; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRoleType() { + return roleType; + } + + public void setRoleType(String roleType) { + this.roleType = roleType; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + public ApiKeyPairState getState() { + return state; + } + + public void setState(ApiKeyPairState state) { + this.state = state; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java new file mode 100644 index 000000000000..b259de56218b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/AttachedIsoResponse.java @@ -0,0 +1,76 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class AttachedIsoResponse extends BaseResponse { + + @SerializedName("id") + @Param(description = "The ID of the attached ISO") + private String id; + + @SerializedName("name") + @Param(description = "The name of the attached ISO") + private String name; + + @SerializedName("displaytext") + @Param(description = "The display text of the attached ISO") + private String displayText; + + @SerializedName("deviceseq") + @Param(description = "The cdrom slot that holds this ISO (3=hdc, 4=hdd, ...)") + private Integer deviceSeq; + + @SerializedName("bootable") + @Param(description = "Whether this is the bootable ISO for the VM") + private Boolean bootable; + + public AttachedIsoResponse() { + } + + public AttachedIsoResponse(String id, String name, String displayText, Integer deviceSeq, boolean bootable) { + this.id = id; + this.name = name; + this.displayText = displayText; + this.deviceSeq = deviceSeq; + this.bootable = bootable; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDisplayText() { + return displayText; + } + + public Integer getDeviceSeq() { + return deviceSeq; + } + + public Boolean getBootable() { + return bootable; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java index c4f3ee31dadc..69bee63e652f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.response; import java.util.Date; +import java.util.Map; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; @@ -79,6 +80,10 @@ public class BackupOfferingResponse extends BaseResponse { @Param(description = "The date this backup offering was created") private Date created; + @SerializedName(ApiConstants.BACKUP_OFFERING_DETAILS) + @Param(description = "Details for the backup offering", since = "4.23.0") + private Map details; + public void setId(String id) { this.id = id; } @@ -127,4 +132,7 @@ public void setDomain(String domain) { this.domain = domain; } + public void setDetails(Map details) { + this.details = details; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index b855bfe40b8d..70db01445edd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -71,10 +71,22 @@ public class BackupResponse extends BaseResponse { @Param(description = "Backup protected (virtual) size in bytes") private Long protectedSize; + @SerializedName(ApiConstants.UNCOMPRESSED_SIZE) + @Param(description = "Backup uncompressed size in bytes. Only defined if backup is compressed.") + private Long uncompressedSize; + @SerializedName(ApiConstants.STATUS) @Param(description = "Backup status") private Backup.Status status; + @SerializedName(ApiConstants.COMPRESSION_STATUS) + @Param(description = "Backup compression status.") + private Backup.CompressionStatus compressionStatus; + + @SerializedName(ApiConstants.VALIDATION_STATUS) + @Param(description = "Backup validation status.") + private Backup.ValidationStatus validationStatus; + @SerializedName(ApiConstants.VOLUMES) @Param(description = "Backed up volumes") private String volumes; @@ -127,6 +139,18 @@ public class BackupResponse extends BaseResponse { @Param(description = "Indicates whether the VM from which the backup was taken is expunged or not", since = "4.22.0") private Boolean isVmExpunged; + @SerializedName(ApiConstants.FROM_CHECKPOINT_ID) + @Param(description = "Previous active checkpoint ID for incremental backups", since = "4.23.0") + private String fromCheckpointId; + + @SerializedName(ApiConstants.TO_CHECKPOINT_ID) + @Param(description = "Next checkpoint ID for incremental backups", since = "4.23.0") + private String toCheckpointId; + + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "Host ID where the backup is running", since = "4.23.0") + private String hostId; + public String getId() { return id; } @@ -207,6 +231,14 @@ public void setProtectedSize(Long protectedSize) { this.protectedSize = protectedSize; } + public Long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(Long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + public Backup.Status getStatus() { return status; } @@ -215,6 +247,22 @@ public void setStatus(Backup.Status status) { this.status = status; } + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public void setCompressionStatus(Backup.CompressionStatus compressionStatus) { + this.compressionStatus = compressionStatus; + } + + public Backup.ValidationStatus getValidationStatus() { + return validationStatus; + } + + public void setValidationStatus(Backup.ValidationStatus validationStatus) { + this.validationStatus = validationStatus; + } + public String getVolumes() { return volumes; } @@ -314,4 +362,28 @@ public void setVmOfferingRemoved(Boolean vmOfferingRemoved) { public void setVmExpunged(Boolean isVmExpunged) { this.isVmExpunged = isVmExpunged; } + + public void setFromCheckpointId(String fromCheckpointId) { + this.fromCheckpointId = fromCheckpointId; + } + + public String getFromCheckpointId() { + return this.fromCheckpointId; + } + + public void setToCheckpointId(String toCheckpointId) { + this.toCheckpointId = toCheckpointId; + } + + public String getToCheckpointId() { + return this.toCheckpointId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } + + public String getHostId() { + return this.hostId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java index 13d0c5d8c562..5da07864603a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java @@ -56,6 +56,10 @@ public class BackupScheduleResponse extends BaseResponse { @Param(description = "maximum number of backups retained") private Integer maxBackups; + @SerializedName(ApiConstants.ISOLATED) + @Param(description = ApiConstants.PARAMETER_DESCRIPTION_ISOLATED_BACKUPS) + private boolean isolated; + public void setId(String id) { this.id = id; } @@ -111,4 +115,8 @@ public void setMaxBackups(Integer maxBackups) { public void setQuiesceVM(Boolean quiesceVM) { this.quiesceVM = quiesceVM; } + + public void setIsolated(boolean isolated) { + this.isolated = isolated; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java new file mode 100644 index 000000000000..0d4984fe34f5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupServiceJobResponse.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.util.Date; + +public class BackupServiceJobResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "Compression job ID.") + private Long id; + + @SerializedName(ApiConstants.BACKUP_ID) + @Param(description = "Backup ID.") + private String backupId; + + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "Host where the job is being executed.") + private String hostId; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "Zone where the job is being executed.") + private String zoneId; + + @SerializedName(ApiConstants.ATTEMPTS) + @Param(description = "Number of attempts already made to complete this job.") + private Integer attempts; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "Compression job type.") + private String type; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Compression job start date.") + private Date startDate; + + @SerializedName(ApiConstants.SCHEDULED_DATE) + @Param(description = "Compression job scheduled start date.") + private Date scheduledDate; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "Compression job scheduled removed date.") + private Date removed; + + public BackupServiceJobResponse(Long id, String backupId, String zoneId, Integer attempts, String type, Date startDate, Date scheduledDate, Date removed) { + super("backupservicejob"); + this.id = id; + this.backupId = backupId; + this.zoneId = zoneId; + this.attempts = attempts; + this.type = type; + this.startDate = startDate; + this.scheduledDate = scheduledDate; + this.removed = removed; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java new file mode 100644 index 000000000000..2bec7711064f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java @@ -0,0 +1,53 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class CheckpointResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the checkpoint ID") + private String id; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the checkpoint creation time") + private Date created; + + @SerializedName(ApiConstants.IS_ACTIVE) + @Param(description = "whether this is the active checkpoint") + private Boolean isActive; + + public void setId(String id) { + this.id = id; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java new file mode 100644 index 000000000000..f3a571f40639 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java @@ -0,0 +1,45 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsProviderResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the DNS provider (e.g. PowerDNS, Cloudflare)") + private String name; + + public DnsProviderResponse(String name) { + this.name = name; + setObjectName("dnsprovider"); // Sets the JSON wrapper name + } + + // Accessors + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java new file mode 100644 index 000000000000..6e2082485512 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java @@ -0,0 +1,57 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsRecordResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "The record name (e.g., www.example.com.)") + private String name; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "The record type (e.g., A, CNAME, TXT)") + private DnsRecord.RecordType type; + + @SerializedName("contents") + @Param(description = "The contents of the record (IP address or target)") + private List contents; + + @SerializedName("ttl") + @Param(description = "Time to live (TTL) in seconds") + private Integer ttl; + + public DnsRecordResponse() { + super(); + setObjectName("dnsrecord"); + } + + // Setters + public void setName(String name) { this.name = name; } + public void setType(DnsRecord.RecordType type) { this.type = type; } + public void setContent(List contents) { this.contents = contents; } + public void setTtl(Integer ttl) { this.ttl = ttl; } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java new file mode 100644 index 000000000000..fecf49f0f984 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -0,0 +1,133 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsServer.class) +public class DnsServerResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the DNS server") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the DNS server") + private String name; + + @SerializedName(ApiConstants.URL) + @Param(description = "URL of the DNS server API") + private String url; + + @SerializedName(ApiConstants.PORT) + @Param(description = "The port of the DNS server") + private Integer port; + + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "The provider type of the DNS server") + private String provider; + + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "Is the DNS server publicly available") + private Boolean isPublic; + + @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) + @Param(description = "The public domain suffix for the DNS server") + private String publicDomainSuffix; + + @SerializedName(ApiConstants.NAME_SERVERS) + @Param(description = "Name servers entries associated to DNS server") + private List nameServers; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS server") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS server") + private String domainName; + + @SerializedName(ApiConstants.STATE) + @Param(description = "The state of the account") + private String state; + + public DnsServerResponse() { + super(); + + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public void setPublic(Boolean value) { + isPublic = value; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setPublicDomainSuffix(String publicDomainSuffix) { + this.publicDomainSuffix = publicDomainSuffix; + } + + public void setNameServers(List nameServers) { + this.nameServers = nameServers; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java new file mode 100644 index 000000000000..84fe46ce5aca --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsZoneNetworkMapResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the mapping") + private String id; + + @SerializedName(ApiConstants.DNS_ZONE_ID) + @Param(description = "The ID of the DNS zone") + private String dnsZoneId; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "The ID of the Network") + private String networkId; + + @SerializedName("subdomain") + @Param(description = "The sub domain name of the auto-registered DNS record") + private String subDomain; + + public DnsZoneNetworkMapResponse() { + super(); + setObjectName("dnszonenetwork"); + } + + // Setters + public void setId(String id) { + this.id = id; + } + + public void setDnsZoneId(String dnsZoneId) { + this.dnsZoneId = dnsZoneId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setSubDomain(String subDomain) { + this.subDomain = subDomain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java new file mode 100644 index 000000000000..e179d2fd4b13 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -0,0 +1,139 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsZone.class) +public class DnsZoneResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the DNS zone") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the DNS zone") + private String name; + + @SerializedName("dnsserverid") + @Param(description = "ID of the DNS server this zone belongs to") + private String dnsServerId; + + @SerializedName("dnsservername") + @Param(description = "the name of the DNS server hosting this zone") + private String dnsServerName; + + @SerializedName("dnsserveraccount") + @Param(description = "the account name of the DNS server owner") + private String dnsServerAccountName; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS zone") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS zone") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "ID of the network this zone is associated with") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "Name of the network this zone is associated with") + private String networkName; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "The type of the zone (Public/Private)") + private DnsZone.ZoneType type; + + @SerializedName(ApiConstants.STATE) + @Param(description = "The state of the zone (Active/Inactive)") + private DnsZone.State state; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description for the DNS zone") + private String description; + + public DnsZoneResponse() { + super(); + setObjectName("dnszone"); + } + + public void setName(String name) { + this.name = name; + } + + public void setDnsServerId(String dnsServerId) { + this.dnsServerId = dnsServerId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public void setType(DnsZone.ZoneType type) { + this.type = type; + } + + public void setState(DnsZone.State state) { + this.state = state; + } + + public void setId(String id) { + this.id = id; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setDnsServerName(String dnsServerName) { + this.dnsServerName = dnsServerName; + } + + public void setDnsServerAccountName(String dnsServerAccountName) { + this.dnsServerAccountName = dnsServerAccountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java index fdf1e87df506..911839da405f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java @@ -85,6 +85,12 @@ public class ExtensionResponse extends BaseResponse { @Param(description = "Removal timestamp of the extension, if applicable") private Date removed; + @SerializedName(ApiConstants.RESERVED_RESOURCE_DETAILS) + @Param(description = "Resource detail names as comma separated string that should be reserved and not visible " + + "to end users", + since = "4.22.1") + protected String reservedResourceDetails; + public ExtensionResponse(String id, String name, String description, String type) { this.id = id; this.name = name; @@ -179,4 +185,8 @@ public Date getRemoved() { public void setRemoved(Date removed) { this.removed = removed; } + + public void setReservedResourceDetails(String reservedResourceDetails) { + this.reservedResourceDetails = reservedResourceDetails; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java index 5986c16dc8c0..f6cc9e5d9499 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/FirewallResponse.java @@ -51,6 +51,10 @@ public class FirewallResponse extends BaseResponse { @Param(description = "The Network ID of the firewall rule") private String networkId; + @SerializedName(ApiConstants.VPC_ID) + @Param(description = "The VPC ID of the firewall rule") + private String vpcId; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The public IP address for the firewall rule") private String publicIpAddress; @@ -115,6 +119,10 @@ public void setNetworkId(String networkId) { this.networkId = networkId; } + public void setVpcId(String vpcId) { + this.vpcId = vpcId; + } + public void setState(String state) { this.state = state; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java index 48097e51d992..aed56a369089 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/FirewallRuleResponse.java @@ -94,6 +94,10 @@ public class FirewallRuleResponse extends BaseResponse { @Param(description = "The ID of the guest Network the port forwarding rule belongs to") private String networkId; + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "The Name of the guest Network the port forwarding rule belongs to") + private String networkName; + @SerializedName(ApiConstants.FOR_DISPLAY) @Param(description = "Is firewall for display to the regular user", since = "4.4", authorized = {RoleType.Admin}) private Boolean forDisplay; @@ -223,6 +227,10 @@ public void setNetworkId(String networkId) { this.networkId = networkId; } + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + public void setForDisplay(Boolean forDisplay) { this.forDisplay = forDisplay; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java index fe8a85b4176e..3c53bdb9d93d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java @@ -52,6 +52,11 @@ public class GuiThemeResponse extends BaseResponse { @Param(description = "A set of Common Names (CN) (fixed or wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") private String commonNames; + @SerializedName(ApiConstants.LOGIN_BASE_DOMAIN) + @Param(description = "The ACS domain to be used as base for the login when accessing the GUI through the common name defined in the theme. If a " + + "common name is not defined, this parameter is ignored on the GUI.") + private String loginBaseDomain; + @SerializedName(ApiConstants.DOMAIN_IDS) @Param(description = "A set of domain UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.") private String domainIds; @@ -176,4 +181,12 @@ public void setRecursiveDomains(Boolean recursiveDomains) { public void setRemoved(Date removed) { this.removed = removed; } + + public String getLoginBaseDomain() { + return loginBaseDomain; + } + + public void setLoginBaseDomain(String loginBaseDomain) { + this.loginBaseDomain = loginBaseDomain; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java new file mode 100644 index 000000000000..7576c76bef15 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java @@ -0,0 +1,254 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + +import java.util.Date; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.kms.HSMProfile; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = HSMProfile.class) +public class HSMProfileResponse extends BaseResponse implements ControlledViewEntityResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the HSM profile") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the HSM profile") + private String name; + + @SerializedName(ApiConstants.PROTOCOL) + @Param(description = "the protocol of the HSM profile") + private String protocol; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "the account ID of the HSM profile owner") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account name of the HSM profile owner") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the domain ID of the HSM profile owner") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the domain name of the HSM profile owner") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "the domain path of the HSM profile owner") + private String domainPath; + + @SerializedName(ApiConstants.PROJECT_ID) + @Param(description = "the project ID of the HSM profile owner") + private String projectId; + + @SerializedName(ApiConstants.PROJECT) + @Param(description = "the project name of the HSM profile owner") + private String projectName; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "the zone ID where the HSM profile is available") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) + @Param(description = "the zone name where the HSM profile is available") + private String zoneName; + + @SerializedName("vendor") + @Param(description = "the vendor name of the HSM profile") + private String vendorName; + + @SerializedName(ApiConstants.STATE) + @Param(description = "the state of the HSM profile") + private String state; + + @SerializedName(ApiConstants.ENABLED) + @Param(description = "whether the HSM profile is enabled") + private Boolean enabled; + + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "whether this is a system HSM profile available to all users globally") + private Boolean isPublic; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the date the HSM profile was created") + private Date created; + + @SerializedName(ApiConstants.DETAILS) + @Param(description = "HSM configuration details (sensitive values are encrypted)") + private Map details; + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + @Override + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + @Override + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + @Override + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + @Override + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public void setVendorName(String vendorName) { + this.vendorName = vendorName; + } + + public void setState(String state) { + this.state = state; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public void setIsPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setDetails(Map details) { + this.details = details; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getProtocol() { + return protocol; + } + + public String getAccountId() { + return accountId; + } + + public String getAccountName() { + return accountName; + } + + public String getDomainId() { + return domainId; + } + + public String getDomainName() { + return domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public String getProjectId() { + return projectId; + } + + public String getProjectName() { + return projectName; + } + + public String getZoneId() { + return zoneId; + } + + public String getZoneName() { + return zoneName; + } + + public String getVendorName() { + return vendorName; + } + + public String getState() { + return state; + } + + public Boolean getEnabled() { + return enabled; + } + + public Boolean getPublic() { + return isPublic; + } + + public Date getCreated() { + return created; + } + + public Map getDetails() { + return details; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 5d085d1bee05..10bd62804fb2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -63,6 +63,10 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "The OS category name of the host") private String osCategoryName; + @SerializedName(ApiConstants.GUEST_OS_RULE) + @Param(description = "the guest OS rule") + private String guestOsRule; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The IP address of the host") private String ipAddress; @@ -197,6 +201,10 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "the virtual machine id for host type ConsoleProxy and SecondaryStorageVM", since = "4.21.0") private String virtualMachineId; + @SerializedName("msid") + @Param(description = "(only for details=core) the msid of the host's management server") + private Long msId; + @SerializedName(ApiConstants.MANAGEMENT_SERVER_ID) @Param(description = "The management server ID of the host") private String managementServerId; @@ -488,6 +496,14 @@ public void setVirtualMachineId(String virtualMachineId) { this.virtualMachineId = virtualMachineId; } + public Long getMsId() { + return msId; + } + + public void setMsId(Long msId) { + this.msId = msId; + } + public void setManagementServerId(String managementServerId) { this.managementServerId = managementServerId; } @@ -999,4 +1015,12 @@ public void setExtensionName(String extensionName) { public String getExtensionName() { return extensionName; } + + public String getGuestOsRule() { + return guestOsRule; + } + + public void setGuestOsRule(String guestOsRule) { + this.guestOsRule = guestOsRule; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java new file mode 100644 index 000000000000..15576e8f1012 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java @@ -0,0 +1,104 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.ImageTransfer; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = ImageTransfer.class) +public class ImageTransferResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the image transfer") + private String id; + + @SerializedName("backupid") + @Param(description = "the backup ID") + private String backupId; + + @SerializedName("vmid") + @Param(description = "the VM ID") + private String vmId; + + @SerializedName(ApiConstants.VOLUME_ID) + @Param(description = "the disk/volume ID") + private String diskId; + + @SerializedName("devicename") + @Param(description = "the device name (vda, vdb, etc)") + private String deviceName; + + @SerializedName("transferurl") + @Param(description = "the transfer URL") + private String transferUrl; + + @SerializedName("phase") + @Param(description = "the transfer phase") + private String phase; + + @SerializedName("direction") + @Param(description = "the image transfer direction: upload / download") + private String direction; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the date created") + private Date created; + + public void setId(String id) { + this.id = id; + } + + public void setBackupId(String backupId) { + this.backupId = backupId; + } + + public void setVmId(String vmId) { + this.vmId = vmId; + } + + public void setDiskId(String diskId) { + this.diskId = diskId; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + + public void setPhase(String phase) { + this.phase = phase; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/KMSKeyResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/KMSKeyResponse.java new file mode 100644 index 000000000000..cd7a360a497c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/KMSKeyResponse.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.kms.KMSKey; + +import java.util.Date; + +@EntityReference(value = KMSKey.class) +public class KMSKeyResponse extends BaseResponse implements ControlledViewEntityResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the UUID of the key") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the key") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the key") + private String description; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account owning the key") + private String accountName; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "the account ID owning the key") + private String accountId; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the domain ID of the key") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the domain name of the key") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "the domain path of the key") + private String domainPath; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "the zone ID where the key is valid") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) + @Param(description = "the zone name where the key is valid") + private String zoneName; + + @SerializedName(ApiConstants.HSM_PROFILE_ID) + @Param(description = "the zone ID where the key is valid") + private String hsmProfileId; + + @SerializedName(ApiConstants.HSM_PROFILE) + @Param(description = "the zone name where the key is valid") + private String hsmProfileName; + + @SerializedName(ApiConstants.ALGORITHM) + @Param(description = "the encryption algorithm") + private String algorithm; + + @SerializedName(ApiConstants.KEY_BITS) + @Param(description = "the key size in bits") + private Integer keyBits; + + @SerializedName(ApiConstants.VERSION) + @Param(description = "the key size in bits") + private Integer version; + + @SerializedName(ApiConstants.ENABLED) + @Param(description = "whether the key is enabled") + private Boolean enabled; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the creation timestamp") + private Date created; + + @SerializedName(ApiConstants.PROJECT_ID) + @Param(description = "the project ID of the key") + private String projectId; + + @SerializedName(ApiConstants.PROJECT) + @Param(description = "the project name of the key") + private String projectName; + + @SerializedName(ApiConstants.KEK_LABEL) + @Param(description = "the provider-specific KEK label (admin only)", authorized = { RoleType.Admin }) + private String kekLabel; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getAccountName() { + return accountName; + } + + @Override + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + @Override + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getDomainId() { + return domainId; + } + + @Override + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getDomainName() { + return domainName; + } + + @Override + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getDomainPath() { + return domainPath; + } + + @Override + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public String getZoneName() { + return zoneName; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public String getHsmProfileId() { + return hsmProfileId; + } + + public void setHsmProfileId(String hsmProfileId) { + this.hsmProfileId = hsmProfileId; + } + + public String getHsmProfileName() { + return hsmProfileName; + } + + public void setHsmProfileName(String hsmProfileName) { + this.hsmProfileName = hsmProfileName; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public Integer getKeyBits() { + return keyBits; + } + + public void setKeyBits(Integer keyBits) { + this.keyBits = keyBits; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getKekLabel() { + return kekLabel; + } + + public void setKekLabel(String kekLabel) { + this.kekLabel = kekLabel; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java index c20f700fe08e..6e3ef4678d28 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -90,6 +90,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @Param(description = "Management Server ID that the user logged to", since = "4.21.0.0") private String managementServerId; + @SerializedName(value = ApiConstants.PASSWORD_CHANGE_REQUIRED) + @Param(description = "Indicates whether the User is required to change password on next login.", since = "4.23.0") + private Boolean passwordChangeRequired; + public String getUsername() { return username; } @@ -223,4 +227,12 @@ public String getManagementServerId() { public void setManagementServerId(String managementServerId) { this.managementServerId = managementServerId; } + + public Boolean getPasswordChangeRequired() { + return passwordChangeRequired; + } + + public void setPasswordChangeRequired(Boolean passwordChangeRequired) { + this.passwordChangeRequired = passwordChangeRequired; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index 7c4b733a80f5..aeaf333540c7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -331,6 +331,18 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement @Param(description = "The BGP peers for the network", since = "4.20.0") private Set bgpPeers; + @SerializedName(ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC) + @Param(description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, since = "4.23.0") + private Boolean keepMacAddressOnPublicNic; + + @SerializedName(ApiConstants.DNS_ZONE) + @Param(description = "DNS zone associated to the network", since = "4.23.0") + private String dnsZone; + + @SerializedName(ApiConstants.DNS_SUB_DOMAIN) + @Param(description = "DNS subdomain associated to the network", since = "4.23.0") + private String dnsSubdomain; + public NetworkResponse() {} public Boolean getDisplayNetwork() { @@ -702,4 +714,16 @@ public void setIpv6Dns1(String ipv6Dns1) { public void setIpv6Dns2(String ipv6Dns2) { this.ipv6Dns2 = ipv6Dns2; } + + public void setKeepMacAddressOnPublicNic(Boolean keepMacAddressOnPublicNic) { + this.keepMacAddressOnPublicNic = keepMacAddressOnPublicNic; + } + + public void setDnsZone(String dnsZone) { + this.dnsZone = dnsZone; + } + + public void setDnsSubdomain(String dnsSubdomain) { + this.dnsSubdomain = dnsSubdomain; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index f992514b8db2..5ad41ad62244 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -146,6 +146,14 @@ public class NicResponse extends BaseResponse { @Param(description = "Public IP address associated with this NIC via Static NAT rule") private String publicIp; + @SerializedName(ApiConstants.ENABLED) + @Param(description = "whether the NIC is enabled or not") + private Boolean isEnabled; + + @SerializedName(ApiConstants.NIC_DNS_NAME) + @Param(description = "DNS name associated with this NIC's IP address") + private String nicDnsName; + public void setVmId(String vmId) { this.vmId = vmId; } @@ -416,4 +424,16 @@ public void setPublicIpId(String publicIpId) { public void setPublicIp(String publicIp) { this.publicIp = publicIp; } + + public Boolean getEnabled() { + return isEnabled; + } + + public void setEnabled(Boolean enabled) { + isEnabled = enabled; + } + + public void setNicDnsName(String nicDnsName) { + this.nicDnsName = nicDnsName; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java index eba00da25cfe..8734e65dfa58 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java @@ -53,6 +53,10 @@ public class NicSecondaryIpResponse extends BaseResponse { @Param(description = "The ID of the Instance") private String vmId; + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the secondary IP address") + private String description; + @Override public String getObjectId() { return this.getId(); @@ -98,6 +102,14 @@ public void setId(String id) { this.id = id; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public List getSecondaryIpsList() { return secondaryIpsList; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ResourceScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ResourceScheduleResponse.java new file mode 100644 index 000000000000..ff799b467cff --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ResourceScheduleResponse.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.api.response; + +import java.util.Date; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.schedule.ResourceSchedule; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = ResourceSchedule.class) +public class ResourceScheduleResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of resource schedule") + private String id; + + @SerializedName(ApiConstants.RESOURCE_TYPE) + @Param(description = "Type of the resource") + private ApiCommandResourceType resourceType; + + @SerializedName(ApiConstants.RESOURCE_ID) + @Param(description = "ID of the resource") + private String resourceId; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of resource schedule") + private String description; + + @SerializedName(ApiConstants.SCHEDULE) + @Param(description = "Cron formatted resource schedule") + private String schedule; + + @SerializedName(ApiConstants.TIMEZONE) + @Param(description = "Timezone of the schedule") + private String timeZone; + + @SerializedName(ApiConstants.ACTION) + @Param(description = "Action") + private ResourceSchedule.Action action; + + @SerializedName(ApiConstants.ENABLED) + @Param(description = "Resource schedule is enabled") + private boolean enabled; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Date from which the schedule is active") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Date after which the schedule becomes inactive") + private Date endDate; + + @SerializedName(ApiConstants.DETAILS) + @Param(description = "Schedule details") + private Map details; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "Date when the schedule was created") + private Date created; + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setResourceType(ApiCommandResourceType resourceType) { + this.resourceType = resourceType; + } + + public ApiCommandResourceType getResourceType() { + return resourceType; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public String getResourceId() { + return resourceId; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + public String getSchedule() { + return schedule; + } + + public void setTimeZone(String timeZone) { + this.timeZone = timeZone; + } + + public String getTimeZone() { + return timeZone; + } + + public void setAction(ResourceSchedule.Action action) { + this.action = action; + } + + public ResourceSchedule.Action getAction() { + return action; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean getEnabled() { + return enabled; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getStartDate() { + return startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setDetails(Map details) { + this.details = details; + } + + public Map getDetails() { + return details; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getCreated() { + return created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UnmanagedInstanceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UnmanagedInstanceResponse.java index 195323b741d2..3f2dc045e87c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UnmanagedInstanceResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UnmanagedInstanceResponse.java @@ -51,6 +51,14 @@ public class UnmanagedInstanceResponse extends BaseResponse { @Param(description = "The name of the host to which Instance belongs") private String hostName; + @SerializedName(ApiConstants.HYPERVISOR) + @Param(description = "The hypervisor to which Instance belongs") + private String hypervisor; + + @SerializedName(ApiConstants.HYPERVISOR_VERSION) + @Param(description = "The hypervisor version of the host to which Instance belongs") + private String hypervisorVersion; + @SerializedName(ApiConstants.POWER_STATE) @Param(description = "The power state of the Instance") private String powerState; @@ -140,6 +148,22 @@ public void setHostName(String hostName) { this.hostName = hostName; } + public String getHypervisor() { + return hypervisor; + } + + public void setHypervisor(String hypervisor) { + this.hypervisor = hypervisor; + } + + public String getHypervisorVersion() { + return hypervisorVersion; + } + + public void setHypervisorVersion(String hypervisorVersion) { + this.hypervisorVersion = hypervisorVersion; + } + public String getPowerState() { return powerState; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java index 3b1c1a10885a..61b025b206eb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java @@ -95,12 +95,12 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons @Param(description = "The timezone user was created in") private String timezone; - @SerializedName("apikey") + @SerializedName(ApiConstants.API_KEY) @Param(description = "The API key of the user", isSensitive = true) private String apiKey; @Deprecated - @SerializedName("secretkey") + @SerializedName(ApiConstants.SECRET_KEY) @Param(description = "The secret key of the user", isSensitive = true) private String secretKey; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 745c0ba46832..e08c6019f150 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -166,6 +166,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "An alternate display text of the ISO attached to the Instance") private String isoDisplayText; + @SerializedName("isos") + @Param(description = "All ISOs attached to the Instance, keyed by cdrom slot. The first entry mirrors isoid/isoname for back-compat.", responseObject = AttachedIsoResponse.class, since = "4.23.0") + private List isos; + + @SerializedName("isomaxcount") + @Param(description = "Maximum number of ISOs that may be attached to this Instance, after applying the cluster-scoped vm.iso.max.count and the hypervisor's own cap.", since = "4.23.0") + private Integer isoMaxCount; + @SerializedName(ApiConstants.SERVICE_OFFERING_ID) @Param(description = "The ID of the service offering of the Instance") private String serviceOfferingId; @@ -226,6 +234,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "The name of the backup offering of the Instance", since = "4.14") private String backupOfferingName; + @SerializedName(ApiConstants.BACKUP_PROVIDER) + @Param(description = "The name of the backup provider of the offering attached to the Instance", since = "4.23.0") + private String backupProvider; + @SerializedName("forvirtualnetwork") @Param(description = "The virtual Network for the service offering") private Boolean forVirtualNetwork; @@ -340,6 +352,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "List of read-only Instance details as comma separated string.", since = "4.16.0") private String readOnlyDetails; + @SerializedName("alloweddetails") + @Param(description = "List of allowed Vm details as comma separated string if VM instance settings are read from OVA.", since = "4.22.1") + private String allowedDetails; + @SerializedName(ApiConstants.SSH_KEYPAIRS) @Param(description = "SSH key-pairs") private String keyPairNames; @@ -867,6 +883,22 @@ public void setIsoId(String isoId) { this.isoId = isoId; } + public void setIsos(List isos) { + this.isos = isos; + } + + public List getIsos() { + return isos; + } + + public void setIsoMaxCount(Integer isoMaxCount) { + this.isoMaxCount = isoMaxCount; + } + + public Integer getIsoMaxCount() { + return isoMaxCount; + } + public void setIsoName(String isoName) { this.isoName = isoName; } @@ -1091,6 +1123,10 @@ public void setReadOnlyDetails(String readOnlyDetails) { this.readOnlyDetails = readOnlyDetails; } + public void setAllowedDetails(String allowedDetails) { + this.allowedDetails = allowedDetails; + } + public void setOsTypeId(String osTypeId) { this.osTypeId = osTypeId; } @@ -1115,6 +1151,10 @@ public String getReadOnlyDetails() { return readOnlyDetails; } + public String getAllowedDetails() { + return allowedDetails; + } + public Boolean getDynamicallyScalable() { return isDynamicallyScalable; } @@ -1326,4 +1366,11 @@ public void setLeaseExpiryDate(Date leaseExpiryDate) { this.leaseExpiryDate = leaseExpiryDate; } + public String getBackupProvider() { + return backupProvider; + } + + public void setBackupProvider(String backupProvider) { + this.backupProvider = backupProvider; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VMScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VMScheduleResponse.java index 6800c25b0234..34455b3a0bf4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VMScheduleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VMScheduleResponse.java @@ -23,11 +23,11 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; -import org.apache.cloudstack.vm.schedule.VMSchedule; +import org.apache.cloudstack.schedule.ResourceSchedule; import java.util.Date; -@EntityReference(value = VMSchedule.class) +@EntityReference(value = ResourceSchedule.class) public class VMScheduleResponse extends BaseResponse { @SerializedName(ApiConstants.ID) @Param(description = "The ID of Instance schedule") @@ -51,7 +51,7 @@ public class VMScheduleResponse extends BaseResponse { @SerializedName(ApiConstants.ACTION) @Param(description = "Action") - private VMSchedule.Action action; + private ResourceSchedule.Action action; @SerializedName(ApiConstants.ENABLED) @Param(description = "VM schedule is enabled") @@ -69,6 +69,20 @@ public class VMScheduleResponse extends BaseResponse { @Param(description = "Date when the schedule was created") private Date created; + public VMScheduleResponse(ResourceScheduleResponse scheduleResponse) { + super("vmschedule"); + this.id = scheduleResponse.getId(); + this.vmId = scheduleResponse.getResourceId(); + this.description = scheduleResponse.getDescription(); + this.schedule = scheduleResponse.getSchedule(); + this.timeZone = scheduleResponse.getTimeZone(); + this.action = scheduleResponse.getAction(); + this.enabled = scheduleResponse.getEnabled(); + this.startDate = scheduleResponse.getStartDate(); + this.endDate = scheduleResponse.getEndDate(); + this.created = scheduleResponse.getCreated(); + } + public void setId(String id) { this.id = id; } @@ -89,7 +103,7 @@ public void setTimeZone(String timeZone) { this.timeZone = timeZone; } - public void setAction(VMSchedule.Action action) { + public void setAction(ResourceSchedule.Action action) { this.action = action; } @@ -105,5 +119,7 @@ public void setEndDate(Date endDate) { this.endDate = endDate; } - public void setCreated(Date created) {this.created = created;} + public void setCreated(Date created) { + this.created = created; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java index 058ea50f991e..031d9315cd08 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java @@ -309,6 +309,18 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co @Param(description = "the format of the disk encryption if applicable", since = "4.19.1") private String encryptionFormat; + @SerializedName(ApiConstants.KMS_KEY) + @Param(description = "KMS key name of the volume", since = "4.23.0") + private String kmsKey; + + @SerializedName(ApiConstants.KMS_KEY_ID) + @Param(description = "KMS key id of the volume", since = "4.23.0") + private String kmsKeyId; + + @SerializedName(ApiConstants.KMS_KEY_VERSION) + @Param(description = "Version number of the KMS key used for disk encryption if applicable", since = "4.23.0") + private Integer kmsKeyVersion; + public String getPath() { return path; } @@ -868,7 +880,35 @@ public void setVolumeRepairResult(Map volumeRepairResult) { this.volumeRepairResult = volumeRepairResult; } + public String getEncryptionFormat() { + return encryptionFormat; + } + public void setEncryptionFormat(String encryptionFormat) { this.encryptionFormat = encryptionFormat; } + + public String getKmsKey() { + return kmsKey; + } + + public void setKmsKey(String kmsKey) { + this.kmsKey = kmsKey; + } + + public String getKmsKeyId() { + return kmsKeyId; + } + + public void setKmsKeyId(String kmsKeyId) { + this.kmsKeyId = kmsKeyId; + } + + public Integer getKmsKeyVersion() { + return kmsKeyVersion; + } + + public void setKmsKeyVersion(Integer kmsKeyVersion) { + this.kmsKeyVersion = kmsKeyVersion; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java index a0516e660e48..2e821dae52de 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java @@ -102,6 +102,10 @@ public class VpcOfferingResponse extends BaseResponse { @Param(description = "The routing mode for the network offering, supported types are Static or Dynamic.") private String routingMode; + @SerializedName(ApiConstants.CONSERVE_MODE) + @Param(description = "True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers.", since = "4.23.0") + private Boolean conserveMode; + public void setId(String id) { this.id = id; } @@ -201,4 +205,12 @@ public String getRoutingMode() { public void setRoutingMode(String routingMode) { this.routingMode = routingMode; } + + public Boolean getConserveMode() { + return conserveMode; + } + + public void setConserveMode(Boolean conserveMode) { + this.conserveMode = conserveMode; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java index 2648ba836785..34d50d5b9f92 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcResponse.java @@ -73,6 +73,10 @@ public class VpcResponse extends BaseResponseWithAnnotations implements Controll @Param(description = "VPC offering name the VPC is created from", since = "4.13.2") private String vpcOfferingName; + @SerializedName(ApiConstants.VPC_OFFERING_CONSERVE_MODE) + @Param(description = "true if VPC offering is ip conserve mode enabled", since = "4.23") + private Boolean vpcOfferingConserveMode; + @SerializedName(ApiConstants.CREATED) @Param(description = "The date this VPC was created") private Date created; @@ -181,6 +185,10 @@ public class VpcResponse extends BaseResponseWithAnnotations implements Controll @Param(description = "The BGP peers for the VPC", since = "4.20.0") private Set bgpPeers; + @SerializedName(ApiConstants.KEEP_MAC_ADDRESS_ON_PUBLIC_NIC) + @Param(description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, since = "4.23.0") + private Boolean keepMacAddressOnPublicNic; + public void setId(final String id) { this.id = id; } @@ -197,6 +205,10 @@ public void setDisplayText(final String displayText) { this.displayText = displayText; } + public void setVpcOfferingConserveMode(Boolean vpcOfferingConserveMode) { + this.vpcOfferingConserveMode = vpcOfferingConserveMode; + } + public void setCreated(final Date created) { this.created = created; } @@ -358,4 +370,8 @@ public void addBgpPeer(BgpPeerResponse bgpPeer) { } this.bgpPeers.add(bgpPeer); } + + public void setKeepMacAddressOnPublicNic(Boolean keepMacAddressOnPublicNic) { + this.keepMacAddressOnPublicNic = keepMacAddressOnPublicNic; + } } diff --git a/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java index ee3b98b8a4b6..cccc1fff9823 100644 --- a/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java +++ b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java @@ -42,8 +42,19 @@ public interface UserOAuth2Authenticator extends Adapter { * Verifies the code provided by provider and fetches email * @return returns email */ - String verifyCodeAndFetchEmail(String secretCode); + String verifySecretCodeAndFetchEmail(String secretCode); + /** + * Verifies if the logged in user is valid for a specific domain + * @return true if it's a valid user, otherwise false + */ + boolean verifyUser(String email, String secretCode, Long domainId); + + /** + * Verifies the secret code provided by provider and fetches email for a specific domain + * @return email for the specified domain + */ + String verifySecretCodeAndFetchEmail(String secretCode, Long domainId); /** * Fetches email using the accessToken diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java index 951af9180e7f..2124423da108 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/Backup.java +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.backup; +import java.io.Serializable; import java.util.Date; import java.util.List; import java.util.Map; @@ -30,8 +31,36 @@ public interface Backup extends ControlledEntity, InternalIdentity, Identity { + String getFromCheckpointId(); + + String getToCheckpointId(); + + Long getCheckpointCreateTime(); + + Long getHostId(); + enum Status { - Allocated, Queued, BackingUp, BackedUp, Error, Failed, Restoring, Removed, Expunged + Allocated, Queued, BackingUp, ReadyForImageTransfer, FinalizingImageTransfer, BackedUp, Error, Failed, Restoring, Removed, Expunged, + // Hidden: a chain backup kept as a tombstone after the user deleted it while it still has + // live descendants (incremental chains). Excluded from listBackups and from all backup + // operations (which require BackedUp); swept from the DB once its last descendant is gone. + Hidden + } + + enum CompressionStatus { + Uncompressed, Compressing, FinalizingCompression, Compressed, CompressionError + } + + enum ValidationStatus { + NotValidated, Validating, Valid, UnableToValidate, NotValid + } + + enum ValidationSteps { + wait_for_boot, screenshot, execute_command + } + + enum CompressionLibrary { + zstd, zlib } class Metric { @@ -120,7 +149,7 @@ public void setDataSize(Long dataSize) { } } - class VolumeInfo { + class VolumeInfo implements Serializable { private String uuid; private Volume.Type type; private Long size; @@ -189,11 +218,14 @@ public String toString() { String getType(); Date getDate(); Backup.Status getStatus(); + Backup.CompressionStatus getCompressionStatus(); + Backup.ValidationStatus getValidationStatus(); Long getSize(); Long getProtectedSize(); void setName(String name); String getDescription(); void setDescription(String description); + Long getUncompressedSize(); List getBackedUpVolumes(); long getZoneId(); Map getDetails(); diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index cbaf61405970..30fc2bbec40d 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -20,8 +20,11 @@ import java.util.List; import java.util.Map; +import com.cloud.storage.Volume; +import com.cloud.vm.VirtualMachine; import com.cloud.capacity.Capacity; import com.cloud.exception.ResourceAllocationException; +import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; @@ -30,17 +33,16 @@ import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupOfferingCmd; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; -import com.cloud.storage.Volume; import com.cloud.utils.Pair; import com.cloud.utils.component.Manager; import com.cloud.utils.component.PluggableService; -import com.cloud.vm.VirtualMachine; import com.cloud.vm.VmDiskInfo; /** @@ -56,7 +58,8 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer ConfigKey BackupProviderPlugin = new ConfigKey<>("Advanced", String.class, "backup.framework.provider.plugin", "dummy", - "The backup and recovery provider plugin. Valid plugin values: dummy, veeam, networker and nas", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); + "The backup and recovery provider plugin.", + true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); ConfigKey BackupSyncPollingInterval = new ConfigKey<>("Advanced", Long.class, "backup.framework.sync.interval", @@ -136,8 +139,20 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer */ BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd); + /** + * Create a new Backup and Recovery policy to CloudStack. Currently only supported for KBOSS. + * @param cmd create backup offering cmd + */ + BackupOffering createBackupOffering(final CreateBackupOfferingCmd cmd); + List getBackupOfferingDomains(final Long offeringId); + /** + * Clone an existing backup offering with updated values + * @param cmd clone backup offering cmd + */ + BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd); + /** * List backup offerings * @param ListBackupOfferingsCmd API cmd @@ -202,7 +217,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer /** * Restore a full VM from backup */ - boolean restoreBackup(final Long backupId); + boolean restoreBackup(final Long backupId, boolean quickRestore, Long hostId); Map getIpToNetworkMapFromBackup(Backup backup, boolean preserveIps, List networkIds); @@ -213,12 +228,12 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer /** * Restore a backup to a new Instance */ - boolean restoreBackupToVM(Long backupId, Long vmId) throws ResourceUnavailableException; + boolean restoreBackupToVM(Long backupId, Long vmId, boolean quickrestore) throws ResourceUnavailableException; /** * Restore a backed up volume and attach it to a VM */ - boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception; + boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId, boolean isQuickRestore, Long hostId) throws Exception; /** * Deletes a backup @@ -226,7 +241,7 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer * @param forced Indicates if backup will be force removed or not * @return returns operation success */ - boolean deleteBackup(final Long backupId, final Boolean forced); + boolean deleteBackup(final Long backupId, final Boolean forced) throws ResourceAllocationException; void validateBackupForZone(Long zoneId); diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java index 23b8092425d9..66e4501c460e 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java @@ -63,6 +63,7 @@ public interface BackupProvider { */ boolean removeVMFromBackupOffering(VirtualMachine vm); + /** * Whether the provider will delete backups on removal of VM from the offering * @return boolean result @@ -73,11 +74,14 @@ public interface BackupProvider { * Starts and creates an adhoc backup process * for a previously registered VM backup * - * @param vm the machine to make a backup of - * @param quiesceVM instance will be quiesced for checkpointing for backup. Applicable only to NAS plugin. + * @param vm + * the machine to make a backup of + * @param quiesceVM + * instance will be quiesced for checkpointing for backup. Applicable only to NAS plugin. + * @param isolated * @return the result and {code}Backup{code} {code}Object{code} */ - Pair takeBackup(VirtualMachine vm, Boolean quiesceVM); + Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated); /** * Delete an existing backup @@ -87,17 +91,30 @@ public interface BackupProvider { */ boolean deleteBackup(Backup backup, boolean forced); - Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid); + /** + * Whether {@link #deleteBackup(Backup, boolean)} owns DB-row removal and resource-count / + * usage accounting for every backup it physically removes. Providers that manage incremental + * chains (e.g. NAS) delete several backups per call — the leaf plus swept delete-pending + * ancestors — and decrement once per removed backup themselves, so the manager must NOT + * decrement or remove the row again. Defaults to {@code false}: the manager does the + * single-backup accounting (the historical behaviour for non-chain providers). + */ + default boolean handlesChainDeleteResourceAccounting() { + return false; + } + + Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore); /** * Restore VM from backup */ - boolean restoreVMFromBackup(VirtualMachine vm, Backup backup); + boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId); /** * Restore a volume from a backup */ - Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState); + Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore); /** * Syncs backup metrics (backup size, protected size) from the plugin and stores it within the provider @@ -140,5 +157,4 @@ default boolean supportsMemoryVmSnapshot() { * @param zoneId the zone for which to return metrics */ void syncBackupStorageStats(Long zoneId); - } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java index 44fdf70c4c15..ddc823a14ff5 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java @@ -34,4 +34,5 @@ public interface BackupSchedule extends ControlledEntity, InternalIdentity { Boolean getQuiesceVM(); int getMaxBackups(); String getUuid(); + boolean isIsolated(); } diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java similarity index 52% rename from server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java rename to api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java index a1ec6854ecc0..e1153be3ae02 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java +++ b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java @@ -15,27 +15,47 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.storage.heuristics.presetvariables; +package org.apache.cloudstack.backup; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.InternalIdentity; -@RunWith(MockitoJUnitRunner.class) -public class DomainTest { +public interface ImageTransfer extends ControlledEntity, InternalIdentity { + long getDataCenterId(); - @Test - public void toStringTestReturnsValidJson() { - Domain variable = new Domain(); - variable.setName("test name"); - variable.setId("test id"); + public enum Direction { + upload, download + } + + public enum Format { + raw, + cow + } - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "id"); - String result = variable.toString(); + public enum Backend { + nbd, + file + } - Assert.assertEquals(expected, result); + public enum Phase { + initializing, transferring, finished, failed } + String getUuid(); + + Long getBackupId(); + + long getVolumeId(); + + long getHostId(); + + String getTransferUrl(); + + Phase getPhase(); + + Direction getDirection(); + + Backend getBackend(); + + String getSignedTicketId(); } diff --git a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java new file mode 100644 index 000000000000..0543fb10af36 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java @@ -0,0 +1,142 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.backup; + +import com.cloud.storage.Volume; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; +import org.apache.cloudstack.framework.config.ConfigKey; + +import java.util.Set; + +public interface InternalBackupProvider extends BackupProvider { + String VM_WORK_JOB_HANDLER = InternalBackupService.class.getSimpleName(); + + ConfigKey backupCompressionTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.compression.timeout", "28800", "Backup compression timeout (in " + + "seconds). Will only start counting once the backup compression async job actually starts. This setting is currently only applicable to KBOSS.", true, + ConfigKey.Scope.Cluster); + + ConfigKey backupCompressionMinimumFreeStorage = new ConfigKey<>("Advanced", Double.class, "backup.compression.minimum.free.storage", "1", "The minimum " + + "amount of free storage that should be available to start the compression. This configuration uses a multiplier on the backup size, by default, it needs the same " + + "amount of free storage as the backup uses while uncompressed. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Zone); + + ConfigKey backupCompressionCoroutines = new ConfigKey<>("Advanced", Integer.class, "backup.compression.coroutines", "1", "Number of parallel coroutines " + + "for the compression process. This is translated to qemu-img '-m' parameter. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + ConfigKey backupCompressionRateLimit = new ConfigKey<>("Advanced", Integer.class, "backup.compression.rate.limit", "0", "Limit the compression rate to " + + "this configuration's value (in MB/s). Values lower than 1 disable the limit. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + ConfigKey backupValidationTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.validation.timeout", "3600", "Backup validation job timeout (in " + + "seconds). Will only start counting once the backup validation async job actually starts. This setting is currently only applicable to KBOSS.", true, ConfigKey.Scope.Cluster); + + /** + * Actually execute the backup after being queued. + * */ + default Pair orchestrateTakeBackup(Backup backup, boolean quiesceVm, boolean isolated) { + return null; + } + + /** + * Actually delete the backup after being queued. + * */ + default Boolean orchestrateDeleteBackup(Backup backup, boolean forced) { + return null; + } + + /** + * Actually restore the backup after being queued. + * */ + default Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, boolean quickRestore, Long hostId, boolean sameVmAsBackup) { + return null; + } + + /** + * This method should be overwritten by any backup providers that want to schedule their backup restore jobs in the same queue as the VM jobs. + * Otherwise, just use the restoreBackedUpVolume method. + * */ + default Pair orchestrateRestoreBackedUpVolume(Backup backup, VirtualMachine vm, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + return null; + } + + /** + * This method should be overwritten by any native backup providers that want to allow backup compression through ACS.
+ * The compression is done in two steps:
+ * 1) Compress the backup to a different file;
+ * 2) Switch the old file for the newly compressed one.
+ *

+ * This method is supposed to execute step 1. + * + * @return + */ + default boolean startBackupCompression(long backupId, long hostId) { + return false; + } + + /** + * This method should be overwritten by any native backup providers that want to allow backup compression through ACS.
+ * The compression is done in two steps:
+ * 1) Compress the backup to a different file;
+ * 2) Switch the old file for the newly compressed one.
+ *

+ * This method is supposed to execute step 2. + * + * @return + */ + default boolean finalizeBackupCompression(long backupId, long hostId) { + return false; + } + + default boolean validateBackup(long backupId, long hostId) { + return false; + } + + /** + * This method should be overwritten by any native backup providers that allow volume detach but need to prepare it beforehand. + * */ + default void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { + } + + /** + * This method should be overwritten by any native backup providers that allow volume migration but need to prepare it beforehand. + * */ + default void prepareVolumeForMigration(Volume volume, VirtualMachine virtualMachine) { + } + + /** + * This method should be overwritten by any native backup providers that must update metadata regarding a volume after certain operations (such as after a volume migration). + * */ + default void updateVolumeId(VirtualMachine virtualMachine, long oldVolumeId, long newVolumeId) { + } + + default Set getSecondaryStorageUrls(UserVm userVm) { + return Set.of(); + } + + /** + * This method should be overwritten by any native backup providers that are compatible with VM Snapshots but need to prepare the VM to be reverted. + * Currently, the only strategy that calls this method is the {@code KvmFileBasedStorageVmSnapshotStrategy}. + * */ + default void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { + } + + default boolean finishBackupChain(VirtualMachine virtualMachine) { + return false; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java new file mode 100644 index 000000000000..76c71f0eb4ce --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupService.java @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.storage.Volume; +import com.cloud.uservm.UserVm; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; +import org.apache.cloudstack.api.response.ExtractResponse; + +import java.util.Set; + +public interface InternalBackupService { + + void configureChainInfo(DataTO volumeTo, Command cmd); + + void cleanupBackupMetadata(long volumeId); + + void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine); + + void prepareVolumeForMigration(Volume volume); + + void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot); + + void updateVolumeId(long oldVolumeId, long newVolumeId); + + Set getSecondaryStorageUrls(UserVm userVm); + + boolean startBackupCompression(long backupId, long hostId, long zoneId); + + boolean finalizeBackupCompression(long backupId, long hostId, long zoneId); + + boolean validateBackup(long backupId, long hostId, long zoneId); + + ExtractResponse downloadScreenshot(long backupId); + + boolean finishBackupChain(long vmId); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java b/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java new file mode 100644 index 000000000000..e1affb311ec6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java @@ -0,0 +1,106 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import java.util.List; + +import org.apache.cloudstack.api.command.admin.backup.CreateImageTransferCmd; +import org.apache.cloudstack.api.command.admin.backup.DeleteVmCheckpointCmd; +import org.apache.cloudstack.api.command.admin.backup.FinalizeBackupCmd; +import org.apache.cloudstack.api.command.admin.backup.FinalizeImageTransferCmd; +import org.apache.cloudstack.api.command.admin.backup.ListImageTransfersCmd; +import org.apache.cloudstack.api.command.admin.backup.ListVmCheckpointsCmd; +import org.apache.cloudstack.api.command.admin.backup.StartBackupCmd; +import org.apache.cloudstack.api.response.CheckpointResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.utils.component.PluggableService; + +/** + * Service for Creating Backups and ImageTransfer sessions which will be consumed by an external orchestrator. + */ +public interface KVMBackupExportService extends Configurable, PluggableService { + + ConfigKey ImageTransferIdleTimeoutSeconds = new ConfigKey<>("Advanced", Integer.class, + "image.transfer.idle.timeout.seconds", + "600", + "Seconds since last completed HTTP request to an image transfer before the image server unregisters it (idle timeout).", + true, ConfigKey.Scope.Zone); + + ConfigKey ExposeKVMBackupExportServiceApis = new ConfigKey<>("Advanced", Boolean.class, + "expose.kvm.backup.export.service.apis", + "false", + "Enable to expose APIs for testing the KVM Backup Export Service.", + false, ConfigKey.Scope.Global); + /** + * Creates a backup session for a VM + */ + Backup createBackup(StartBackupCmd cmd); + + /** + * Start a backup session for a VM + * Creates a new checkpoint and starts NBD server for pull-mode backup + */ + Backup startBackup(StartBackupCmd cmd); + + /** + * Finalize a backup session + * Stops NBD server, updates checkpoint tracking, deletes old checkpoints + */ + Backup finalizeBackup(FinalizeBackupCmd cmd); + + /** + * Create an image transfer object for a disk + * Registers NBD endpoint with ImageIO (stubbed for POC) + */ + ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd); + + ImageTransfer createImageTransfer(long volumeId, Long backupId, ImageTransfer.Direction direction, ImageTransfer.Format format); + + boolean cancelImageTransfer(long imageTransferId); + + /** + * Finalize an image transfer + * Marks transfer as complete (NBD is closed globally in finalize backup) + */ + boolean finalizeImageTransfer(FinalizeImageTransferCmd cmd); + + boolean finalizeImageTransfer(long imageTransferId); + + /** + * List image transfers for a backup + */ + List listImageTransfers(ListImageTransfersCmd cmd); + + /** + * List checkpoints for a VM + */ + List listVmCheckpoints(ListVmCheckpointsCmd cmd); + + /** + * Delete a VM checkpoint (no-op for normal flow, kept for API parity) + */ + boolean deleteVmCheckpoint(DeleteVmCheckpointCmd cmd); + + /** + * List Compatible Data Center Ids for the service + */ + List listCompatibleDataCenterIds(); +} diff --git a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java index b0fb1ac73c21..d2ebdc25f1bd 100644 --- a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java +++ b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Map; +import com.trilead.ssh2.Connection; + import org.apache.cloudstack.framework.ca.CAProvider; import org.apache.cloudstack.framework.ca.CAService; import org.apache.cloudstack.framework.ca.Certificate; @@ -39,7 +41,10 @@ public interface CAManager extends CAService, Configurable, PluggableService { ConfigKey CAProviderPlugin = new ConfigKey<>("Advanced", String.class, "ca.framework.provider.plugin", "root", - "The CA provider plugin that is used for secure CloudStack management server-agent communication for encryption and authentication. Restart management server(s) when changed.", true); + "The CA provider plugin used for CloudStack internal certificate management (MS-agent encryption and authentication). " + + "The default 'root' provider auto-generates a CA on first startup, but also supports user-provided custom CA material " + + "via the ca.plugin.root.private.key, ca.plugin.root.public.key, and ca.plugin.root.ca.certificate settings. " + + "Restart management server(s) when changed.", false); ConfigKey CertKeySize = new ConfigKey<>("Advanced", Integer.class, "ca.framework.cert.keysize", @@ -85,6 +90,12 @@ public interface CAManager extends CAService, Configurable, PluggableService { "The actual implementation will depend on the configured CA provider.", false); + ConfigKey CaInjectDefaultTruststore = new ConfigKey<>("Advanced", Boolean.class, + "ca.framework.inject.default.truststore", "true", + "When true, injects the CA provider's certificate into the JVM default truststore on management server startup. " + + "This allows outgoing HTTPS connections from the management server to trust servers with certificates signed by the configured CA. " + + "Restart management server(s) when changed.", false); + /** * Returns a list of available CA provider plugins * @return returns list of CAProvider @@ -130,12 +141,26 @@ public interface CAManager extends CAService, Configurable, PluggableService { boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider); /** - * Provisions certificate for given active and connected agent host + * Provisions certificate for given agent host. + * When forced=true, uses SSH to re-provision bypassing the NIO agent connection (for disconnected agents). * @param host + * @param reconnect * @param provider + * @param forced when true, provisions via SSH instead of NIO; supports KVM hosts and SystemVMs * @return returns success/failure as boolean */ - boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider); + boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider, final boolean forced); + + /** + * Provisions certificate for a KVM host using an existing SSH connection. + * Runs keystore-setup to generate a CSR, issues a certificate, then runs keystore-cert-import. + * Used during host discovery and for forced re-provisioning when the NIO agent is unreachable. + * @param sshConnection active SSH connection to the KVM host + * @param agentIp IP address of the KVM host agent + * @param agentHostname hostname of the KVM host agent + * @param caProvider optional CA provider plugin name (null uses default) + */ + void provisionCertificateViaSsh(Connection sshConnection, String agentIp, String agentHostname, String caProvider); /** * Setups up a new keystore and generates CSR for a host diff --git a/api/src/main/java/org/apache/cloudstack/context/CallContext.java b/api/src/main/java/org/apache/cloudstack/context/CallContext.java index 4cefd7847fdd..fcfb5b6b1e00 100644 --- a/api/src/main/java/org/apache/cloudstack/context/CallContext.java +++ b/api/src/main/java/org/apache/cloudstack/context/CallContext.java @@ -63,6 +63,7 @@ protected Stack initialValue() { private User user; private long userId; private final Map context = new HashMap(); + private final Map apiResourcesUuids = new HashMap<>(); private Project project; private String apiName; @@ -388,6 +389,14 @@ public void setEventDisplayEnabled(boolean eventDisplayEnabled) { isEventDisplayEnabled = eventDisplayEnabled; } + public String getApiResourceUuid(String paramName) { + return apiResourcesUuids.get(paramName); + } + + public void putApiResourceUuid(String paramName, String uuid) { + apiResourcesUuids.put(paramName, uuid); + } + public Map getContextParameters() { return context; } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java new file mode 100644 index 000000000000..231583244c2a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -0,0 +1,45 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import java.util.List; + +import org.apache.cloudstack.dns.exception.DnsProviderException; + +import com.cloud.utils.component.Adapter; + +public interface DnsProvider extends Adapter { + DnsProviderType getProviderType(); + + // Validates connectivity to the server + void validate(DnsServer server) throws Exception; + + String validateAndResolveServer(DnsServer server) throws Exception; + + // Zone Operations + String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException; + + String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException; + String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException; + boolean dnsZoneExists(DnsServer server, DnsZone zone); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java new file mode 100644 index 000000000000..5430884a32d9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import java.util.List; + +import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import com.cloud.user.Account; +import com.cloud.utils.component.Manager; +import com.cloud.utils.component.PluggableService; + +public interface DnsProviderManager extends Manager, PluggableService { + + DnsServer addDnsServer(AddDnsServerCmd cmd); + ListResponse listDnsServers(ListDnsServersCmd cmd); + DnsServer updateDnsServer(UpdateDnsServerCmd cmd); + boolean deleteDnsServer(DeleteDnsServerCmd cmd); + DnsServerResponse createDnsServerResponse(DnsServer server); + + // Allocates the DB row (State: Inactive) + DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); + // Calls the Plugin (State: Inactive -> Active) + DnsZone provisionDnsZone(long zoneId, boolean isImport); + + DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); + boolean deleteDnsZone(Long id, boolean isUnmanage); + ListResponse listDnsZones(ListDnsZonesCmd cmd); + + DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd); + boolean deleteDnsRecord(DeleteDnsRecordCmd cmd); + ListResponse listDnsRecords(ListDnsRecordsCmd cmd); + + List listProviderNames(); + + // Helper to create the response object + DnsZoneResponse createDnsZoneResponse(DnsZone zone); + DnsRecordResponse createDnsRecordResponse(DnsRecord record); + + DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd); + + boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); + + void checkDnsServerPermission(Account caller, DnsServer dnsServer); + + void checkDnsZonePermission(Account caller, DnsZone dnsZone); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java new file mode 100644 index 000000000000..23c8e936613f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java @@ -0,0 +1,23 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +public enum DnsProviderType { + PowerDNS; +// Cloudflare +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java new file mode 100644 index 000000000000..ae62e4729cca --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import java.util.List; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class DnsRecord { + + public enum RecordType { + A, AAAA, CNAME, MX, TXT, SRV, PTR, NS; + + public static RecordType fromString(String type) { + if (type == null) return null; + try { + return RecordType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("Invalid DNS Record Type: " + type + + ". Supported: " + java.util.Arrays.toString(values())); + } + } + } + + private String name; + private RecordType type; + private List contents; + private int ttl; + + public DnsRecord() {} + + public DnsRecord(String name, RecordType type, List contents, int ttl) { + this.name = name; + this.type = type; + this.contents = contents; + this.ttl = ttl; + } + + // Getters and Setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public RecordType getType() { return type; } + public void setType(RecordType type) { this.type = type; } + + public List getContents() { return contents; } + public void setContents(List contents) { this.contents = contents; } + + public int getTtl() { return ttl; } + public void setTtl(int ttl) { this.ttl = ttl; } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java new file mode 100644 index 000000000000..f34f94726d1b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DnsServer extends InternalIdentity, Identity, ControlledEntity { + enum State { + Enabled, Disabled + }; + + String getName(); + + String getUrl(); + + DnsProviderType getProviderType(); + + List getNameServers(); + + String getDnsApiKey(); + + long getAccountId(); + + boolean getPublicServer(); + + Date getCreated(); + + Date getRemoved(); + + String getPublicDomainSuffix(); + + Integer getPort(); + + Map getDetails(); + + String getDetail(String name); + + void setDetails(Map details); + + void appendDetails(String name, String value); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java new file mode 100644 index 000000000000..193185a6b0c6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import java.util.List; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DnsZone extends InternalIdentity, Identity, ControlledEntity { + enum ZoneType { + Public, Private + } + enum State { + Active, Inactive + } + + String getName(); + + long getDnsServerId(); + + long getAccountId(); + + ZoneType getType(); + + String getDescription(); + + List getAssociatedNetworks(); + + State getState(); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java new file mode 100644 index 000000000000..325cb78241ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.exception; + +/** + * Thrown when authentication to the DNS provider fails. + */ +public class DnsAuthenticationException extends DnsProviderException { + public DnsAuthenticationException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java new file mode 100644 index 000000000000..9a36bb87478a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.exception; + +/** + * Thrown when attempting to create a zone or record that already exists. + */ +public class DnsConflictException extends DnsProviderException { + public DnsConflictException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java new file mode 100644 index 000000000000..aa88f308ce84 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.exception; + +/** + * Thrown when the requested zone or record does not exist. + */ +public class DnsNotFoundException extends DnsProviderException { + public DnsNotFoundException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java new file mode 100644 index 000000000000..564acdc9a6f0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.exception; + +/** + * Thrown for unexpected or unknown errors returned by the DNS provider. + */ +public class DnsOperationException extends DnsProviderException { + public DnsOperationException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java new file mode 100644 index 000000000000..de307c9903e4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.exception; + +public class DnsProviderException extends Exception { + public DnsProviderException(String message) { + super(message); + } + + public DnsProviderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java similarity index 63% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java rename to api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java index 88265ee4e551..50f04143c921 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java @@ -15,20 +15,16 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.quota.activationrule.presetvariables; +package org.apache.cloudstack.dns.exception; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import java.io.IOException; -@RunWith(MockitoJUnitRunner.class) -public class RoleTest { +/** + * Thrown when HTTP or network errors occur communicating with the DNS provider. + */ +public class DnsTransportException extends DnsProviderException { - @Test - public void setTagsTestAddFieldTagsToCollection() { - Role variable = new Role(); - variable.setType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("type")); + public DnsTransportException(String message, IOException cause) { + super(message, cause); } } diff --git a/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java index 33ff70fcace5..558340694b8a 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java +++ b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java @@ -62,4 +62,12 @@ public Boolean getSuccess() { public void setResult(Map result) { this.result = result; } + + public Map getResult() { + return result; + } + + public boolean isSuccess() { + return Boolean.TRUE.equals(success); + } } diff --git a/api/src/main/java/org/apache/cloudstack/extension/Extension.java b/api/src/main/java/org/apache/cloudstack/extension/Extension.java index 3068612ed6fe..c1d905718b24 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/Extension.java +++ b/api/src/main/java/org/apache/cloudstack/extension/Extension.java @@ -24,7 +24,8 @@ public interface Extension extends InternalIdentity, Identity { enum Type { - Orchestrator + Orchestrator, + NetworkOrchestrator } enum State { Enabled, Disabled; diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java index 776b42f671b7..245faa762a2f 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java @@ -48,7 +48,9 @@ public interface ExtensionCustomAction extends InternalIdentity, Identity { enum ResourceType { - VirtualMachine(com.cloud.vm.VirtualMachine.class); + VirtualMachine(com.cloud.vm.VirtualMachine.class), + Network(com.cloud.network.Network.class), + Vpc(com.cloud.network.vpc.Vpc.class); private final Class clazz; diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java index f50f841ed74a..e3d84745f997 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java @@ -17,8 +17,126 @@ package org.apache.cloudstack.extension; +import java.util.List; +import java.util.Map; + +import com.cloud.network.Network.Capability; +import com.cloud.network.Network.Service; + public interface ExtensionHelper { Long getExtensionIdForCluster(long clusterId); Extension getExtension(long id); + Extension getExtensionByNameAndType(String name, Extension.Type type); Extension getExtensionForCluster(long clusterId); + List getExtensionReservedResourceDetails(long extensionId); + + /** + * Network Guru of Network Extension + */ + String NETWORK_EXTENSION_GURU_NAME = "NetworkExtensionGuestNetworkGuru"; + + /** + * Extension resource-map detail key holding the isolation method for the physical + * network (e.g. {@code "NetworkExtension"}). + * If not specified, the isolation method of physical network will be used. + */ + String NETWORK_ISOLATION_METHOD_DETAIL_KEY = "network.isolation.method"; + + /** + * Value of {@link #NETWORK_ISOLATION_METHOD_DETAIL_KEY} that indicates + * the extension owns the network isolation. + */ + String NETWORK_EXTENSION_ISOLATION_METHOD = "NetworkExtension"; + + /** + * Detail key used to store the comma-separated list of network services provided + * by a NetworkOrchestrator extension (e.g. {@code "SourceNat,StaticNat,Firewall"}). + */ + String NETWORK_SERVICES_DETAIL_KEY = "network.services"; + + /** + * Detail key used to store a JSON object mapping each service name to its + * CloudStack {@link com.cloud.network.Network.Capability} key/value pairs. + * Example: {@code {"SourceNat":{"SupportedSourceNatTypes":"peraccount"}}}. + * Used together with {@link #NETWORK_SERVICES_DETAIL_KEY}. + */ + String NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY = "network.service.capabilities"; + + String getExtensionScriptPath(Extension extension); + + /** + * Finds the extension registered with the given physical network whose name + * matches the given provider name (case-insensitive). Returns {@code null} + * if no matching extension is found. + * + *

This is the preferred lookup when multiple extensions are registered on + * the same physical network: the provider name stored in + * {@code ntwk_service_map} is used to pinpoint the exact extension that + * handles a given network.

+ * + * @param physicalNetworkId the physical network ID + * @param providerName the provider name (must equal the extension name) + * @return the matching {@link Extension}, or {@code null} + */ + Extension getExtensionForPhysicalNetworkAndProvider(long physicalNetworkId, String providerName); + + /** + * Returns ALL {@code extension_resource_map_details} (including hidden) for + * the specific extension registered on the given physical network. Used by + * {@code NetworkExtensionElement} to inject device credentials into the script + * environment for the correct extension when multiple different extensions are + * registered on the same physical network. + * + * @param physicalNetworkId the physical network ID + * @param extensionId the extension ID + * @return all key/value details including non-display ones, or an empty map + */ + Map getAllResourceMapDetailsForExtensionOnPhysicalNetwork(long physicalNetworkId, long extensionId); + + /** + * Returns {@code true} if the given provider name is backed by a + * {@code NetworkOrchestrator} extension registered on any physical network. + * This is used by {@code NetworkModelImpl} to detect extension-backed providers + * that are not in the static {@code s_providerToNetworkElementMap}. + * + * @param providerName the provider / extension name + * @return true if the provider is a NetworkExtension provider + */ + boolean isNetworkExtensionProvider(String providerName); + + /** + * List all registered extensions filtered by extension {@link Extension.Type}. + * Useful for callers that need to discover available providers of a given + * type (e.g. Orchestrator, NetworkOrchestrator). + * + * @param type extension type to filter by + * @return list of matching {@link Extension} instances (empty list if none) + */ + List listExtensionsByType(Extension.Type type); + + /** + * Returns {@code true} when the extension registered for {@code providerName} on + * the given physical network has its {@link #NETWORK_ISOLATION_METHOD_DETAIL_KEY} + * resource-map detail set to {@link #NETWORK_EXTENSION_ISOLATION_METHOD}. + * + * @param providerName provider / extension name + * @return true if the extension uses NetworkExtension isolation + */ + boolean usesNetworkExtensionIsolation(String providerName); + + /** + * Returns the effective {@link Service} → ({@link Capability} → value) capabilities + * for the given external network provider, looking it up by name on the given + * physical network. + * + *

If {@code physicalNetworkId} is {@code null}, the method searches across all + * physical networks that have extensions registered and returns the capabilities for + * the first matching extension.

+ * + * @param physicalNetworkId physical network ID, or {@code null} for offering-level queries + * @param providerName provider / extension name + * @return capabilities map, or the default capabilities if no matching extension is found + */ + Map> getNetworkCapabilitiesForProvider(Long physicalNetworkId, String providerName); + } diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java index 40ebc19eb5e3..604e64a1f894 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java @@ -24,7 +24,8 @@ public interface ExtensionResourceMap extends InternalIdentity, Identity { enum ResourceType { - Cluster + Cluster, + PhysicalNetwork } long getExtensionId(); diff --git a/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java new file mode 100644 index 000000000000..1c281c15f286 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/NetworkCustomActionProvider.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.extension; + +import java.util.Map; + +import com.cloud.network.Network; +import com.cloud.network.vpc.Vpc; + +/** + * Implemented by network elements that support running custom actions on a + * managed network or VPC (e.g. NetworkExtensionElement). + * + *

This interface is looked up by {@code ExtensionsManagerImpl} to dispatch + * {@code runCustomAction} requests whose resource type is {@code Network} + * or {@code Vpc}.

+ */ +public interface NetworkCustomActionProvider { + + /** + * Returns {@code true} if this provider handles networks whose physical + * network has an ExternalNetwork service provider registered. + * + * @param network the target network + * @return {@code true} if this provider can handle the network + */ + boolean canHandleCustomAction(Network network); + + /** + * Returns {@code true} if this provider can handle custom actions for + * the given VPC. + * + * @param vpc the target VPC + * @return {@code true} if this provider can handle the VPC + */ + boolean canHandleVpcCustomAction(Vpc vpc); + + /** + * Runs a named custom action against the external network device that + * manages the given network. + * + * @param network the CloudStack network on which to run the action + * @param actionName the action name (e.g. {@code "reboot-device"}, {@code "dump-config"}) + * @param parameters optional parameters supplied by the caller + * @return output from the action script, or {@code null} on failure + */ + String runCustomAction(Network network, String actionName, Map parameters); + + /** + * Runs a named custom action against the external network device that + * manages the given VPC. + * + * @param vpc the CloudStack VPC on which to run the action + * @param actionName the action name + * @param parameters optional parameters supplied by the caller + * @return output from the action script, or {@code null} on failure + */ + String runCustomAction(Vpc vpc, String actionName, Map parameters); +} diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java index e54d53138ef6..cb3708cda373 100644 --- a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java @@ -44,4 +44,6 @@ public interface GuiThemeJoin extends InternalIdentity, Identity { Date getCreated(); Date getRemoved(); + + String getLoginBaseDomain(); } diff --git a/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java b/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java new file mode 100644 index 000000000000..bbb8617ff24f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface HSMProfile extends Identity, InternalIdentity, ControlledEntity { + String getName(); + + String getProtocol(); + + long getAccountId(); + + long getDomainId(); + + Long getZoneId(); + + String getVendorName(); + + boolean isEnabled(); + + boolean getIsPublic(); + + Date getCreated(); + + Date getRemoved(); +} diff --git a/api/src/main/java/org/apache/cloudstack/kms/KMSKey.java b/api/src/main/java/org/apache/cloudstack/kms/KMSKey.java new file mode 100644 index 000000000000..c956a1ec66d8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/kms/KMSKey.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.kms; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.framework.kms.KeyPurpose; + +import java.util.Date; + +/** + * KMS Key (Key Encryption Key) metadata. + * Represents a KEK that can be used to wrap/unwrap Data Encryption Keys (DEKs). + * KEKs are account-scoped and used for envelope encryption. + */ +public interface KMSKey extends Identity, InternalIdentity, ControlledEntity { + + String getName(); + + String getDescription(); + + /** + * Provider-specific KEK label/ID (internal identifier used by the KMS provider) + */ + String getKekLabel(); + + KeyPurpose getPurpose(); + + Long getZoneId(); + + String getAlgorithm(); + + Integer getKeyBits(); + + boolean isEnabled(); + + Date getCreated(); + + Date getRemoved(); + + Long getHsmProfileId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java b/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java new file mode 100644 index 000000000000..6e19010227d7 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java @@ -0,0 +1,292 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import com.cloud.storage.Volume; +import com.cloud.user.Account; +import com.cloud.utils.component.Manager; +import org.apache.cloudstack.api.command.admin.kms.MigrateVolumesToKMSCmd; +import org.apache.cloudstack.api.command.user.kms.RotateKMSKeyCmd; +import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd; +import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd; +import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd; +import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.CreateHSMProfileCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.DeleteHSMProfileCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.ListHSMProfilesCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.UpdateHSMProfileCmd; +import org.apache.cloudstack.api.response.HSMProfileResponse; +import org.apache.cloudstack.api.response.KMSKeyResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.framework.kms.KMSProvider; +import org.apache.cloudstack.framework.kms.WrappedKey; + +import java.util.List; + +public interface KMSManager extends Manager, Configurable { + + ConfigKey KMSDekSizeBits = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.dek.size.bits", + "256", + "The size of Data Encryption Keys (DEK) in bits (128, 192, or 256)", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSRetryCount = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.retry.count", + "3", + "Number of retry attempts for transient KMS failures", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSRetryDelayMs = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.retry.delay.ms", + "1000", + "Delay in milliseconds between KMS retry attempts (exponential backoff)", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSOperationTimeoutSec = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.operation.timeout.sec", + "30", + "Timeout in seconds for KMS cryptographic operations", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSRewrapBatchSize = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.rewrap.batch.size", + "50", + "Number of wrapped keys to rewrap per batch in background job", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSRewrapIntervalMs = new ConfigKey<>( + "Advanced", + Long.class, + "kms.rewrap.interval.ms", + "300000", + "Interval in milliseconds between background rewrap job executions (default: 5 minutes)", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSOperationPoolCoreSize = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.operation.pool.core.size", + "2", + "Minimum number of threads kept alive for KMS cryptographic operations", + true, + ConfigKey.Scope.Global + ); + + ConfigKey KMSOperationPoolMaxSize = new ConfigKey<>( + "Advanced", + Integer.class, + "kms.operation.pool.max.size", + "100", + "Maximum number of concurrent threads for KMS cryptographic operations. " + + "Set this to match the concurrency limit of your HSM appliance or external KMS provider.", + true, + ConfigKey.Scope.Global + ); + + /** + * List all registered KMS providers + * + * @return list of available providers + */ + List listKMSProviders(); + + /** + * Get a specific KMS provider by name + * + * @param name provider name + * @return the provider, or null if not found + */ + KMSProvider getKMSProvider(String name); + + /** + * Check if caller has permission to use a KMS key + * + * @param callerAccountId the caller's account ID + * @param key the KMS key + * @return true if caller has permission + */ + boolean hasPermission(Long callerAccountId, KMSKey key); + + /** + * Validates that the KMS key can be used for volume encryption: key exists, not deleted, + * owner account matches the volume owner, key state is Enabled, and key purpose is VOLUME_ENCRYPTION. + * No-op if kmsKeyId is null. + * + * @param owner the account that will own the volume + * @param kmsKeyId the KMS key database ID + * @param zoneId the zone ID of the target resource (volume/VM) + * @throws InvalidParameterValueException if key not found, disabled, wrong purpose, zone mismatch, or account mismatch + */ + void checkKmsKeyForVolumeEncryption(Account owner, Long kmsKeyId, Long zoneId); + + /** + * Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed + * + * @param wrappedKeyId the wrapped key database ID + * @return plaintext DEK (caller must zeroize!) + * @throws KMSException if unwrap fails + */ + byte[] unwrapKey(Long wrappedKeyId) throws KMSException; + + /** + * Generate and wrap a DEK using a specific KMS key UUID + * + * @param kmsKey the KMS key + * @param callerAccountId the caller's account ID + * @return wrapped key ready for database storage + * @throws KMSException if operation fails + */ + WrappedKey generateVolumeKeyWithKek(KMSKey kmsKey, Long callerAccountId) throws KMSException; + + /** + * Create a KMS key and return the response object. + * Handles validation, account resolution, and permission checks. + * + * @param cmd the create command with all parameters + * @return KMSKeyResponse + * @throws KMSException if creation fails + */ + KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException; + + /** + * List KMS keys and return the response object. + * Handles validation and permission checks. + * + * @param cmd the list command with all parameters + * @return ListResponse with KMSKeyResponse objects + */ + ListResponse listKMSKeys(ListKMSKeysCmd cmd); + + /** + * Update a KMS key and return the response object. + * Handles validation and permission checks. + * + * @param cmd the update command with all parameters + * @return KMSKeyResponse + * @throws KMSException if update fails + */ + KMSKeyResponse updateKMSKey(UpdateKMSKeyCmd cmd) throws KMSException; + + boolean deleteKMSWrappedKey(Volume vol) throws KMSException; + + /** + * Delete a KMS key and return the response object. + * Handles validation and permission checks. + * + * @param cmd the delete command with all parameters + * @return SuccessResponse + * @throws KMSException if deletion fails + */ + SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException; + + /** + * Rotate KEK by creating new version and scheduling gradual re-encryption + * + * @param cmd the rotate command with all parameters + * @return New KEK version UUID + * @throws KMSException if rotation fails + */ + String rotateKMSKey(RotateKMSKeyCmd cmd) throws KMSException; + + /** + * Migrate passphrase-based volumes to KMS encryption + * + * @param cmd the migrate command with all parameters + * @return Number of volumes successfully migrated + * @throws KMSException if migration fails + */ + int migrateVolumesToKMS(MigrateVolumesToKMSCmd cmd) throws KMSException; + + /** + * Delete all KMS keys owned by an account (called during account cleanup) + * + * @param accountId the account ID + * @return true if all keys were successfully deleted + */ + boolean deleteKMSKeysByAccountId(Long accountId); + + /** + * Add a new HSM profile + * + * @param cmd the add command + * @return the created HSM profile + * @throws KMSException if addition fails + */ + HSMProfile addHSMProfile(CreateHSMProfileCmd cmd) throws KMSException; + + /** + * List HSM profiles + * + * @param cmd the list command + * @return list of HSM profiles + */ + ListResponse listHSMProfiles(ListHSMProfilesCmd cmd); + + /** + * Delete an HSM profile + * + * @param cmd the delete command + * @return true if deletion was successful + * @throws KMSException if deletion fails + */ + boolean deleteHSMProfile(DeleteHSMProfileCmd cmd) throws KMSException; + + /** + * Update an HSM profile + * + * @param cmd the update command + * @return the updated HSM profile + * @throws KMSException if update fails + */ + HSMProfile updateHSMProfile(UpdateHSMProfileCmd cmd) throws KMSException; + + /** + * Create a response object for an HSM profile + * + * @param profile the HSM profile + * @return the response object + */ + HSMProfileResponse createHSMProfileResponse(HSMProfile profile); +} diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 5cd67ffe9bad..b6362e9a9c9b 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -41,6 +41,7 @@ import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; import org.apache.cloudstack.api.command.user.address.ListQuarantinedIpsCmd; import org.apache.cloudstack.api.command.user.affinitygroup.ListAffinityGroupsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupServiceJobsCmd; import org.apache.cloudstack.api.command.user.bucket.ListBucketsCmd; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; import org.apache.cloudstack.api.command.user.iso.ListIsosCmd; @@ -63,6 +64,7 @@ import org.apache.cloudstack.api.command.user.zone.ListZonesCmd; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.BackupServiceJobResponse; import org.apache.cloudstack.api.response.BucketResponse; import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse; @@ -119,7 +121,7 @@ public interface QueryService { "Determines whether users can view certain VM settings. When set to empty, default value used is: rootdisksize, cpuOvercommitRatio, memoryOvercommitRatio, Message.ReservedCapacityFreed.Flag.", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null); ConfigKey UserVMReadOnlyDetails = new ConfigKey<>(String.class, - "user.vm.readonly.details", "Advanced", "dataDiskController, rootDiskController", + "user.vm.readonly.details", "Advanced", "dataDiskController, rootDiskController, backupValidationCommandTimeout, backupValidationScreenshotWait, backupValidationBootTimeout", "List of read-only VM settings/details as comma separated string", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null, ""); ConfigKey SortKeyAscending = new ConfigKey<>("Advanced", Boolean.class, "sortkey.algorithm", "true", @@ -145,6 +147,8 @@ public interface QueryService { ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; + List searchForAccessibleUsers(); + ListResponse searchForEvents(ListEventsCmd cmd); ListResponse listTags(ListTagsCmd cmd); @@ -222,4 +226,6 @@ public interface QueryService { ListResponse searchForObjectStores(ListObjectStoragePoolsCmd listObjectStoragePoolsCmd); ListResponse searchForBuckets(ListBucketsCmd listBucketsCmd); + + ListResponse listBackupServiceJobs(ListBackupServiceJobsCmd cmd); } diff --git a/api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java b/api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java new file mode 100644 index 000000000000..6b3f57b6aa5e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/resourcelimit/Reserver.java @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.resourcelimit; + +/** + * Interface implemented by CheckedReservation. + *

+ * This is defined in cloud-api to allow methods declared in modules that do not depend on cloud-server + * to receive CheckedReservations as parameters. + */ +public interface Reserver extends AutoCloseable { + + void close(); + +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedule.java b/api/src/main/java/org/apache/cloudstack/schedule/ResourceSchedule.java similarity index 66% rename from api/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedule.java rename to api/src/main/java/org/apache/cloudstack/schedule/ResourceSchedule.java index b1e7df3c39cf..4bd39211a27f 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMSchedule.java +++ b/api/src/main/java/org/apache/cloudstack/schedule/ResourceSchedule.java @@ -16,20 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule; +package org.apache.cloudstack.schedule; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; import java.time.ZoneId; import java.util.Date; -public interface VMSchedule extends Identity, InternalIdentity { - enum Action { - START, STOP, REBOOT, FORCE_STOP, FORCE_REBOOT +public interface ResourceSchedule extends Identity, InternalIdentity { + + /** + * Common contract for scheduler actions. Each provider defines its own enum + * implementing this interface so the generic machinery can call {@link #name()} + * and {@link #getEventType()} without knowing the concrete type. + */ + interface Action { + String name(); + + String getEventType(); } - long getVmId(); + ApiCommandResourceType getResourceType(); + + long getResourceId(); String getDescription(); @@ -37,7 +48,7 @@ enum Action { String getTimeZone(); - Action getAction(); + String getActionName(); boolean getEnabled(); diff --git a/api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleManager.java b/api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleManager.java new file mode 100644 index 000000000000..fb650a6cf68d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleManager.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.schedule; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface ResourceScheduleManager { + + ResourceScheduleResponse createSchedule(ApiCommandResourceType resourceType, String resourceUuid, + String description, String schedule, String timeZone, String action, + Date startDate, Date endDate, boolean enabled, Map details); + + ResourceScheduleResponse updateSchedule(Long id, String description, String schedule, String timeZone, + Date startDate, Date endDate, Boolean enabled, Map details); + + ListResponse listSchedule(Long id, List ids, ApiCommandResourceType resourceType, + String resourceUuid, String action, Boolean enabled, + Long startIndex, Long pageSize); + + Long removeSchedule(ApiCommandResourceType resourceType, String resourceUuid, Long id, List ids); + + void removeSchedulesForResource(ApiCommandResourceType resourceType, long resourceId); +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJob.java b/api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJob.java similarity index 77% rename from api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJob.java rename to api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJob.java index d7a18b768279..a7bf4d9aa05e 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJob.java +++ b/api/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJob.java @@ -16,23 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule; +package org.apache.cloudstack.schedule; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; import java.util.Date; -public interface VMScheduledJob extends Identity, InternalIdentity { - long getVmId(); +public interface ResourceScheduledJob extends Identity, InternalIdentity { + ApiCommandResourceType getResourceType(); - long getVmScheduleId(); + long getResourceId(); + + long getScheduleId(); Long getAsyncJobId(); void setAsyncJobId(long asyncJobId); - VMSchedule.Action getAction(); + String getActionName(); Date getScheduledTime(); } diff --git a/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java b/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java new file mode 100644 index 000000000000..c05dafe88796 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/schedule/autoscale/AutoScaleScheduleAction.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.schedule.autoscale; + +import com.cloud.event.EventTypes; +import org.apache.cloudstack.schedule.ResourceSchedule; + +public enum AutoScaleScheduleAction implements ResourceSchedule.Action { + UPDATE { + @Override + public String getEventType() { + return EventTypes.EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE; + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/schedule/vm/VMScheduleAction.java b/api/src/main/java/org/apache/cloudstack/schedule/vm/VMScheduleAction.java new file mode 100644 index 000000000000..1648305803ff --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/schedule/vm/VMScheduleAction.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.schedule.vm; + +import com.cloud.event.EventTypes; +import org.apache.cloudstack.schedule.ResourceSchedule; + +public enum VMScheduleAction implements ResourceSchedule.Action { + START { + @Override + public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_START; } + }, + STOP { + @Override + public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_STOP; } + }, + REBOOT { + @Override + public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_REBOOT; } + }, + FORCE_STOP { + @Override + public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_FORCE_STOP; } + }, + FORCE_REBOOT { + @Override + public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_FORCE_REBOOT; } + }; +} diff --git a/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java b/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java index f23e4b0b633b..489e9bf54f23 100644 --- a/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java +++ b/api/src/main/java/org/apache/cloudstack/secstorage/heuristics/HeuristicType.java @@ -18,8 +18,8 @@ /** * The type of the heuristic used in the allocation process of secondary storage resources. - * Valid options are: {@link #ISO}, {@link #SNAPSHOT}, {@link #TEMPLATE} and {@link #VOLUME} + * Valid options are: {@link #ISO}, {@link #SNAPSHOT}, {@link #TEMPLATE}, {@link #VOLUME} and {@link #BACKUP} */ public enum HeuristicType { - ISO, SNAPSHOT, TEMPLATE, VOLUME + ISO, SNAPSHOT, TEMPLATE, VOLUME, BACKUP } diff --git a/api/src/main/java/org/apache/cloudstack/storage/object/BucketApiService.java b/api/src/main/java/org/apache/cloudstack/storage/object/BucketApiService.java index e27ef308d7f2..8c164133db88 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/object/BucketApiService.java +++ b/api/src/main/java/org/apache/cloudstack/storage/object/BucketApiService.java @@ -95,7 +95,7 @@ public interface BucketApiService { */ Bucket createBucket(CreateBucketCmd cmd); - boolean deleteBucket(long bucketId, Account caller); + boolean deleteBucket(long bucketId, Account caller) throws ResourceAllocationException; boolean updateBucket(UpdateBucketCmd cmd, Account caller) throws ResourceAllocationException; diff --git a/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java b/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java index 21184de27a26..3e349cf0bd39 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java +++ b/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java @@ -23,6 +23,10 @@ import org.apache.cloudstack.api.command.user.storage.sharedfs.ChangeSharedFSServiceOfferingCmd; import org.apache.cloudstack.api.command.user.storage.sharedfs.CreateSharedFSCmd; import org.apache.cloudstack.api.command.user.storage.sharedfs.DestroySharedFSCmd; +import org.apache.cloudstack.api.command.user.storage.sharedfs.ListSharedFSCmd; +import org.apache.cloudstack.api.command.user.storage.sharedfs.UpdateSharedFSCmd; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.SharedFSResponse; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ManagementServerException; @@ -31,11 +35,6 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.VirtualMachineMigrationException; -import org.apache.cloudstack.api.command.user.storage.sharedfs.ListSharedFSCmd; -import org.apache.cloudstack.api.command.user.storage.sharedfs.UpdateSharedFSCmd; -import org.apache.cloudstack.api.response.SharedFSResponse; -import org.apache.cloudstack.api.response.ListResponse; - public interface SharedFSService { List getSharedFSProviders(); @@ -69,4 +68,10 @@ public interface SharedFSService { SharedFS recoverSharedFS(Long sharedFSId); void deleteSharedFS(Long sharedFSId); + + SharedFS getSharedFSByUuid(String uuid); + + SharedFS getSharedFSForVmId(long vmId); + + SharedFS updateSharedFSPostRestore(long sharedFsId, long volumeId); } diff --git a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java index 5f69f3e46e73..13f01b4944ad 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java +++ b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java @@ -25,17 +25,28 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.VolumeForImportResponse; import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import java.util.Arrays; import java.util.List; -public interface VolumeImportUnmanageService extends PluggableService { +public interface VolumeImportUnmanageService extends PluggableService, Configurable { List SUPPORTED_HYPERVISORS = Arrays.asList(Hypervisor.HypervisorType.KVM, Hypervisor.HypervisorType.VMware); List SUPPORTED_STORAGE_POOL_TYPES_FOR_KVM = Arrays.asList(Storage.StoragePoolType.NetworkFilesystem, - Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD); + Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint); + + ConfigKey AllowImportVolumeWithBackingFile = new ConfigKey<>(Boolean.class, + "allow.import.volume.with.backing.file", + "Advanced", + "false", + "If enabled, allows QCOW2 volumes with backing files to be imported or unmanaged", + true, + ConfigKey.Scope.Global, + null); ListResponse listVolumesForImport(ListVolumesForImportCmd cmd); diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java index bba97dff71cb..cbb7c4de698a 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java @@ -55,6 +55,9 @@ public enum PowerState { private String hostName; + private String hypervisorType; + private String hostHypervisorVersion; + private List disks; private List nics; @@ -168,6 +171,22 @@ public void setHostName(String hostName) { this.hostName = hostName; } + public String getHypervisorType() { + return hypervisorType; + } + + public void setHypervisorType(String hypervisorType) { + this.hypervisorType = hypervisorType; + } + + public String getHostHypervisorVersion() { + return hostHypervisorVersion; + } + + public void setHostHypervisorVersion(String hostHypervisorVersion) { + this.hostHypervisorVersion = hostHypervisorVersion; + } + public List getDisks() { return disks; } diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java index b6233b9c2704..e1963313eb6f 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java @@ -26,6 +26,8 @@ public interface UnmanagedVMsManager extends VmImportService, UnmanageVMService, PluggableService, Configurable { + String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso"; + String KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME = "kvm-default-vm-import-dummy-template"; ConfigKey UnmanageVMPreserveNic = new ConfigKey<>("Advanced", Boolean.class, "unmanage.vm.preserve.nics", "false", "If set to true, do not remove VM nics (and its MAC addresses) when unmanaging a VM, leaving them allocated but not reserved. " + "If set to false, nics are removed and MAC addresses can be reassigned", true, ConfigKey.Scope.Zone); diff --git a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleManager.java b/api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleManager.java deleted file mode 100644 index 6aca05b58d5e..000000000000 --- a/api/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleManager.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.cloudstack.vm.schedule; - -import org.apache.cloudstack.api.command.user.vm.CreateVMScheduleCmd; -import org.apache.cloudstack.api.command.user.vm.DeleteVMScheduleCmd; -import org.apache.cloudstack.api.command.user.vm.ListVMScheduleCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVMScheduleCmd; -import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.api.response.VMScheduleResponse; - -public interface VMScheduleManager { - VMScheduleResponse createSchedule(CreateVMScheduleCmd createVMScheduleCmd); - - VMScheduleResponse createResponse(VMSchedule vmSchedule); - - ListResponse listSchedule(ListVMScheduleCmd listVMScheduleCmd); - - VMScheduleResponse updateSchedule(UpdateVMScheduleCmd updateVMScheduleCmd); - - long removeScheduleByVmId(long vmId, boolean expunge); - - Long removeSchedule(DeleteVMScheduleCmd deleteVMScheduleCmd); -} diff --git a/api/src/test/java/com/cloud/network/NetworkTest.java b/api/src/test/java/com/cloud/network/NetworkTest.java new file mode 100644 index 000000000000..dba4d1fb7eb5 --- /dev/null +++ b/api/src/test/java/com/cloud/network/NetworkTest.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.network; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + + +public class NetworkTest { + + @Test + public void testProviderContains() { + List providers = new ArrayList<>(); + providers.add(Network.Provider.VirtualRouter); + + // direct instance present + assertTrue("List should contain VirtualRouter provider", providers.contains(Network.Provider.VirtualRouter)); + + // resolved provider by name (registered provider) + Network.Provider resolved = Network.Provider.getProvider("VirtualRouter"); + assertNotNull("Resolved provider should not be null", resolved); + assertTrue("List should contain resolved VirtualRouter provider", providers.contains(resolved)); + + // transient provider with same name should be considered equal (equals by name) + Network.Provider transientProvider = Network.Provider.createTransientProvider("NetworkExtension"); + assertFalse("List should not contain the transient provider", providers.contains(transientProvider)); + + providers.add(transientProvider); + assertTrue("List should contain the transient provider", providers.contains(transientProvider)); + + // another transient provider with same name should be considered equal + Network.Provider transientProviderNew = Network.Provider.createTransientProvider("NetworkExtension"); + assertTrue("List should contain the new transient provider with same name", providers.contains(transientProviderNew)); + } + + @Test + public void testCustomActionServiceLookup() { + Network.Service customAction = Network.Service.getService("CustomAction"); + assertNotNull("CustomAction service should be available", customAction); + assertTrue("CustomAction should be part of the supported services list", + Network.Service.listAllServices().contains(customAction)); + } + + @Test + public void testTransientProviderIsNotGloballyRegistered() { + Network.Provider transientProvider = Network.Provider.createTransientProvider("TransientOnly"); + assertNotNull(transientProvider); + assertNull("Transient provider should not be retrievable from the global registry", + Network.Provider.getProvider("TransientOnly")); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java index 79e6127d29ad..b99ba48c66dc 100644 --- a/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java +++ b/api/src/test/java/org/apache/cloudstack/acl/RuleTest.java @@ -17,13 +17,46 @@ package org.apache.cloudstack.acl; import com.cloud.exception.InvalidParameterValueException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.apache.cloudstack.api.APICommand; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import java.util.Arrays; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; public class RuleTest { + private static List apiNames; + private static List apiRules; + private static ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + + @BeforeClass + public static void setup() { + provider.addIncludeFilter(new AnnotationTypeFilter(APICommand.class)); + Set beanDefinitions = provider.findCandidateComponents("org.apache.cloudstack.api"); + + apiNames = new ArrayList<>(); + apiRules = new ArrayList<>(); + for(BeanDefinition bd : beanDefinitions) { + if (bd instanceof AnnotatedBeanDefinition) { + Map annotationAttributeMap = ((AnnotatedBeanDefinition) bd).getMetadata() + .getAnnotationAttributes(APICommand.class.getName()); + String apiName = annotationAttributeMap.get("name").toString(); + apiNames.add(apiName); + apiRules.add(new Rule(apiName)); + } + } + } + @Test public void testToString() throws Exception { Rule rule = new Rule("someString"); @@ -31,21 +64,89 @@ public void testToString() throws Exception { } @Test - public void testMatchesEmpty() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches("")); + public void ruleMatchesTestNoMatchesOnEmptyString() throws Exception { + String testCmd = ""; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertEquals(matches.size(), 0); } @Test - public void testMatchesNull() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(null)); + public void ruleMatchesTestNoMatchesOnNull() throws Exception { + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(null)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); } @Test - public void testMatchesSpace() throws Exception { - Rule rule = new Rule("someString"); - Assert.assertFalse(rule.matches(" ")); + public void ruleMatchesTestNoMatchesOnSpaceCharacter() throws Exception { + String testCmd = " "; + List matches = new ArrayList<>(); + for (Rule rule : apiRules) { + if (rule.matches(testCmd)) { + matches.add(rule.getRuleString()); + } + } + + Assert.assertTrue(matches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnEndWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*"); + Rule acsRegexRule = new Rule("list*"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnMiddleWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile("list.*s"); + Rule acsRegexRule = new Rule("list*s"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); + } + + @Test + public void ruleMatchesTestWildCardOnStartWorksAsNormalRegex() { + setup(); + Pattern regexPattern = Pattern.compile(".*User"); + Rule acsRegexRule = new Rule("*User"); + + List nonMatches = new ArrayList<>(); + for (String apiName : apiNames) { + if (acsRegexRule.matches(apiName) != regexPattern.matcher(apiName).matches()) { + nonMatches.add(apiName); + } + } + + Assert.assertTrue(nonMatches.isEmpty()); } @Test @@ -73,7 +174,25 @@ public void testMatchesWildcardMiddle() throws Exception { } @Test - public void testValidateRuleWithValidData() throws Exception { + public void ruleMatchesTestWildcardOnRuleAndCommand() throws Exception { + Rule rule = new Rule("*"); + Assert.assertTrue(rule.matches("list*")); + } + + @Test + public void ruleMatchesTestWildcardOnRuleAndCommandNotAllowed() throws Exception { + Rule rule = new Rule("list*"); + Assert.assertFalse(rule.matches("*")); + } + + @Test + public void ruleMatchesTestWithMultipleStars() throws Exception { + Rule rule = new Rule("list***"); + Assert.assertFalse(rule.matches("api")); + } + + @Test + public void testRuleToStringWithValidStrings() throws Exception { for (String rule : Arrays.asList("a", "1", "someApi", "someApi321", "123SomeApi", "prefix*", "*middle*", "*Suffix", "*", "**", "f***", "m0nk3yMa**g1c*")) { @@ -82,7 +201,7 @@ public void testValidateRuleWithValidData() throws Exception { } @Test - public void testValidateRuleWithInvalidData() throws Exception { + public void testRuleToStringWithInvalidStrings() throws Exception { for (String rule : Arrays.asList(null, "", " ", " ", "\n", "\t", "\r", "\"", "\'", "^someApi$", "^someApi", "some$", "some-Api;", "some,Api", "^", "$", "^$", ".*", "\\w+", "r**l3rd0@Kr3", "j@s1n|+|0ȷ", diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java new file mode 100644 index 000000000000..a1412d5a76a7 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmdTest.java @@ -0,0 +1,301 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.backup; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneBackupOfferingCmdTest { + + private CloneBackupOfferingCmd cloneBackupOfferingCmd; + + @Mock + private BackupManager backupManager; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private BackupOffering mockBackupOffering; + + @Mock + private BackupOfferingResponse mockBackupOfferingResponse; + + @Before + public void setUp() { + cloneBackupOfferingCmd = new CloneBackupOfferingCmd(); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "backupManager", backupManager); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneBackupOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetName() { + String name = "ClonedBackupOffering"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", name); + assertEquals(name, cloneBackupOfferingCmd.getName()); + } + + @Test + public void testGetDescription() { + String description = "Cloned Backup Offering Description"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", description); + assertEquals(description, cloneBackupOfferingCmd.getDescription()); + } + + @Test + public void testGetZoneId() { + Long zoneId = 123L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", zoneId); + assertEquals(zoneId, cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testGetExternalId() { + String externalId = "external-backup-123"; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", externalId); + assertEquals(externalId, cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testGetAllowUserDrivenBackups() { + Boolean allowUserDrivenBackups = true; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", allowUserDrivenBackups); + assertEquals(allowUserDrivenBackups, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testAllowUserDrivenBackupsDefaultTrue() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", null); + Boolean result = cloneBackupOfferingCmd.getUserDrivenBackups(); + assertTrue(result == null || result); + } + + @Test + public void testAllowUserDrivenBackupsFalse() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false); + assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testExecuteSuccess() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering); + when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse); + + cloneBackupOfferingCmd.execute(); + + assertNotNull(cloneBackupOfferingCmd.getResponseObject()); + assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteFailure() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(null); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone backup offering", e.getMessage()); + } + } + + @Test + public void testExecuteWithInvalidParameterException() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))) + .thenThrow(new InvalidParameterValueException("Invalid source offering ID")); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode()); + assertEquals("Invalid source offering ID", e.getMessage()); + } + } + + @Test + public void testExecuteWithCloudRuntimeException() throws Exception { + Long sourceOfferingId = 999L; + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))) + .thenThrow(new CloudRuntimeException("Runtime error during clone")); + + try { + cloneBackupOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Runtime error during clone", e.getMessage()); + } + } + + @Test + public void testExecuteSuccessWithAllParameters() throws Exception { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Test Description"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "ext-123"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true); + + when(backupManager.cloneBackupOffering(any(CloneBackupOfferingCmd.class))).thenReturn(mockBackupOffering); + when(responseGenerator.createBackupOfferingResponse(mockBackupOffering)).thenReturn(mockBackupOfferingResponse); + + cloneBackupOfferingCmd.execute(); + + assertNotNull(cloneBackupOfferingCmd.getResponseObject()); + assertEquals(mockBackupOfferingResponse, cloneBackupOfferingCmd.getResponseObject()); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Cloned backup offering for testing"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 123L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "externalId", "external-backup-123"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", true); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName()); + assertEquals("Cloned backup offering for testing", cloneBackupOfferingCmd.getDescription()); + assertEquals(Long.valueOf(123L), cloneBackupOfferingCmd.getZoneId()); + assertEquals("external-backup-123", cloneBackupOfferingCmd.getExternalId()); + assertEquals(Boolean.TRUE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } + + @Test + public void testCloneWithMinimalParameters() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Description"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedBackupOffering", cloneBackupOfferingCmd.getName()); + assertEquals("Description", cloneBackupOfferingCmd.getDescription()); + + assertNull(cloneBackupOfferingCmd.getZoneId()); + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testNameNullByDefault() { + assertNull(cloneBackupOfferingCmd.getName()); + } + + @Test + public void testDescriptionNullByDefault() { + assertNull(cloneBackupOfferingCmd.getDescription()); + } + + @Test + public void testZoneIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testExternalIdNullByDefault() { + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testCloneBackupOfferingInheritingZone() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited zone"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertNull(cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testCloneBackupOfferingInheritingExternalId() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with inherited external ID"); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertNull(cloneBackupOfferingCmd.getExternalId()); + } + + @Test + public void testCloneBackupOfferingOverridingZone() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone with new zone"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "zoneId", 456L); + + assertEquals(Long.valueOf(999L), cloneBackupOfferingCmd.getSourceOfferingId()); + assertEquals(Long.valueOf(456L), cloneBackupOfferingCmd.getZoneId()); + } + + @Test + public void testCloneBackupOfferingDisallowUserDrivenBackups() { + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "sourceOfferingId", 999L); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "name", "ClonedBackupOffering"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "description", "Clone without user-driven backups"); + ReflectionTestUtils.setField(cloneBackupOfferingCmd, "userDrivenBackups", false); + + assertEquals(Boolean.FALSE, cloneBackupOfferingCmd.getUserDrivenBackups()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java new file mode 100644 index 000000000000..096395b1359e --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CloneNetworkOfferingCmdTest.java @@ -0,0 +1,324 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.network; + +import com.cloud.offering.NetworkOffering; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkOfferingResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneNetworkOfferingCmdTest { + + private CloneNetworkOfferingCmd cloneNetworkOfferingCmd; + + @Mock + private com.cloud.configuration.ConfigurationService configService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private NetworkOffering mockNetworkOffering; + + @Mock + private NetworkOfferingResponse mockNetworkOfferingResponse; + + @Before + public void setUp() { + cloneNetworkOfferingCmd = new CloneNetworkOfferingCmd(); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_configService", configService); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneNetworkOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetAddServices() { + List addServices = Arrays.asList("Dhcp", "Dns"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices); + assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices()); + } + + @Test + public void testGetDropServices() { + List dropServices = Arrays.asList("Firewall", "Vpn"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices); + assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices()); + } + + @Test + public void testGetGuestIpType() { + String guestIpType = "Isolated"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", guestIpType); + assertEquals(guestIpType, cloneNetworkOfferingCmd.getGuestIpType()); + } + + @Test + public void testGetTraffictype() { + String trafficType = "GUEST"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", trafficType); + assertEquals(trafficType, cloneNetworkOfferingCmd.getTraffictype()); + } + + @Test + public void testGetName() { + String name = "ClonedNetworkOffering"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name); + assertEquals(name, cloneNetworkOfferingCmd.getNetworkOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned Network Offering Display Text"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneNetworkOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedNetworkOffering"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", name); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", null); + assertEquals(name, cloneNetworkOfferingCmd.getDisplayText()); + } + + @Test + public void testGetAvailability() { + String availability = "Required"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", availability); + assertEquals(availability, cloneNetworkOfferingCmd.getAvailability()); + } + + @Test + public void testGetTags() { + String tags = "tag1,tag2,tag3"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "tags", tags); + assertEquals(tags, cloneNetworkOfferingCmd.getTags()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(mockNetworkOffering); + when(responseGenerator.createNetworkOfferingResponse(mockNetworkOffering)).thenReturn(mockNetworkOfferingResponse); + + cloneNetworkOfferingCmd.execute(); + + assertNotNull(cloneNetworkOfferingCmd.getResponseObject()); + assertEquals(mockNetworkOfferingResponse, cloneNetworkOfferingCmd.getResponseObject()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() { + Long sourceOfferingId = 123L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneNetworkOffering(any(CloneNetworkOfferingCmd.class))).thenReturn(null); + + try { + cloneNetworkOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone network offering", e.getMessage()); + throw e; + } + } + + @Test + public void testGetConserveMode() { + Boolean conserveMode = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", conserveMode); + assertEquals(conserveMode, cloneNetworkOfferingCmd.getConserveMode()); + } + + @Test + public void testGetSpecifyVlan() { + Boolean specifyVlan = false; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", specifyVlan); + assertEquals(specifyVlan, cloneNetworkOfferingCmd.getSpecifyVlan()); + } + + @Test + public void testGetSpecifyIpRanges() { + Boolean specifyIpRanges = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyIpRanges", specifyIpRanges); + assertEquals(specifyIpRanges, cloneNetworkOfferingCmd.getSpecifyIpRanges()); + } + + @Test + public void testGetIsPersistent() { + Boolean isPersistent = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", isPersistent); + assertEquals(isPersistent, cloneNetworkOfferingCmd.getIsPersistent()); + } + + @Test + public void testGetEgressDefaultPolicy() { + Boolean egressDefaultPolicy = false; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", egressDefaultPolicy); + assertEquals(egressDefaultPolicy, cloneNetworkOfferingCmd.getEgressDefaultPolicy()); + } + + @Test + public void testGetServiceOfferingId() { + Long serviceOfferingId = 456L; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", serviceOfferingId); + assertEquals(serviceOfferingId, cloneNetworkOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testGetForVpc() { + Boolean forVpc = true; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "forVpc", forVpc); + assertEquals(forVpc, cloneNetworkOfferingCmd.getForVpc()); + } + + @Test + public void testGetMaxConnections() { + Integer maxConnections = 1000; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "maxConnections", maxConnections); + assertEquals(maxConnections, cloneNetworkOfferingCmd.getMaxconnections()); + } + + @Test + public void testGetNetworkRate() { + Integer networkRate = 200; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", networkRate); + assertEquals(networkRate, cloneNetworkOfferingCmd.getNetworkRate()); + } + + @Test + public void testGetInternetProtocol() { + String internetProtocol = "ipv4"; + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "internetProtocol", internetProtocol); + assertEquals(internetProtocol, cloneNetworkOfferingCmd.getInternetProtocol()); + } + + @Test + public void testAddServicesNullByDefault() { + assertNull(cloneNetworkOfferingCmd.getAddServices()); + } + + @Test + public void testDropServicesNullByDefault() { + assertNull(cloneNetworkOfferingCmd.getDropServices()); + } + + @Test + public void testSupportedServicesParameter() { + List supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "supportedServices", supportedServices); + assertEquals(supportedServices, cloneNetworkOfferingCmd.getSupportedServices()); + } + + @Test + public void testServiceProviderListParameter() { + Map> serviceProviderList = new HashMap<>(); + + HashMap dhcpProvider = new HashMap<>(); + dhcpProvider.put("service", "Dhcp"); + dhcpProvider.put("provider", "VirtualRouter"); + + HashMap dnsProvider = new HashMap<>(); + dnsProvider.put("service", "Dns"); + dnsProvider.put("provider", "VirtualRouter"); + + serviceProviderList.put("0", dhcpProvider); + serviceProviderList.put("1", dnsProvider); + + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceProviderList", serviceProviderList); + + Map> result = cloneNetworkOfferingCmd.getServiceProviders(); + assertNotNull(result); + assertEquals(2, result.size()); + assertNotNull(result.get("Dhcp")); + assertNotNull(result.get("Dns")); + assertEquals("VirtualRouter", result.get("Dhcp").get(0)); + assertEquals("VirtualRouter", result.get("Dns").get(0)); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkOfferingName", "ClonedOffering"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "displayText", "Cloned Offering Display"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "availability", "Optional"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "guestIptype", "Isolated"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "traffictype", "GUEST"); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "conserveMode", true); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "specifyVlan", false); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "isPersistent", true); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "egressDefaultPolicy", false); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "networkRate", 200); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "serviceOfferingId", 456L); + + assertEquals(Long.valueOf(123L), cloneNetworkOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedOffering", cloneNetworkOfferingCmd.getNetworkOfferingName()); + assertEquals("Cloned Offering Display", cloneNetworkOfferingCmd.getDisplayText()); + assertEquals("Optional", cloneNetworkOfferingCmd.getAvailability()); + assertEquals("Isolated", cloneNetworkOfferingCmd.getGuestIpType()); + assertEquals("GUEST", cloneNetworkOfferingCmd.getTraffictype()); + assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getConserveMode()); + assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getSpecifyVlan()); + assertEquals(Boolean.TRUE, cloneNetworkOfferingCmd.getIsPersistent()); + assertEquals(Boolean.FALSE, cloneNetworkOfferingCmd.getEgressDefaultPolicy()); + assertEquals(Integer.valueOf(200), cloneNetworkOfferingCmd.getNetworkRate()); + assertEquals(Long.valueOf(456L), cloneNetworkOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testCloneWithAddAndDropServices() { + List addServices = Arrays.asList("StaticNat", "PortForwarding"); + List dropServices = Arrays.asList("Vpn"); + + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "sourceOfferingId", 123L); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "addServices", addServices); + ReflectionTestUtils.setField(cloneNetworkOfferingCmd, "dropServices", dropServices); + + assertEquals(addServices, cloneNetworkOfferingCmd.getAddServices()); + assertEquals(dropServices, cloneNetworkOfferingCmd.getDropServices()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java index e1393e316993..38d0df2e8b82 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForGuestNetworkCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.Ipv4SubnetForGuestNetworkResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.Ipv4GuestSubnetNetworkMap; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -29,6 +30,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class CreateIpv4SubnetForGuestNetworkCmdTest { @@ -37,6 +40,7 @@ public class CreateIpv4SubnetForGuestNetworkCmdTest { @Test public void testCreateIpv4SubnetForGuestNetworkCmd() { Long parentId = 1L; + String parentUuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; Integer cidrSize = 26; @@ -46,12 +50,14 @@ public void testCreateIpv4SubnetForGuestNetworkCmd() { ReflectionTestUtils.setField(cmd, "cidrSize", cidrSize); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("parentid", parentUuid); + Assert.assertEquals(parentId, cmd.getParentId()); Assert.assertEquals(subnet, cmd.getSubnet()); Assert.assertEquals(cidrSize, cmd.getCidrSize()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_IP4_GUEST_SUBNET_CREATE, cmd.getEventType()); - Assert.assertEquals(String.format("Creating guest IPv4 subnet %s in zone subnet=%s", subnet, parentId), cmd.getEventDescription()); + Assert.assertEquals(String.format("Creating guest IPv4 subnet %s in zone subnet: %s", subnet, parentUuid), cmd.getEventDescription()); Ipv4GuestSubnetNetworkMap ipv4GuestSubnetNetworkMap = Mockito.mock(Ipv4GuestSubnetNetworkMap.class); Mockito.when(routedIpv4Manager.createIpv4SubnetForGuestNetwork(cmd)).thenReturn(ipv4GuestSubnetNetworkMap); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java index 51c1eb986c47..560ecdc3b296 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/CreateIpv4SubnetForZoneCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.DataCenterIpv4SubnetResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -29,6 +30,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class CreateIpv4SubnetForZoneCmdTest { @@ -37,6 +40,7 @@ public class CreateIpv4SubnetForZoneCmdTest { @Test public void testCreateIpv4SubnetForZoneCmd() { Long zoneId = 1L; + String zoneUuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; String accountName = "user"; Long projectId = 10L; @@ -50,6 +54,8 @@ public void testCreateIpv4SubnetForZoneCmd() { ReflectionTestUtils.setField(cmd,"domainId", domainId); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("zoneid", zoneUuid); + Assert.assertEquals(zoneId, cmd.getZoneId()); Assert.assertEquals(subnet, cmd.getSubnet()); Assert.assertEquals(accountName, cmd.getAccountName()); @@ -57,7 +63,7 @@ public void testCreateIpv4SubnetForZoneCmd() { Assert.assertEquals(domainId, cmd.getDomainId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_ZONE_IP4_SUBNET_CREATE, cmd.getEventType()); - Assert.assertEquals(String.format("Creating guest IPv4 subnet %s for zone=%s", subnet, zoneId), cmd.getEventDescription()); + Assert.assertEquals(String.format("Creating guest IPv4 subnet %s for zone: %s", subnet, zoneUuid), cmd.getEventDescription()); DataCenterIpv4GuestSubnet zoneSubnet = Mockito.mock(DataCenterIpv4GuestSubnet.class); Mockito.when(routedIpv4Manager.createDataCenterIpv4GuestSubnet(cmd)).thenReturn(zoneSubnet); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java index 7db77098b233..4640510ccdab 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DedicateIpv4SubnetForZoneCmdTest.java @@ -19,6 +19,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.DataCenterIpv4SubnetResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class DedicateIpv4SubnetForZoneCmdTest { @@ -36,6 +39,7 @@ public class DedicateIpv4SubnetForZoneCmdTest { @Test public void testDedicateIpv4SubnetForZoneCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; @@ -47,6 +51,8 @@ public void testDedicateIpv4SubnetForZoneCmd() { ReflectionTestUtils.setField(cmd,"domainId", domainId); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(accountName, cmd.getAccountName()); Assert.assertEquals(projectId, cmd.getProjectId()); @@ -54,7 +60,7 @@ public void testDedicateIpv4SubnetForZoneCmd() { Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_ZONE_IP4_SUBNET_DEDICATE, cmd.getEventType()); - Assert.assertEquals(String.format("Dedicating zone IPv4 subnet %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Dedicating zone's IPv4 subnet with ID: %s", uuid), cmd.getEventDescription()); DataCenterIpv4GuestSubnet zoneSubnet = Mockito.mock(DataCenterIpv4GuestSubnet.class); Mockito.when(routedIpv4Manager.dedicateDataCenterIpv4GuestSubnet(cmd)).thenReturn(zoneSubnet); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java index a4af5ddf748f..cd25d8d24014 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForGuestNetworkCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class DeleteIpv4SubnetForGuestNetworkCmdTest { @@ -36,15 +39,18 @@ public class DeleteIpv4SubnetForGuestNetworkCmdTest { @Test public void testDeleteIpv4SubnetForGuestNetworkCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); DeleteIpv4SubnetForGuestNetworkCmd cmd = new DeleteIpv4SubnetForGuestNetworkCmd(); ReflectionTestUtils.setField(cmd, "id", id); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_IP4_GUEST_SUBNET_DELETE, cmd.getEventType()); - Assert.assertEquals(String.format("Deleting guest IPv4 subnet %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Deleting guest IPv4 subnet with ID: %s", uuid), cmd.getEventDescription()); Mockito.when(routedIpv4Manager.deleteIpv4SubnetForGuestNetwork(cmd)).thenReturn(true); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java index 7af173f09d96..269fb3f3c192 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/DeleteIpv4SubnetForZoneCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class DeleteIpv4SubnetForZoneCmdTest { @@ -36,15 +39,18 @@ public class DeleteIpv4SubnetForZoneCmdTest { @Test public void testDeleteIpv4SubnetForZoneCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); DeleteIpv4SubnetForZoneCmd cmd = new DeleteIpv4SubnetForZoneCmd(); ReflectionTestUtils.setField(cmd, "id", id); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_ZONE_IP4_SUBNET_DELETE, cmd.getEventType()); - Assert.assertEquals(String.format("Deleting zone IPv4 subnet %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Deleting zone IPv4 subnet with ID: %s", uuid), cmd.getEventDescription()); Mockito.when(routedIpv4Manager.deleteDataCenterIpv4GuestSubnet(cmd)).thenReturn(true); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java index 9ce9a4f9464f..6e5d2a95f3ab 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/ReleaseDedicatedIpv4SubnetForZoneCmdTest.java @@ -19,6 +19,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.DataCenterIpv4SubnetResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class ReleaseDedicatedIpv4SubnetForZoneCmdTest { @@ -36,15 +39,18 @@ public class ReleaseDedicatedIpv4SubnetForZoneCmdTest { @Test public void testReleaseDedicatedIpv4SubnetForZoneCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); ReleaseDedicatedIpv4SubnetForZoneCmd cmd = new ReleaseDedicatedIpv4SubnetForZoneCmd(); ReflectionTestUtils.setField(cmd, "id", id); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_ZONE_IP4_SUBNET_RELEASE, cmd.getEventType()); - Assert.assertEquals(String.format("Releasing a dedicated zone IPv4 subnet %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Releasing dedicated zone IPv4 subnet with ID: %s", uuid), cmd.getEventDescription()); DataCenterIpv4GuestSubnet zoneSubnet = Mockito.mock(DataCenterIpv4GuestSubnet.class); Mockito.when(routedIpv4Manager.releaseDedicatedDataCenterIpv4GuestSubnet(cmd)).thenReturn(zoneSubnet); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java index cdb9cce22d83..af37006eafd2 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/UpdateIpv4SubnetForZoneCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.DataCenterIpv4SubnetResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -29,6 +30,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class UpdateIpv4SubnetForZoneCmdTest { @@ -37,6 +40,7 @@ public class UpdateIpv4SubnetForZoneCmdTest { @Test public void testUpdateIpv4SubnetForZoneCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); String subnet = "192.168.1.0/24"; UpdateIpv4SubnetForZoneCmd cmd = new UpdateIpv4SubnetForZoneCmd(); @@ -44,11 +48,13 @@ public void testUpdateIpv4SubnetForZoneCmd() { ReflectionTestUtils.setField(cmd, "subnet", subnet); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(subnet, cmd.getSubnet()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_ZONE_IP4_SUBNET_UPDATE, cmd.getEventType()); - Assert.assertEquals(String.format("Updating zone IPv4 subnet %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Updating zone IPv4 subnet with ID: %s", uuid), cmd.getEventDescription()); DataCenterIpv4GuestSubnet zoneSubnet = Mockito.mock(DataCenterIpv4GuestSubnet.class); Mockito.when(routedIpv4Manager.updateDataCenterIpv4GuestSubnet(cmd)).thenReturn(zoneSubnet); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java index 28ddad17afe5..3db1fab466f6 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForNetworkCmdTest.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -33,6 +34,7 @@ import java.util.Arrays; import java.util.List; +import java.util.UUID; @RunWith(MockitoJUnitRunner.class) public class ChangeBgpPeersForNetworkCmdTest { @@ -44,6 +46,7 @@ public class ChangeBgpPeersForNetworkCmdTest { @Test public void testChangeBgpPeersForNetworkCmd() { Long networkId = 10L; + String networkUuid = UUID.randomUUID().toString(); List bgpPeerIds = Arrays.asList(20L, 21L); ChangeBgpPeersForNetworkCmd cmd = new ChangeBgpPeersForNetworkCmd(); @@ -52,11 +55,13 @@ public void testChangeBgpPeersForNetworkCmd() { ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); ReflectionTestUtils.setField(cmd,"_responseGenerator", _responseGenerator); + CallContext.current().putApiResourceUuid("networkid", networkUuid); + Assert.assertEquals(networkId, cmd.getNetworkId()); Assert.assertEquals(bgpPeerIds, cmd.getBgpPeerIds()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_NETWORK_BGP_PEER_UPDATE, cmd.getEventType()); - Assert.assertEquals(String.format("Changing Bgp Peers for network %s", networkId), cmd.getEventDescription()); + Assert.assertEquals(String.format("Changing BGP Peers for Network with ID: %s", networkUuid), cmd.getEventDescription()); Network network = Mockito.mock(Network.class); Mockito.when(routedIpv4Manager.changeBgpPeersForNetwork(cmd)).thenReturn(network); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java index 96eb1f020de2..fb85f7060684 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ChangeBgpPeersForVpcCmdTest.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.VpcResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -33,6 +34,7 @@ import java.util.Arrays; import java.util.List; +import java.util.UUID; @RunWith(MockitoJUnitRunner.class) public class ChangeBgpPeersForVpcCmdTest { @@ -44,6 +46,7 @@ public class ChangeBgpPeersForVpcCmdTest { @Test public void testChangeBgpPeersForVpcCmd() { Long VpcId = 10L; + String vpcUuid = UUID.randomUUID().toString(); List bgpPeerIds = Arrays.asList(20L, 21L); ChangeBgpPeersForVpcCmd cmd = new ChangeBgpPeersForVpcCmd(); @@ -52,11 +55,13 @@ public void testChangeBgpPeersForVpcCmd() { ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); ReflectionTestUtils.setField(cmd,"_responseGenerator", _responseGenerator); + CallContext.current().putApiResourceUuid("vpcid", vpcUuid); + Assert.assertEquals(VpcId, cmd.getVpcId()); Assert.assertEquals(bgpPeerIds, cmd.getBgpPeerIds()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_VPC_BGP_PEER_UPDATE, cmd.getEventType()); - Assert.assertEquals(String.format("Changing Bgp Peers for VPC %s", VpcId), cmd.getEventDescription()); + Assert.assertEquals(String.format("Changing BGP Peers for VPC with ID: %s", vpcUuid), cmd.getEventDescription()); Vpc Vpc = Mockito.mock(Vpc.class); Mockito.when(routedIpv4Manager.changeBgpPeersForVpc(cmd)).thenReturn(Vpc); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java index 0d802bf36199..3abc5f57d017 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/CreateBgpPeerCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.BgpPeerResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.BgpPeer; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -29,6 +30,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class CreateBgpPeerCmdTest { @@ -37,6 +40,7 @@ public class CreateBgpPeerCmdTest { @Test public void testCreateBgpPeerCmd() { Long zoneId = 1L; + String zoneUuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; @@ -56,6 +60,8 @@ public void testCreateBgpPeerCmd() { ReflectionTestUtils.setField(cmd,"domainId", domainId); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("zoneid", zoneUuid); + Assert.assertEquals(zoneId, cmd.getZoneId()); Assert.assertEquals(ip4Address, cmd.getIp4Address()); Assert.assertEquals(ip6Address, cmd.getIp6Address()); @@ -67,7 +73,7 @@ public void testCreateBgpPeerCmd() { Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_BGP_PEER_CREATE, cmd.getEventType()); - Assert.assertEquals(String.format("Creating Bgp Peer %s for zone=%s", peerAsNumber, zoneId), cmd.getEventDescription()); + Assert.assertEquals(String.format("Creating BGP Peer %s for zone with ID: %s", peerAsNumber, zoneUuid), cmd.getEventDescription()); BgpPeer bgpPeer = Mockito.mock(BgpPeer.class); Mockito.when(routedIpv4Manager.createBgpPeer(cmd)).thenReturn(bgpPeer); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java index f3ae007da285..c5edb1b8f53f 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DedicateBgpPeerCmdTest.java @@ -19,6 +19,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.BgpPeerResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.BgpPeer; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class DedicateBgpPeerCmdTest { @@ -36,6 +39,7 @@ public class DedicateBgpPeerCmdTest { @Test public void testDedicateBgpPeerCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); String accountName = "user"; Long projectId = 10L; Long domainId = 11L; @@ -47,6 +51,8 @@ public void testDedicateBgpPeerCmd() { ReflectionTestUtils.setField(cmd,"domainId", domainId); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(accountName, cmd.getAccountName()); Assert.assertEquals(projectId, cmd.getProjectId()); @@ -54,7 +60,7 @@ public void testDedicateBgpPeerCmd() { Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_BGP_PEER_DEDICATE, cmd.getEventType()); - Assert.assertEquals(String.format("Dedicating Bgp Peer %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Dedicating BGP Peer with ID: %s", uuid), cmd.getEventDescription()); BgpPeer bgpPeer = Mockito.mock(BgpPeer.class); Mockito.when(routedIpv4Manager.dedicateBgpPeer(cmd)).thenReturn(bgpPeer); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java index 5e747188fda3..5228a63dc92d 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/DeleteBgpPeerCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class DeleteBgpPeerCmdTest { @@ -36,15 +39,18 @@ public class DeleteBgpPeerCmdTest { @Test public void testDeleteBgpPeerCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); DeleteBgpPeerCmd cmd = new DeleteBgpPeerCmd(); ReflectionTestUtils.setField(cmd, "id", id); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_BGP_PEER_DELETE, cmd.getEventType()); - Assert.assertEquals(String.format("Deleting Bgp Peer %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Deleting BGP Peer with ID: %s", uuid), cmd.getEventDescription()); Mockito.when(routedIpv4Manager.deleteBgpPeer(cmd)).thenReturn(true); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java index 8c55c4a73479..60a814d63050 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/ReleaseDedicatedBgpPeerCmdTest.java @@ -19,6 +19,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.BgpPeerResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.BgpPeer; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -28,6 +29,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class ReleaseDedicatedBgpPeerCmdTest { @@ -36,15 +39,18 @@ public class ReleaseDedicatedBgpPeerCmdTest { @Test public void testReleaseDedicatedBgpPeerCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); ReleaseDedicatedBgpPeerCmd cmd = new ReleaseDedicatedBgpPeerCmd(); ReflectionTestUtils.setField(cmd, "id", id); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_BGP_PEER_RELEASE, cmd.getEventType()); - Assert.assertEquals(String.format("Releasing a dedicated Bgp Peer %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Releasing dedicated BGP Peer with ID: %s", uuid), cmd.getEventDescription()); BgpPeer bgpPeer = Mockito.mock(BgpPeer.class); Mockito.when(routedIpv4Manager.releaseDedicatedBgpPeer(cmd)).thenReturn(bgpPeer); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java index 003944c61474..d594bc5718b7 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/network/bgp/UpdateBgpPeerCmdTest.java @@ -20,6 +20,7 @@ import com.cloud.event.EventTypes; import org.apache.cloudstack.api.response.BgpPeerResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.BgpPeer; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; @@ -29,6 +30,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class UpdateBgpPeerCmdTest { @@ -37,6 +40,7 @@ public class UpdateBgpPeerCmdTest { @Test public void testUpdateBgpPeerCmd() { Long id = 1L; + String uuid = UUID.randomUUID().toString(); String ip4Address = "ip4-address"; String ip6Address = "ip6-address"; Long peerAsNumber = 15000L; @@ -50,6 +54,8 @@ public void testUpdateBgpPeerCmd() { ReflectionTestUtils.setField(cmd, "password", peerPassword); ReflectionTestUtils.setField(cmd,"routedIpv4Manager", routedIpv4Manager); + CallContext.current().putApiResourceUuid("id", uuid); + Assert.assertEquals(id, cmd.getId()); Assert.assertEquals(ip4Address, cmd.getIp4Address()); Assert.assertEquals(ip6Address, cmd.getIp6Address()); @@ -57,7 +63,7 @@ public void testUpdateBgpPeerCmd() { Assert.assertEquals(peerPassword, cmd.getPassword()); Assert.assertEquals(1L, cmd.getEntityOwnerId()); Assert.assertEquals(EventTypes.EVENT_BGP_PEER_UPDATE, cmd.getEventType()); - Assert.assertEquals(String.format("Updating Bgp Peer %s", id), cmd.getEventDescription()); + Assert.assertEquals(String.format("Updating BGP Peer with ID: %s", uuid), cmd.getEventDescription()); BgpPeer bgpPeer = Mockito.mock(BgpPeer.class); Mockito.when(routedIpv4Manager.updateBgpPeer(cmd)).thenReturn(bgpPeer); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java new file mode 100644 index 000000000000..b4f7c55bd1fa --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CloneServiceOfferingCmdTest.java @@ -0,0 +1,669 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.offering; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.offering.ServiceOffering; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.vm.lease.VMLeaseManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneServiceOfferingCmdTest { + + private CloneServiceOfferingCmd cloneServiceOfferingCmd; + + @Mock + private com.cloud.configuration.ConfigurationService configService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private ServiceOffering mockServiceOffering; + + @Mock + private ServiceOfferingResponse mockServiceOfferingResponse; + + @Before + public void setUp() { + cloneServiceOfferingCmd = new CloneServiceOfferingCmd(); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_configService", configService); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetServiceOfferingName() { + String name = "ClonedServiceOffering"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name); + assertEquals(name, cloneServiceOfferingCmd.getServiceOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned Service Offering Display Text"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneServiceOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedServiceOffering"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", name); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", null); + assertEquals(name, cloneServiceOfferingCmd.getDisplayText()); + } + + @Test + public void testGetCpu() { + Integer cpu = 4; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", cpu); + assertEquals(cpu, cloneServiceOfferingCmd.getCpuNumber()); + } + + @Test + public void testGetMemory() { + Integer memory = 8192; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", memory); + assertEquals(memory, cloneServiceOfferingCmd.getMemory()); + } + + @Test + public void testGetCpuSpeed() { + Integer cpuSpeed = 2000; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", cpuSpeed); + assertEquals(cpuSpeed, cloneServiceOfferingCmd.getCpuSpeed()); + } + + @Test + public void testGetOfferHa() { + Boolean offerHa = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", offerHa); + assertEquals(offerHa, cloneServiceOfferingCmd.isOfferHa()); + } + + @Test + public void testGetLimitCpuUse() { + Boolean limitCpuUse = false; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", limitCpuUse); + assertEquals(limitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse()); + } + + @Test + public void testGetVolatileVm() { + Boolean volatileVm = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", volatileVm); + assertEquals(volatileVm, cloneServiceOfferingCmd.isVolatileVm()); + } + + @Test + public void testGetStorageType() { + String storageType = "local"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", storageType); + assertEquals(storageType, cloneServiceOfferingCmd.getStorageType()); + } + + @Test + public void testGetTags() { + String tags = "ssd,premium,dedicated"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", tags); + assertEquals(tags, cloneServiceOfferingCmd.getTags()); + } + + @Test + public void testGetHostTag() { + String hostTag = "gpu-enabled"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", hostTag); + assertEquals(hostTag, cloneServiceOfferingCmd.getHostTag()); + } + + @Test + public void testGetNetworkRate() { + Integer networkRate = 1000; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", networkRate); + assertEquals(networkRate, cloneServiceOfferingCmd.getNetworkRate()); + } + + @Test + public void testGetDeploymentPlanner() { + String deploymentPlanner = "UserDispersingPlanner"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", deploymentPlanner); + assertEquals(deploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner()); + } + + @Test + public void testGetDetails() { + Map> details = new HashMap<>(); + + HashMap cpuOvercommit = new HashMap<>(); + cpuOvercommit.put("key", "cpuOvercommitRatio"); + cpuOvercommit.put("value", "2.0"); + + HashMap memoryOvercommit = new HashMap<>(); + memoryOvercommit.put("key", "memoryOvercommitRatio"); + memoryOvercommit.put("value", "1.5"); + + details.put("0", cpuOvercommit); + details.put("1", memoryOvercommit); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details); + + Map result = cloneServiceOfferingCmd.getDetails(); + assertNotNull(result); + assertEquals("2.0", result.get("cpuOvercommitRatio")); + assertEquals("1.5", result.get("memoryOvercommitRatio")); + } + + @Test + public void testIsPurgeResources() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesFalse() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", false); + assertFalse(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testIsPurgeResourcesDefaultFalse() { + assertFalse(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testGetLeaseDuration() { + Integer leaseDuration = 3600; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration); + assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration()); + } + + @Test + public void testGetLeaseExpiryAction() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "stop"); + assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction()); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "DESTROY"); + assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetLeaseExpiryActionInvalidValue() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "InvalidAction"); + cloneServiceOfferingCmd.getLeaseExpiryAction(); + } + + @Test + public void testGetVgpuProfileId() { + Long vgpuProfileId = 10L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId); + assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId()); + } + + @Test + public void testGetGpuCount() { + Integer gpuCount = 2; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount); + assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount()); + } + + @Test + public void testGetGpuDisplay() { + Boolean gpuDisplay = true; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay); + assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteFailure() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(null); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone service offering", e.getMessage()); + } + } + + @Test + public void testExecuteSuccessWithAllParameters() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedOffering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Test Display"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithInvalidParameterException() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))) + .thenThrow(new InvalidParameterValueException("Invalid source offering ID")); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode()); + assertEquals("Invalid source offering ID", e.getMessage()); + } + } + + @Test + public void testExecuteWithCloudRuntimeException() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))) + .thenThrow(new com.cloud.utils.exception.CloudRuntimeException("Runtime error during clone")); + + try { + cloneServiceOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Runtime error during clone", e.getMessage()); + } + } + + @Test + public void testExecuteResponseNameIsSet() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + // Verify that response name would be set (actual verification would require accessing the response object's internal state) + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "ClonedServiceOffering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", "Cloned Service Offering"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", 4); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", 8192); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", 2000); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", true); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", false); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "isVolatile", true); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", "local"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", "premium"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", "gpu-enabled"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", 1000); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", "UserDispersingPlanner"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", true); + + assertEquals(Long.valueOf(555L), cloneServiceOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedServiceOffering", cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals("Cloned Service Offering", cloneServiceOfferingCmd.getDisplayText()); + assertEquals(Integer.valueOf(4), cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(Integer.valueOf(8192), cloneServiceOfferingCmd.getMemory()); + assertEquals(Integer.valueOf(2000), cloneServiceOfferingCmd.getCpuSpeed()); + assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isOfferHa()); + assertEquals(Boolean.FALSE, cloneServiceOfferingCmd.isLimitCpuUse()); + assertEquals(Boolean.TRUE, cloneServiceOfferingCmd.isVolatileVm()); + assertEquals("local", cloneServiceOfferingCmd.getStorageType()); + assertEquals("premium", cloneServiceOfferingCmd.getTags()); + assertEquals("gpu-enabled", cloneServiceOfferingCmd.getHostTag()); + assertEquals(Integer.valueOf(1000), cloneServiceOfferingCmd.getNetworkRate()); + assertEquals("UserDispersingPlanner", cloneServiceOfferingCmd.getDeploymentPlanner()); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneServiceOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetSystemVmType() { + String systemVmType = "domainrouter"; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "systemVmType", systemVmType); + assertEquals(systemVmType, cloneServiceOfferingCmd.getSystemVmType()); + } + + @Test + public void testGetBytesReadRate() { + Long bytesReadRate = 1000000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate); + assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate()); + } + + @Test + public void testGetBytesWriteRate() { + Long bytesWriteRate = 1000000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate); + assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate()); + } + + @Test + public void testGetIopsReadRate() { + Long iopsReadRate = 1000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate); + assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate()); + } + + @Test + public void testGetIopsWriteRate() { + Long iopsWriteRate = 1000L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate); + assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate()); + } + + @Test + public void testCloneServiceOfferingWithGpuProfile() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "GPU-Offering-Clone"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", 10L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", 2); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", true); + + assertEquals(Long.valueOf(10L), cloneServiceOfferingCmd.getVgpuProfileId()); + assertEquals(Integer.valueOf(2), cloneServiceOfferingCmd.getGpuCount()); + assertTrue(cloneServiceOfferingCmd.getGpuDisplay()); + } + + @Test + public void testCloneServiceOfferingWithLease() { + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", 555L); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", "Lease-Offering-Clone"); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", 7200); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", "destroy"); + + assertEquals(Integer.valueOf(7200), cloneServiceOfferingCmd.getLeaseDuration()); + assertEquals(VMLeaseManager.ExpiryAction.DESTROY, cloneServiceOfferingCmd.getLeaseExpiryAction()); + } + + @Test + public void testExecuteWithOverriddenParameters() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "ClonedOffering-Override"; + String newDisplayText = "Overridden Display Text"; + Integer newCpu = 8; + Integer newMemory = 16384; + Integer newCpuSpeed = 3000; + Boolean newOfferHa = true; + Boolean newLimitCpuUse = true; + String newStorageType = "shared"; + String newTags = "premium,gpu"; + String newHostTag = "compute-optimized"; + Integer newNetworkRate = 2000; + String newDeploymentPlanner = "FirstFitPlanner"; + Boolean newPurgeResources = true; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "displayText", newDisplayText); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuSpeed", newCpuSpeed); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "offerHa", newOfferHa); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "limitCpuUse", newLimitCpuUse); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "storageType", newStorageType); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "tags", newTags); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "hostTag", newHostTag); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "networkRate", newNetworkRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "deploymentPlanner", newDeploymentPlanner); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "purgeResources", newPurgeResources); + + assertEquals(sourceOfferingId, cloneServiceOfferingCmd.getSourceOfferingId()); + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(newDisplayText, cloneServiceOfferingCmd.getDisplayText()); + assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(newMemory, cloneServiceOfferingCmd.getMemory()); + assertEquals(newCpuSpeed, cloneServiceOfferingCmd.getCpuSpeed()); + assertEquals(newOfferHa, cloneServiceOfferingCmd.isOfferHa()); + assertEquals(newLimitCpuUse, cloneServiceOfferingCmd.isLimitCpuUse()); + assertEquals(newStorageType, cloneServiceOfferingCmd.getStorageType()); + assertEquals(newTags, cloneServiceOfferingCmd.getTags()); + assertEquals(newHostTag, cloneServiceOfferingCmd.getHostTag()); + assertEquals(newNetworkRate, cloneServiceOfferingCmd.getNetworkRate()); + assertEquals(newDeploymentPlanner, cloneServiceOfferingCmd.getDeploymentPlanner()); + assertTrue(cloneServiceOfferingCmd.isPurgeResources()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithPartialOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "PartialOverride"; + Integer newCpu = 6; + Integer newMemory = 12288; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "cpuNumber", newCpu); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "memory", newMemory); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(newCpu, cloneServiceOfferingCmd.getCpuNumber()); + assertEquals(newMemory, cloneServiceOfferingCmd.getMemory()); + + assertNull(cloneServiceOfferingCmd.getCpuSpeed()); + assertFalse(cloneServiceOfferingCmd.isOfferHa()); + assertNull(cloneServiceOfferingCmd.getStorageType()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithGpuOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "GPU-Clone-Override"; + Long vgpuProfileId = 15L; + Integer gpuCount = 4; + Boolean gpuDisplay = false; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "vgpuProfileId", vgpuProfileId); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuCount", gpuCount); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "gpuDisplay", gpuDisplay); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(vgpuProfileId, cloneServiceOfferingCmd.getVgpuProfileId()); + assertEquals(gpuCount, cloneServiceOfferingCmd.getGpuCount()); + assertEquals(gpuDisplay, cloneServiceOfferingCmd.getGpuDisplay()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithLeaseOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "Lease-Clone-Override"; + Integer leaseDuration = 14400; // 4 hours + String leaseExpiryAction = "stop"; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseDuration", leaseDuration); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "leaseExpiryAction", leaseExpiryAction); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(leaseDuration, cloneServiceOfferingCmd.getLeaseDuration()); + assertEquals(VMLeaseManager.ExpiryAction.STOP, cloneServiceOfferingCmd.getLeaseExpiryAction()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithStorageOverrides() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + String newName = "Storage-Clone-Override"; + Long bytesReadRate = 2000000L; + Long bytesWriteRate = 1500000L; + Long iopsReadRate = 2000L; + Long iopsWriteRate = 1500L; + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesReadRate", bytesReadRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "bytesWriteRate", bytesWriteRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsReadRate", iopsReadRate); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "iopsWriteRate", iopsWriteRate); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + assertEquals(bytesReadRate, cloneServiceOfferingCmd.getBytesReadRate()); + assertEquals(bytesWriteRate, cloneServiceOfferingCmd.getBytesWriteRate()); + assertEquals(iopsReadRate, cloneServiceOfferingCmd.getIopsReadRate()); + assertEquals(iopsWriteRate, cloneServiceOfferingCmd.getIopsWriteRate()); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } + + @Test + public void testExecuteWithDetailsOverride() { + Long sourceOfferingId = 555L; + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "sourceOfferingId", sourceOfferingId); + + String newName = "Details-Clone-Override"; + Map> details = new HashMap<>(); + + HashMap cpuOvercommit = new HashMap<>(); + cpuOvercommit.put("key", "cpuOvercommitRatio"); + cpuOvercommit.put("value", "3.0"); + + HashMap memoryOvercommit = new HashMap<>(); + memoryOvercommit.put("key", "memoryOvercommitRatio"); + memoryOvercommit.put("value", "2.5"); + + HashMap customDetail = new HashMap<>(); + customDetail.put("key", "customParameter"); + customDetail.put("value", "customValue"); + + details.put("0", cpuOvercommit); + details.put("1", memoryOvercommit); + details.put("2", customDetail); + + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "serviceOfferingName", newName); + ReflectionTestUtils.setField(cloneServiceOfferingCmd, "details", details); + + assertEquals(newName, cloneServiceOfferingCmd.getServiceOfferingName()); + Map result = cloneServiceOfferingCmd.getDetails(); + assertNotNull(result); + assertEquals("3.0", result.get("cpuOvercommitRatio")); + assertEquals("2.5", result.get("memoryOvercommitRatio")); + assertEquals("customValue", result.get("customParameter")); + + when(configService.cloneServiceOffering(any(CloneServiceOfferingCmd.class))).thenReturn(mockServiceOffering); + when(responseGenerator.createServiceOfferingResponse(mockServiceOffering)).thenReturn(mockServiceOfferingResponse); + + cloneServiceOfferingCmd.execute(); + + assertNotNull(cloneServiceOfferingCmd.getResponseObject()); + assertEquals(mockServiceOfferingResponse, cloneServiceOfferingCmd.getResponseObject()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java index ad95ce10bd6c..8d2771c969b7 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/storage/DownloadImageStoreObjectCmdTest.java @@ -19,6 +19,7 @@ import com.cloud.utils.Pair; import org.apache.cloudstack.api.response.ExtractResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.browser.StorageBrowser; import org.junit.Assert; import org.junit.Test; @@ -30,6 +31,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.List; +import java.util.UUID; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -95,10 +97,13 @@ public void testGetEventType() { @Test public void testGetEventDescription() { + String uuid = UUID.randomUUID().toString(); + ReflectionTestUtils.setField(cmd, "storeId", 1L); ReflectionTestUtils.setField(cmd, "path", "path/to/object"); + CallContext.current().putApiResourceUuid("id", uuid); String eventDescription = cmd.getEventDescription(); - Assert.assertEquals("Downloading object at path path/to/object on image store 1", eventDescription); + Assert.assertEquals(String.format("Downloading object at path path/to/object on image store %s", uuid), eventDescription); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java index 8a57ac3eb22c..397723dd6069 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java @@ -69,7 +69,7 @@ public void testExecuteWithNotBlankPassword() { } catch (ServerApiException e) { Assert.assertTrue("Received exception as the mock accountService createUser returns null user", true); } - Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null, false); } @Test @@ -82,7 +82,7 @@ public void testExecuteWithNullPassword() { Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode()); Assert.assertEquals("Empty passwords are not allowed", e.getMessage()); } - Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, false); } @Test @@ -95,6 +95,6 @@ public void testExecuteWithEmptyPassword() { Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode()); Assert.assertEquals("Empty passwords are not allowed", e.getMessage()); } - Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, true); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java new file mode 100644 index 000000000000..f86e51adb5ab --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.cloudstack.api.command.admin.user; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateUserCmdTest { + @InjectMocks + private UpdateUserCmd cmd; + + @Test + public void testGetApiResourceId() { + Long userId = 99L; + cmd.setId(userId); + Assert.assertEquals(userId, cmd.getApiResourceId()); + } + + @Test + public void testGetApiResourceType() { + Assert.assertEquals(ApiCommandResourceType.User, cmd.getApiResourceType()); + } + + @Test + public void testIsPasswordChangeRequired_True() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.TRUE); + Assert.assertTrue(cmd.isPasswordChangeRequired()); + } + + @Test + public void testIsPasswordChangeRequired_False() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.FALSE); + Assert.assertFalse(cmd.isPasswordChangeRequired()); + } + + @Test + public void testIsPasswordChangeRequired_Null() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", null); + Assert.assertFalse(cmd.isPasswordChangeRequired()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java new file mode 100644 index 000000000000..27bc4614e1b5 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java @@ -0,0 +1,55 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.vm; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +public class AssignVMCmdTest { + + @Test + public void test_setSkipNetwork_default() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + Object value = ReflectionTestUtils.getField(assignVMCmd, "skipNetwork"); + Assert.assertTrue(value instanceof Boolean); + Assert.assertFalse((Boolean) value); + } + + @Test + public void test_setSkipNetwork_set() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + assignVMCmd.setSkipNetwork(true); + Object value = ReflectionTestUtils.getField(assignVMCmd, "skipNetwork"); + Assert.assertTrue(value instanceof Boolean); + Assert.assertTrue((Boolean) value); + } + + @Test + public void test_isSkipNetwork_default() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + Assert.assertFalse(assignVMCmd.isSkipNetwork()); + } + + @Test + public void test_isSkipNetwork_set() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + ReflectionTestUtils.setField(assignVMCmd, "skipNetwork", true); + Assert.assertTrue(assignVMCmd.isSkipNetwork()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java new file mode 100644 index 000000000000..d82dc9f766f9 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.vm; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class DeployVMCmdByAdminTest { + + @InjectMocks + private DeployVMCmdByAdmin cmd; + + @Test + public void testIsBlankInstance_default() { + assertFalse(cmd.isBlankInstance()); + } + + @Test + public void testIsBlankInstance_true() { + ReflectionTestUtils.setField(cmd, "blankInstance", true); + assertTrue(cmd.isBlankInstance()); + } + + @Test + public void testIsBlankInstance_false() { + ReflectionTestUtils.setField(cmd, "blankInstance", false); + assertFalse(cmd.isBlankInstance()); + } + + @Test + public void testSetBlankInstance_default() { + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNull(obj); + } + + @Test + public void testSetBlankInstance_true() { + cmd.setBlankInstance(true); + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNotNull(obj); + assertTrue((boolean)obj); + } + + @Test + public void testSetBlankInstance_false() { + cmd.setBlankInstance(false); + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNotNull(obj); + assertFalse((boolean)obj); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java index ba7e351a8a8e..59a61806e867 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/volume/UnmanageVolumeCmdTest.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.storage.volume.VolumeImportUnmanageService; import org.junit.Assert; import org.junit.Test; @@ -31,6 +32,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + @RunWith(MockitoJUnitRunner.class) public class UnmanageVolumeCmdTest { @@ -41,6 +44,7 @@ public class UnmanageVolumeCmdTest { public void testUnmanageVolumeCmd() { long accountId = 2L; Long volumeId = 3L; + String volumeUuid = UUID.randomUUID().toString(); Volume volume = Mockito.mock(Volume.class); Mockito.when(responseGenerator.findVolumeById(volumeId)).thenReturn(volume); @@ -51,12 +55,14 @@ public void testUnmanageVolumeCmd() { ReflectionTestUtils.setField(cmd,"volumeImportService", volumeImportService); ReflectionTestUtils.setField(cmd,"_responseGenerator", responseGenerator); + CallContext.current().putApiResourceUuid("id", volumeUuid); + Assert.assertEquals(volumeId, cmd.getVolumeId()); Assert.assertEquals(accountId, cmd.getEntityOwnerId()); Assert.assertEquals(volumeId, cmd.getApiResourceId()); Assert.assertEquals(ApiCommandResourceType.Volume, cmd.getApiResourceType()); Assert.assertEquals(EventTypes.EVENT_VOLUME_UNMANAGE, cmd.getEventType()); - Assert.assertEquals("Unmanaging Volume with ID " + volumeId, cmd.getEventDescription()); + Assert.assertEquals("Unmanaging Volume with ID: " + volumeUuid, cmd.getEventDescription()); Mockito.when(volumeImportService.unmanageVolume(volumeId)).thenReturn(true); try { diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java new file mode 100644 index 000000000000..1e6d6c9e0969 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vpc/CloneVpcOfferingCmdTest.java @@ -0,0 +1,299 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.command.admin.vpc; + +import com.cloud.network.vpc.VpcOffering; +import com.cloud.network.vpc.VpcProvisioningService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.VpcOfferingResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloneVpcOfferingCmdTest { + + private CloneVPCOfferingCmd cloneVpcOfferingCmd; + + @Mock + private VpcProvisioningService vpcService; + + @Mock + private ResponseGenerator responseGenerator; + + @Mock + private VpcOffering mockVpcOffering; + + @Mock + private VpcOfferingResponse mockVpcOfferingResponse; + + @Before + public void setUp() { + cloneVpcOfferingCmd = new CloneVPCOfferingCmd(); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_vpcProvSvc", vpcService); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "_responseGenerator", responseGenerator); + } + + @Test + public void testGetSourceOfferingId() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + assertEquals(sourceOfferingId, cloneVpcOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testGetName() { + String name = "ClonedVpcOffering"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name); + assertEquals(name, cloneVpcOfferingCmd.getVpcOfferingName()); + } + + @Test + public void testGetDisplayText() { + String displayText = "Cloned VPC Offering Display Text"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", displayText); + assertEquals(displayText, cloneVpcOfferingCmd.getDisplayText()); + } + + @Test + public void testGetDisplayTextDefaultsToName() { + String name = "ClonedVpcOffering"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", name); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", null); + assertEquals(name, cloneVpcOfferingCmd.getDisplayText()); + } + + @Test + public void testGetServiceOfferingId() { + Long serviceOfferingId = 456L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", serviceOfferingId); + assertEquals(serviceOfferingId, cloneVpcOfferingCmd.getServiceOfferingId()); + } + + @Test + public void testGetInternetProtocol() { + String internetProtocol = "dualstack"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", internetProtocol); + assertEquals(internetProtocol, cloneVpcOfferingCmd.getInternetProtocol()); + } + + @Test + public void testGetProvider() { + String provider = "NSX"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", provider); + assertEquals(provider, cloneVpcOfferingCmd.getProvider()); + } + + @Test + public void testGetNetworkMode() { + String networkMode = "ROUTED"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", networkMode); + assertEquals(networkMode, cloneVpcOfferingCmd.getNetworkMode()); + } + + @Test + public void testGetRoutingMode() { + String routingMode = "dynamic"; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", routingMode); + assertEquals(routingMode, cloneVpcOfferingCmd.getRoutingMode()); + } + + @Test + public void testGetNsxSupportLb() { + Boolean nsxSupportLb = true; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", nsxSupportLb); + assertEquals(nsxSupportLb, cloneVpcOfferingCmd.getNsxSupportsLbService()); + } + + @Test + public void testGetSpecifyAsnumber() { + Boolean specifyAsnumber = false; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", specifyAsnumber); + assertEquals(specifyAsnumber, cloneVpcOfferingCmd.getSpecifyAsNumber()); + } + + @Test + public void testExecuteSuccess() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(mockVpcOffering); + when(responseGenerator.createVpcOfferingResponse(mockVpcOffering)).thenReturn(mockVpcOfferingResponse); + + cloneVpcOfferingCmd.execute(); + + assertNotNull(cloneVpcOfferingCmd.getResponseObject()); + assertEquals(mockVpcOfferingResponse, cloneVpcOfferingCmd.getResponseObject()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() { + Long sourceOfferingId = 789L; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", sourceOfferingId); + + when(vpcService.cloneVPCOffering(any(CloneVPCOfferingCmd.class))).thenReturn(null); + + try { + cloneVpcOfferingCmd.execute(); + fail("Expected ServerApiException to be thrown"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to clone VPC offering", e.getMessage()); + throw e; + } + } + + @Test + public void testGetSupportedServices() { + List supportedServices = Arrays.asList("Dhcp", "Dns", "SourceNat", "NetworkACL"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "supportedServices", supportedServices); + assertEquals(supportedServices, cloneVpcOfferingCmd.getSupportedServices()); + } + + @Test + public void testGetServiceProviders() { + Map> serviceProviderList = new HashMap<>(); + + HashMap dhcpProvider = new HashMap<>(); + dhcpProvider.put("service", "Dhcp"); + dhcpProvider.put("provider", "VpcVirtualRouter"); + + HashMap dnsProvider = new HashMap<>(); + dnsProvider.put("service", "Dns"); + dnsProvider.put("provider", "VpcVirtualRouter"); + + HashMap aclProvider = new HashMap<>(); + aclProvider.put("service", "NetworkACL"); + aclProvider.put("provider", "VpcVirtualRouter"); + + serviceProviderList.put("0", dhcpProvider); + serviceProviderList.put("1", dnsProvider); + serviceProviderList.put("2", aclProvider); + + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceProviderList", serviceProviderList); + + Map> result = cloneVpcOfferingCmd.getServiceProviders(); + assertNotNull(result); + assertEquals(3, result.size()); + assertNotNull(result.get("Dhcp")); + assertNotNull(result.get("Dns")); + assertNotNull(result.get("NetworkACL")); + assertEquals("VpcVirtualRouter", result.get("Dhcp").get(0)); + assertEquals("VpcVirtualRouter", result.get("Dns").get(0)); + assertEquals("VpcVirtualRouter", result.get("NetworkACL").get(0)); + } + + @Test + public void testGetEnable() { + Boolean enable = true; + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", enable); + assertEquals(enable, cloneVpcOfferingCmd.getEnable()); + } + + @Test + public void testCloneWithAllParameters() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "vpcOfferingName", "ClonedVpcOffering"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "displayText", "Cloned VPC Offering"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceOfferingId", 456L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "internetProtocol", "ipv4"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "static"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", false); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "enable", true); + + assertEquals(Long.valueOf(789L), cloneVpcOfferingCmd.getSourceOfferingId()); + assertEquals("ClonedVpcOffering", cloneVpcOfferingCmd.getVpcOfferingName()); + assertEquals("Cloned VPC Offering", cloneVpcOfferingCmd.getDisplayText()); + assertEquals(Long.valueOf(456L), cloneVpcOfferingCmd.getServiceOfferingId()); + assertEquals("ipv4", cloneVpcOfferingCmd.getInternetProtocol()); + assertEquals("NSX", cloneVpcOfferingCmd.getProvider()); + assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode()); + assertEquals("static", cloneVpcOfferingCmd.getRoutingMode()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService()); + assertEquals(Boolean.FALSE, cloneVpcOfferingCmd.getSpecifyAsNumber()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getEnable()); + } + + @Test + public void testSourceOfferingIdNullByDefault() { + assertNull(cloneVpcOfferingCmd.getSourceOfferingId()); + } + + @Test + public void testProviderNullByDefault() { + assertNull(cloneVpcOfferingCmd.getProvider()); + } + + @Test + public void testServiceCapabilityList() { + Map> serviceCapabilityList = new HashMap<>(); + serviceCapabilityList.put("Connectivity", Arrays.asList("RegionLevelVpc:true", "DistributedRouter:true")); + serviceCapabilityList.put("SourceNat", Arrays.asList("RedundantRouter:true")); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "serviceCapabilityList", serviceCapabilityList); + + Map> result = cloneVpcOfferingCmd.getServiceCapabilityList(); + assertNotNull(result); + assertEquals(serviceCapabilityList, result); + } + + @Test + public void testCloneVpcOfferingWithNsxProvider() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "NSX"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "nsxSupportsLbService", true); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "ROUTED"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "routingMode", "dynamic"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "specifyAsNumber", true); + + assertEquals("NSX", cloneVpcOfferingCmd.getProvider()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getNsxSupportsLbService()); + assertEquals("ROUTED", cloneVpcOfferingCmd.getNetworkMode()); + assertEquals("dynamic", cloneVpcOfferingCmd.getRoutingMode()); + assertEquals(Boolean.TRUE, cloneVpcOfferingCmd.getSpecifyAsNumber()); + } + + @Test + public void testCloneVpcOfferingWithNetrisProvider() { + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "sourceOfferingId", 789L); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "provider", "Netris"); + ReflectionTestUtils.setField(cloneVpcOfferingCmd, "networkMode", "NATTED"); + + assertEquals("Netris", cloneVpcOfferingCmd.getProvider()); + assertEquals("NATTED", cloneVpcOfferingCmd.getNetworkMode()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java index f100822b8c77..cd0390aa2689 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/AddAccountToProjectCmdTest.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.test; +import com.cloud.exception.ResourceAllocationException; import junit.framework.Assert; import junit.framework.TestCase; @@ -149,6 +150,8 @@ public void testExecuteForNullAccountNameEmail() { addAccountToProjectCmd.execute(); } catch (InvalidParameterValueException exception) { Assert.assertEquals("Either accountName or email is required", exception.getLocalizedMessage()); + } catch (ResourceAllocationException exception) { + Assert.fail(); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java index 9ea3a6446e8e..71c6d54c7a8c 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java @@ -59,7 +59,7 @@ public void testCreateSuccess() throws ResourceAllocationException, ResourceUnav NicSecondaryIp secIp = Mockito.mock(NicSecondaryIp.class); Mockito.when( - networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any())) + networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any(), ArgumentMatchers.anyString())) .thenReturn(secIp); ipTonicCmd._networkService = networkService; @@ -79,7 +79,7 @@ public void testCreateFailure() throws ResourceAllocationException, ResourceUnav AddIpToVmNicCmd ipTonicCmd = Mockito.mock(AddIpToVmNicCmd.class); Mockito.when( - networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any())) + networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any(), ArgumentMatchers.anyString())) .thenReturn(null); ipTonicCmd._networkService = networkService; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java index 8188a2e0bb05..032dca8d8007 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/CreateSnapshotCmdTest.java @@ -25,11 +25,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotCmd; import org.apache.cloudstack.api.response.SnapshotResponse; +import org.apache.cloudstack.context.CallContext; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -73,11 +75,6 @@ public Long getVolumeId(){ public long getEntityOwnerId(){ return 1L; } - - @Override - protected String getVolumeUuid() { - return "123"; - } }; } @@ -121,6 +118,9 @@ public void testCreateFailure() { AccountService accountService = Mockito.mock(AccountService.class); Account account = Mockito.mock(Account.class); Mockito.when(accountService.getAccount(anyLong())).thenReturn(account); + String volumeUuid = UUID.randomUUID().toString(); + + CallContext.current().putApiResourceUuid("volumeid", volumeUuid); VolumeApiService volumeApiService = Mockito.mock(VolumeApiService.class); @@ -137,7 +137,7 @@ public void testCreateFailure() { try { createSnapshotCmd.execute(); } catch (ServerApiException exception) { - Assert.assertEquals("Failed to create Snapshot due to an internal error creating Snapshot for volume 123", exception.getDescription()); + Assert.assertEquals("Failed to create Snapshot due to an internal error creating Snapshot for volume " + volumeUuid, exception.getDescription()); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateAutoScaleVmProfileCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateAutoScaleVmProfileCmdTest.java index da6ed31eaa2a..3409ce053a9f 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateAutoScaleVmProfileCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateAutoScaleVmProfileCmdTest.java @@ -100,7 +100,7 @@ public void verifyUpdateAutoScaleVmProfileCmd() { Assert.assertEquals("updateautoscalevmprofileresponse", updateAutoScaleVmProfileCmd.getCommandName()); Assert.assertEquals(EventTypes.EVENT_AUTOSCALEVMPROFILE_UPDATE, updateAutoScaleVmProfileCmd.getEventType()); - Assert.assertEquals("Updating AutoScale Instance Profile. Instance Profile Id: " + profileId, updateAutoScaleVmProfileCmd.getEventDescription()); + Assert.assertEquals("Updating AutoScale Instance Profile with ID: " + profileId, updateAutoScaleVmProfileCmd.getEventDescription()); } @Test diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java index 7d6f8dc35b7e..c78dbe9b56bc 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/UpdateConditionCmdTest.java @@ -31,12 +31,15 @@ import org.apache.cloudstack.api.command.user.autoscale.UpdateConditionCmd; import org.apache.cloudstack.api.response.ConditionResponse; +import org.apache.cloudstack.context.CallContext; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + import static org.mockito.Mockito.when; public class UpdateConditionCmdTest { @@ -53,6 +56,7 @@ public class UpdateConditionCmdTest { private static final Long threshold = 100L; private static final long accountId = 5L; + private static final String conditionUuid = UUID.randomUUID().toString(); @Before public void setUp() { @@ -71,6 +75,8 @@ public void setUp() { ReflectionTestUtils.setField(updateConditionCmd,"relationalOperator", relationalOperator); ReflectionTestUtils.setField(updateConditionCmd,"threshold", threshold); + CallContext.current().putApiResourceUuid("id", conditionUuid); + condition = Mockito.mock(Condition.class); } @@ -83,7 +89,7 @@ public void verifyUpdateConditionCmd() { Assert.assertEquals(ApiCommandResourceType.Condition, updateConditionCmd.getApiResourceType()); Assert.assertEquals("updateconditionresponse", updateConditionCmd.getCommandName()); Assert.assertEquals(EventTypes.EVENT_CONDITION_UPDATE, updateConditionCmd.getEventType()); - Assert.assertEquals("Updating a condition.", updateConditionCmd.getEventDescription()); + Assert.assertEquals("Updating Instance AutoScale condition with ID: " + conditionUuid, updateConditionCmd.getEventDescription()); when(entityMgr.findById(Condition.class, conditionId)).thenReturn(condition); when(condition.getAccountId()).thenReturn(accountId); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java new file mode 100644 index 000000000000..2824a8dacb94 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java @@ -0,0 +1,134 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; +import org.junit.Test; + +public class AddDnsServerCmdTest extends BaseDnsCmdTest { + + private AddDnsServerCmd createCmd() throws Exception { + AddDnsServerCmd cmd = new AddDnsServerCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "name", "test-dns"); + setField(cmd, "url", "http://dns.example.com"); + setField(cmd, "provider", "PowerDNS"); + setField(cmd, "dnsApiKey", "api-key-123"); + setField(cmd, "port", 8081); + setField(cmd, "isPublic", true); + setField(cmd, "publicDomainSuffix", "public.example.com"); + setField(cmd, "nameServers", Arrays.asList("ns1.example.com", "ns2.example.com")); + setField(cmd, "dnsUserName", "admin@example.com"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + AddDnsServerCmd cmd = createCmd(); + + assertEquals("test-dns", cmd.getName()); + assertEquals("http://dns.example.com", cmd.getUrl()); + assertEquals("api-key-123", cmd.getDnsApiKey()); + assertEquals(Integer.valueOf(8081), cmd.getPort()); + assertTrue(cmd.isPublic()); + assertEquals("public.example.com", cmd.getPublicDomainSuffix()); + assertEquals(Arrays.asList("ns1.example.com", "ns2.example.com"), cmd.getNameServers()); + assertEquals(DnsProviderType.PowerDNS, cmd.getProvider()); + assertEquals("admin@example.com", cmd.getDnsUserName()); + } + + @Test + public void testIsPublicFalse() throws Exception { + AddDnsServerCmd cmd = createCmd(); + setField(cmd, "isPublic", false); + assertFalse(cmd.isPublic()); + } + + @Test + public void testIsPublicNull() throws Exception { + AddDnsServerCmd cmd = createCmd(); + setField(cmd, "isPublic", null); + assertFalse(cmd.isPublic()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + AddDnsServerCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetProviderDefault() throws Exception { + AddDnsServerCmd cmd = createCmd(); + setField(cmd, "provider", null); + assertEquals(DnsProviderType.PowerDNS, cmd.getProvider()); + } + + @Test + public void testGetProviderCaseInsensitive() throws Exception { + AddDnsServerCmd cmd = createCmd(); + setField(cmd, "provider", "powerdns"); + assertEquals(DnsProviderType.PowerDNS, cmd.getProvider()); + } + + @Test + public void testExecuteSuccess() throws Exception { + AddDnsServerCmd cmd = createCmd(); + + DnsServer mockServer = mock(DnsServer.class); + DnsServerResponse mockResponse = new DnsServerResponse(); + mockResponse.setName("test-dns"); + + when(dnsProviderManager.addDnsServer(cmd)).thenReturn(mockServer); + when(dnsProviderManager.createDnsServerResponse(mockServer)).thenReturn(mockResponse); + + cmd.execute(); + + DnsServerResponse response = (DnsServerResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("adddnsserverresponse", response.getResponseName()); + verify(dnsProviderManager).addDnsServer(cmd); + verify(dnsProviderManager).createDnsServerResponse(mockServer); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + AddDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.addDnsServer(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + AddDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.addDnsServer(cmd)).thenThrow(new RuntimeException("Connection refused")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java new file mode 100644 index 000000000000..593a0a041cea --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.junit.Test; + +import com.cloud.network.Network; +import com.cloud.user.Account; + +public class AssociateDnsZoneToNetworkCmdTest extends BaseDnsCmdTest { + + private static final long NETWORK_ID = 200L; + + private AssociateDnsZoneToNetworkCmd createCmd() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = new AssociateDnsZoneToNetworkCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "dnsZoneId", ENTITY_ID); + setField(cmd, "networkId", NETWORK_ID); + setField(cmd, "subDomain", "dev"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId()); + assertEquals(Long.valueOf(NETWORK_ID), cmd.getNetworkId()); + assertEquals("dev", cmd.getSubDomain()); + } + + @Test + public void testGetEntityOwnerIdWithNetwork() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = createCmd(); + Network mockNetwork = mock(Network.class); + when(mockNetwork.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(Network.class, NETWORK_ID)) + .thenReturn(mockNetwork); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdNetworkNotFound() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = createCmd(); + when(entityManager.findById(Network.class, NETWORK_ID)) + .thenReturn(null); + + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = createCmd(); + DnsZoneNetworkMapResponse mockResponse = + new DnsZoneNetworkMapResponse(); + when(dnsProviderManager.associateZoneToNetwork(cmd)) + .thenReturn(mockResponse); + + cmd.execute(); + + DnsZoneNetworkMapResponse response = + (DnsZoneNetworkMapResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("associatednszonetonetworkresponse", + response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + AssociateDnsZoneToNetworkCmd cmd = createCmd(); + when(dnsProviderManager.associateZoneToNetwork(cmd)) + .thenThrow(new RuntimeException("Error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java new file mode 100644 index 000000000000..e8b611ca17ca --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; + +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.junit.After; +import org.junit.Before; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.cloud.user.Account; +import com.cloud.utils.db.EntityManager; + +/** + * Shared setup for all DNS command unit tests. + */ +public abstract class BaseDnsCmdTest { + + protected static final long ACCOUNT_ID = 42L; + protected static final long ENTITY_ID = 100L; + + protected DnsProviderManager dnsProviderManager; + protected EntityManager entityManager; + protected Account callingAccount; + + private MockedStatic callContextMock; + + @Before + public void setUp() { + dnsProviderManager = mock(DnsProviderManager.class); + entityManager = mock(EntityManager.class); + + callingAccount = mock(Account.class); + when(callingAccount.getId()).thenReturn(ACCOUNT_ID); + + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(callingAccount); + + callContextMock = Mockito.mockStatic(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + } + + @After + public void tearDown() { + callContextMock.close(); + } + + /** + * Sets a private/inherited field value via reflection. + */ + protected void setField(Object target, String fieldName, Object value) throws Exception { + Field field = null; + Class clazz = target.getClass(); + while (clazz != null) { + try { + field = clazz.getDeclaredField(fieldName); + break; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + if (field == null) { + throw new NoSuchFieldException(fieldName + " not found in hierarchy of " + target.getClass().getName()); + } + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java new file mode 100644 index 000000000000..c37930dd2289 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java @@ -0,0 +1,117 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.dns.DnsRecord; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; + +public class CreateDnsRecordCmdTest extends BaseDnsCmdTest { + + private CreateDnsRecordCmd createCmd() throws Exception { + CreateDnsRecordCmd cmd = new CreateDnsRecordCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "dnsZoneId", ENTITY_ID); + setField(cmd, "name", "www"); + setField(cmd, "type", "A"); + setField(cmd, "contents", Arrays.asList("192.168.1.1")); + setField(cmd, "ttl", 7200); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + + assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId()); + assertEquals("www", cmd.getName()); + assertEquals(DnsRecord.RecordType.A, cmd.getType()); + assertEquals(Arrays.asList("192.168.1.1"), cmd.getContents()); + assertEquals(Integer.valueOf(7200), cmd.getTtl()); + } + + @Test + public void testGetTtlDefault() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + setField(cmd, "ttl", null); + assertEquals(Integer.valueOf(3600), cmd.getTtl()); + } + + @Test + public void testGetTypeCname() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + setField(cmd, "type", "CNAME"); + assertEquals(DnsRecord.RecordType.CNAME, cmd.getType()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetTypeInvalid() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + setField(cmd, "type", "INVALID"); + cmd.getType(); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testEventType() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_DNS_RECORD_CREATE, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + assertEquals("Creating DNS Record: www", cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + + DnsRecordResponse mockResponse = new DnsRecordResponse(); + mockResponse.setName("www"); + when(dnsProviderManager.createDnsRecord(cmd)).thenReturn(mockResponse); + + cmd.execute(); + + DnsRecordResponse response = (DnsRecordResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("creatednsrecordresponse", response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + CreateDnsRecordCmd cmd = createCmd(); + when(dnsProviderManager.createDnsRecord(cmd)).thenThrow(new RuntimeException("Provider error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.java new file mode 100644 index 000000000000..dc37874e6599 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.java @@ -0,0 +1,157 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.dns.DnsZone; +import org.junit.Test; + +import com.cloud.event.EventTypes; + +public class CreateDnsZoneCmdTest extends BaseDnsCmdTest { + + private CreateDnsZoneCmd createCmd() throws Exception { + CreateDnsZoneCmd cmd = new CreateDnsZoneCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "name", "example.com"); + setField(cmd, "dnsServerId", ENTITY_ID); + setField(cmd, "type", "Public"); + setField(cmd, "description", "Test zone"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + + assertEquals("example.com", cmd.getName()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsServerId()); + assertEquals(DnsZone.ZoneType.Public, cmd.getType()); + assertEquals("Test zone", cmd.getDescription()); + } + + @Test + public void testGetTypePrivate() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + setField(cmd, "type", "Private"); + assertEquals(DnsZone.ZoneType.Private, cmd.getType()); + } + + @Test + public void testGetTypeDefaultsToPublicWhenNull() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + setField(cmd, "type", null); + assertEquals(DnsZone.ZoneType.Public, cmd.getType()); + } + + @Test + public void testGetTypeDefaultsToPublicWhenBlank() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + setField(cmd, "type", ""); + assertEquals(DnsZone.ZoneType.Public, cmd.getType()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testEventType() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_DNS_ZONE_CREATE, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + assertEquals("creating DNS zone: example.com", cmd.getEventDescription()); + } + + @Test + public void testCreateSuccess() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + + DnsZone mockZone = mock(DnsZone.class); + when(mockZone.getId()).thenReturn(ENTITY_ID); + when(mockZone.getUuid()).thenReturn("uuid-123"); + when(dnsProviderManager.allocateDnsZone(cmd)).thenReturn(mockZone); + + cmd.create(); + + assertEquals(Long.valueOf(ENTITY_ID), cmd.getEntityId()); + assertEquals("uuid-123", cmd.getEntityUuid()); + } + + @Test(expected = ServerApiException.class) + public void testCreateReturnsNull() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.allocateDnsZone(cmd)).thenReturn(null); + cmd.create(); + } + + @Test(expected = ServerApiException.class) + public void testCreateThrowsException() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.allocateDnsZone(cmd)).thenThrow(new RuntimeException("DB error")); + cmd.create(); + } + + @Test + public void testExecuteSuccess() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + cmd.setEntityId(ENTITY_ID); + + DnsZone mockZone = mock(DnsZone.class); + DnsZoneResponse mockResponse = new DnsZoneResponse(); + mockResponse.setName("example.com"); + + when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenReturn(mockZone); + when(dnsProviderManager.createDnsZoneResponse(mockZone)).thenReturn(mockResponse); + + cmd.execute(); + + DnsZoneResponse response = (DnsZoneResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("creatednszoneresponse", response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + cmd.setEntityId(ENTITY_ID); + + when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + CreateDnsZoneCmd cmd = createCmd(); + cmd.setEntityId(ENTITY_ID); + + when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenThrow(new RuntimeException("Provider error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java new file mode 100644 index 000000000000..46aacf5a68ce --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java @@ -0,0 +1,102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsRecord; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; + +public class DeleteDnsRecordCmdTest extends BaseDnsCmdTest { + + private DeleteDnsRecordCmd createCmd() throws Exception { + DeleteDnsRecordCmd cmd = new DeleteDnsRecordCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "dnsZoneId", ENTITY_ID); + setField(cmd, "name", "www"); + setField(cmd, "type", "A"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + + assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId()); + assertEquals("www", cmd.getName()); + assertEquals(DnsRecord.RecordType.A, cmd.getType()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetTypeInvalid() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + setField(cmd, "type", "BOGUS"); + cmd.getType(); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testEventType() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_DNS_RECORD_DELETE, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + assertEquals("Deleting DNS Record: www", cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsRecord(cmd)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + verify(dnsProviderManager).deleteDnsRecord(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsRecord(cmd)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + DeleteDnsRecordCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsRecord(cmd)).thenThrow(new RuntimeException("Error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java new file mode 100644 index 000000000000..adebd339d710 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java @@ -0,0 +1,119 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsServer; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +public class DeleteDnsServerCmdTest extends BaseDnsCmdTest { + + private DeleteDnsServerCmd createCmd() throws Exception { + DeleteDnsServerCmd cmd = new DeleteDnsServerCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testGetId() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetCleanupDefault() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + assertTrue(cmd.getCleanup()); + } + + @Test + public void testGetCleanupFalse() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + setField(cmd, "cleanup", false); + assertFalse(cmd.getCleanup()); + } + + @Test + public void testGetEntityOwnerIdWithServer() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + DnsServer mockServer = mock(DnsServer.class); + when(mockServer.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(mockServer); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdServerNotFound() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(null); + + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testEventType() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_DNS_SERVER_DELETE, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + assertEquals("Deleting DNS server ID: " + ENTITY_ID, cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsServer(cmd)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + verify(dnsProviderManager).deleteDnsServer(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsServer(cmd)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + DeleteDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsServer(cmd)).thenThrow(new RuntimeException("Error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java new file mode 100644 index 000000000000..350cea76e5c9 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java @@ -0,0 +1,104 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsZone; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +public class DeleteDnsZoneCmdTest extends BaseDnsCmdTest { + + private DeleteDnsZoneCmd createCmd() throws Exception { + DeleteDnsZoneCmd cmd = new DeleteDnsZoneCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testGetId() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerIdWithZone() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + DnsZone mockZone = mock(DnsZone.class); + when(mockZone.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(mockZone); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdZoneNotFound() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(null); + + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testEventType() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_DNS_ZONE_DELETE, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + assertEquals("Deleting DNS Zone ID: " + ENTITY_ID, cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + verify(dnsProviderManager).deleteDnsZone(ENTITY_ID, false); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + DeleteDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenThrow(new RuntimeException("Error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java new file mode 100644 index 000000000000..34ef07efce3b --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java @@ -0,0 +1,100 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.junit.Test; + +import com.cloud.network.Network; +import com.cloud.user.Account; + +public class DisassociateDnsZoneFromNetworkCmdTest extends BaseDnsCmdTest { + + private static final long NETWORK_ID = 200L; + + private DisassociateDnsZoneFromNetworkCmd createCmd() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = + new DisassociateDnsZoneFromNetworkCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "networkId", NETWORK_ID); + return cmd; + } + + @Test + public void testGetNetworkId() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + assertEquals(Long.valueOf(NETWORK_ID), cmd.getNetworkId()); + } + + @Test + public void testGetEntityOwnerIdWithNetwork() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + Network mockNetwork = mock(Network.class); + when(mockNetwork.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(Network.class, NETWORK_ID)) + .thenReturn(mockNetwork); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdNetworkNotFound() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + when(entityManager.findById(Network.class, NETWORK_ID)) + .thenReturn(null); + + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + when(dnsProviderManager.disassociateZoneFromNetwork(cmd)) + .thenReturn(true); + + cmd.execute(); + + SuccessResponse response = + (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + verify(dnsProviderManager).disassociateZoneFromNetwork(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + when(dnsProviderManager.disassociateZoneFromNetwork(cmd)) + .thenReturn(false); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + DisassociateDnsZoneFromNetworkCmd cmd = createCmd(); + when(dnsProviderManager.disassociateZoneFromNetwork(cmd)) + .thenThrow(new RuntimeException("Error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java new file mode 100644 index 000000000000..62340742c878 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.cloudstack.api.response.DnsProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListDnsProvidersCmdTest extends BaseDnsCmdTest { + + private ListDnsProvidersCmd createCmd() throws Exception { + ListDnsProvidersCmd cmd = new ListDnsProvidersCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + return cmd; + } + + @Test + public void testExecute() throws Exception { + ListDnsProvidersCmd cmd = createCmd(); + when(dnsProviderManager.listProviderNames()).thenReturn(Arrays.asList("PowerDNS")); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = + (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listdnsprovidersresponse", response.getResponseName()); + assertNotNull(response.getResponses()); + assertEquals(1, response.getResponses().size()); + } + + @Test + public void testExecuteMultipleProviders() throws Exception { + ListDnsProvidersCmd cmd = createCmd(); + when(dnsProviderManager.listProviderNames()) + .thenReturn(Arrays.asList("PowerDNS", "Cloudflare")); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = + (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals(2, response.getResponses().size()); + } + + @Test + public void testExecuteEmptyList() throws Exception { + ListDnsProvidersCmd cmd = createCmd(); + when(dnsProviderManager.listProviderNames()) + .thenReturn(Collections.emptyList()); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = + (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals(0, response.getResponses().size()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java new file mode 100644 index 000000000000..e75713b55d57 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java @@ -0,0 +1,56 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListDnsRecordsCmdTest extends BaseDnsCmdTest { + + private ListDnsRecordsCmd createCmd() throws Exception { + ListDnsRecordsCmd cmd = new ListDnsRecordsCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "dnsZoneId", ENTITY_ID); + return cmd; + } + + @Test + public void testGetDnsZoneId() throws Exception { + ListDnsRecordsCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId()); + } + + @Test + public void testExecute() throws Exception { + ListDnsRecordsCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(dnsProviderManager.listDnsRecords(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listdnsrecordsresponse", response.getResponseName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.java new file mode 100644 index 000000000000..710aa497167f --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.java @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderType; +import org.junit.Test; + +public class ListDnsServersCmdTest extends BaseDnsCmdTest { + + private ListDnsServersCmd createCmd() throws Exception { + ListDnsServersCmd cmd = new ListDnsServersCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "providerType", "PowerDNS"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + ListDnsServersCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals(DnsProviderType.PowerDNS, cmd.getProviderType()); + } + + @Test + public void testGetProviderTypeNull() throws Exception { + ListDnsServersCmd cmd = createCmd(); + setField(cmd, "providerType", null); + assertEquals(DnsProviderType.PowerDNS, cmd.getProviderType()); + } + + @Test + public void testExecute() throws Exception { + ListDnsServersCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(dnsProviderManager.listDnsServers(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listdnsserversresponse", response.getResponseName()); + assertEquals("dnsserver", response.getObjectName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java new file mode 100644 index 000000000000..7d47266ae6e6 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListDnsZonesCmdTest extends BaseDnsCmdTest { + + private ListDnsZonesCmd createCmd() throws Exception { + ListDnsZonesCmd cmd = new ListDnsZonesCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "dnsServerId", 200L); + setField(cmd, "name", "example.com"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + ListDnsZonesCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals(Long.valueOf(200L), cmd.getDnsServerId()); + assertEquals("example.com", cmd.getName()); + } + + @Test + public void testExecute() throws Exception { + ListDnsZonesCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(dnsProviderManager.listDnsZones(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listdnszonesresponse", response.getResponseName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java new file mode 100644 index 000000000000..f3c0fcd4fe07 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java @@ -0,0 +1,145 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.dns.DnsServer; +import org.junit.Test; + +import com.cloud.user.Account; + +public class UpdateDnsServerCmdTest extends BaseDnsCmdTest { + + private UpdateDnsServerCmd createCmd() throws Exception { + UpdateDnsServerCmd cmd = new UpdateDnsServerCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "name", "updated-dns"); + setField(cmd, "url", "http://updated.dns.com"); + setField(cmd, "dnsApiKey", "new-api-key"); + setField(cmd, "port", 9090); + setField(cmd, "isPublic", true); + setField(cmd, "publicDomainSuffix", "updated.example.com"); + setField(cmd, "nameServers", Arrays.asList("ns1.updated.com", "ns2.updated.com")); + setField(cmd, "state", "Enabled"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals("updated-dns", cmd.getName()); + assertEquals("http://updated.dns.com", cmd.getUrl()); + assertEquals("new-api-key", cmd.getDnsApiKey()); + assertEquals(Integer.valueOf(9090), cmd.getPort()); + assertTrue(cmd.isPublic()); + assertEquals("updated.example.com", cmd.getPublicDomainSuffix()); + assertEquals("ns1.updated.com,ns2.updated.com", cmd.getNameServers()); + assertEquals(DnsServer.State.Enabled, cmd.getState()); + } + + @Test + public void testGetStateDisabled() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + setField(cmd, "state", "Disabled"); + assertEquals(DnsServer.State.Disabled, cmd.getState()); + } + + @Test + public void testGetStateNull() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + setField(cmd, "state", null); + assertNull(cmd.getState()); + } + + @Test + public void testGetStateBlank() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + setField(cmd, "state", ""); + assertNull(cmd.getState()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetStateInvalid() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + setField(cmd, "state", "InvalidState"); + cmd.getState(); + } + + @Test + public void testGetEntityOwnerIdWithServer() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + DnsServer mockServer = mock(DnsServer.class); + when(mockServer.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(mockServer); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdServerNotFound() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(null); + + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + + DnsServer mockServer = mock(DnsServer.class); + DnsServerResponse mockResponse = new DnsServerResponse(); + mockResponse.setName("updated-dns"); + + when(dnsProviderManager.updateDnsServer(cmd)).thenReturn(mockServer); + when(dnsProviderManager.createDnsServerResponse(mockServer)).thenReturn(mockResponse); + + cmd.execute(); + + DnsServerResponse response = (DnsServerResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("updatednsserverresponse", response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.updateDnsServer(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + UpdateDnsServerCmd cmd = createCmd(); + when(dnsProviderManager.updateDnsServer(cmd)).thenThrow(new RuntimeException("Update failed")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.java new file mode 100644 index 000000000000..cac16903185a --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.java @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.dns.DnsZone; +import org.junit.Test; + +import com.cloud.user.Account; + +public class UpdateDnsZoneCmdTest extends BaseDnsCmdTest { + + private UpdateDnsZoneCmd createCmd() throws Exception { + UpdateDnsZoneCmd cmd = new UpdateDnsZoneCmd(); + setField(cmd, "dnsProviderManager", dnsProviderManager); + setField(cmd, "_entityMgr", entityManager); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "description", "Updated description"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals("Updated description", cmd.getDescription()); + } + + @Test + public void testGetEntityOwnerIdWhenZoneExists() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + DnsZone mockZone = mock(DnsZone.class); + when(mockZone.getAccountId()).thenReturn(ACCOUNT_ID); + when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(mockZone); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdWhenZoneNotFound() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(null); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + + DnsZone mockZone = mock(DnsZone.class); + DnsZoneResponse mockResponse = new DnsZoneResponse(); + mockResponse.setName("example.com"); + + when(dnsProviderManager.updateDnsZone(cmd)).thenReturn(mockZone); + when(dnsProviderManager.createDnsZoneResponse(mockZone)).thenReturn(mockResponse); + + cmd.execute(); + + DnsZoneResponse response = (DnsZoneResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("updatednszoneresponse", response.getResponseName()); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.updateDnsZone(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteThrowsException() throws Exception { + UpdateDnsZoneCmd cmd = createCmd(); + when(dnsProviderManager.updateDnsZone(cmd)).thenThrow(new RuntimeException("Update failed")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java index c905974b2be9..504d3914b70e 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/firewall/CreateFirewallRuleCmdTest.java @@ -21,10 +21,15 @@ import java.util.Collections; import java.util.List; +import com.cloud.network.IpAddress; +import com.cloud.network.NetworkService; +import com.cloud.utils.db.EntityManager; import org.apache.commons.collections.CollectionUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; @@ -33,6 +38,12 @@ @RunWith(MockitoJUnitRunner.class) public class CreateFirewallRuleCmdTest { + @Mock + private EntityManager entityManager; + + @Mock + private NetworkService networkService; + private void validateAllIp4Cidr(final CreateFirewallRuleCmd cmd) { Assert.assertTrue(CollectionUtils.isNotEmpty(cmd.getSourceCidrList())); Assert.assertEquals(1, cmd.getSourceCidrList().size()); @@ -88,4 +99,22 @@ public void testGetSourceCidrList_EmptyFirstElementButMore() { Assert.assertEquals(2, cmd.getSourceCidrList().size()); Assert.assertEquals(cidr, cmd.getSourceCidrList().get(1)); } + + @Test + public void testGetNetworkIdVpcWithoutAssociatedNetworkUsesVpcFallbackAndSyncObjId() { + final CreateFirewallRuleCmd cmd = new CreateFirewallRuleCmd(); + final IpAddress ip = Mockito.mock(IpAddress.class); + + cmd._entityMgr = entityManager; + cmd._networkService = networkService; + ReflectionTestUtils.setField(cmd, "ipAddressId", 42L); + + Mockito.when(networkService.getIp(42L)).thenReturn(ip); + Mockito.when(ip.getAssociatedWithNetworkId()).thenReturn(null); + Mockito.when(ip.getVpcId()).thenReturn(100L); + Mockito.when(ip.getNetworkId()).thenReturn(2L); + + Assert.assertEquals(Long.valueOf(2L), cmd.getNetworkId()); + Assert.assertEquals(Long.valueOf(2L), cmd.getSyncObjId()); + } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java index 2b55d4c6a58d..dbe7669431d8 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/network/routing/DeleteRoutingFirewallRuleCmdTest.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.junit.Assert; import org.junit.Test; @@ -31,6 +32,8 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.UUID; + import static org.junit.Assert.assertEquals; @RunWith(MockitoJUnitRunner.class) @@ -46,6 +49,7 @@ public void testProperties() { ReflectionTestUtils.setField(cmd, "_firewallService", _firewallService); long id = 1L; + String uuid = UUID.randomUUID().toString(); long accountId = 2L; long networkId = 3L; @@ -55,12 +59,14 @@ public void testProperties() { Mockito.when(_firewallService.getFirewallRule(id)).thenReturn(firewallRule); ReflectionTestUtils.setField(cmd, "id", id); + CallContext.current().putApiResourceUuid("id", uuid); + assertEquals(id, (long) cmd.getId()); assertEquals(accountId, cmd.getEntityOwnerId()); assertEquals(networkId, (long) cmd.getApiResourceId()); assertEquals(ApiCommandResourceType.Network, cmd.getApiResourceType()); assertEquals(EventTypes.EVENT_ROUTING_IPV4_FIREWALL_RULE_DELETE, cmd.getEventType()); - assertEquals(String.format("Deleting ipv4 routing firewall rule ID=%s", id), cmd.getEventDescription()); + assertEquals(String.format("Deleting IPv4 routing firewall rule with ID: %s", uuid), cmd.getEventDescription()); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmdTest.java index 99bc9d2b3fb7..ae848b2712b1 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/CreateVMScheduleCmdTest.java @@ -21,8 +21,9 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.db.EntityManager; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -34,9 +35,11 @@ public class CreateVMScheduleCmdTest { @Mock - public VMScheduleManager vmScheduleManager; + public ResourceScheduleManager resourceScheduleManager; + @Mock public EntityManager entityManager; + @InjectMocks private CreateVMScheduleCmd createVMScheduleCmd = new CreateVMScheduleCmd(); @@ -59,10 +62,19 @@ public void tearDown() throws Exception { */ @Test public void testSuccessfulExecution() { - VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class); - Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenReturn(vmScheduleResponse); + ResourceScheduleResponse scheduleResponse = new ResourceScheduleResponse(); + scheduleResponse.setId("schedule-uuid"); + scheduleResponse.setResourceId("vm-uuid"); + Mockito.when(resourceScheduleManager.createSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.anyBoolean(), Mockito.any() + )).thenReturn(scheduleResponse); createVMScheduleCmd.execute(); - Assert.assertEquals(vmScheduleResponse, createVMScheduleCmd.getResponseObject()); + VMScheduleResponse response = (VMScheduleResponse) createVMScheduleCmd.getResponseObject(); + Assert.assertNotNull(response); + Assert.assertEquals("schedule-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals("vm-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "vmId")); } /** @@ -72,7 +84,11 @@ public void testSuccessfulExecution() { */ @Test(expected = InvalidParameterValueException.class) public void testInvalidParameterValueException() { - Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenThrow(InvalidParameterValueException.class); + Mockito.when(resourceScheduleManager.createSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.anyBoolean(), Mockito.any() + )).thenThrow(new InvalidParameterValueException("Invalid schedule")); createVMScheduleCmd.execute(); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmdTest.java index 1f764a84365f..cec7503bd52b 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeleteVMScheduleCmdTest.java @@ -23,8 +23,7 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.SuccessResponse; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -36,7 +35,8 @@ public class DeleteVMScheduleCmdTest { @Mock - public VMScheduleManager vmScheduleManager; + public ResourceScheduleManager resourceScheduleManager; + @Mock public EntityManager entityManager; @@ -64,9 +64,11 @@ public void tearDown() throws Exception { public void testSuccessfulExecution() { final SuccessResponse response = new SuccessResponse(); response.setResponseName(deleteVMScheduleCmd.getCommandName()); - response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase()); + response.setObjectName("vmschedule"); - Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(1L); + Mockito.when(resourceScheduleManager.removeSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenReturn(1L); deleteVMScheduleCmd.execute(); SuccessResponse actualResponse = (SuccessResponse) deleteVMScheduleCmd.getResponseObject(); Assert.assertEquals(response.getResponseName(), actualResponse.getResponseName()); @@ -80,7 +82,9 @@ public void testSuccessfulExecution() { */ @Test(expected = ServerApiException.class) public void testServerApiException() { - Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(0L); + Mockito.when(resourceScheduleManager.removeSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenReturn(0L); deleteVMScheduleCmd.execute(); } @@ -91,7 +95,9 @@ public void testServerApiException() { */ @Test(expected = InvalidParameterValueException.class) public void testInvalidParameterValueException() { - Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenThrow(InvalidParameterValueException.class); + Mockito.when(resourceScheduleManager.removeSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenThrow(new InvalidParameterValueException("Invalid schedule")); deleteVMScheduleCmd.execute(); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java index f7e3e38d9c3f..09d396e40237 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.vm; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -41,6 +42,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.NetworkService; import com.cloud.utils.db.EntityManager; import com.cloud.vm.VmDetailConstants; @@ -480,4 +482,146 @@ public void testGetDataDiskTemplateToDiskOfferingMapInvalidTemplateId() { }); assertTrue(thrownException.getMessage().contains("Unable to translate and find entity with datadisktemplateid")); } + + @Test + public void testSetServiceOfferingId() { + cmd.setServiceOfferingId(101L); + assertEquals(Long.valueOf(101L), cmd.getServiceOfferingId()); + } + + @Test + public void testSetTemplateId() { + cmd.setTemplateId(102L); + assertEquals(Long.valueOf(102L), cmd.getTemplateId()); + } + + @Test + public void testSetVolumeId() { + cmd.setVolumeId(103L); + assertEquals(Long.valueOf(103L), cmd.getVolumeId()); + } + + @Test + public void testSetSnapshotId() { + cmd.setSnapshotId(104L); + assertEquals(Long.valueOf(104L), cmd.getSnapshotId()); + } + + @Test + public void testSetZoneId() { + cmd.setZoneId(105L); + assertEquals(Long.valueOf(105L), cmd.getZoneId()); + } + + @Test + public void testSetName() { + cmd.setName("vm-name"); + assertEquals("vm-name", cmd.getName()); + } + + @Test + public void testSetDisplayName() { + cmd.setDisplayName("vm-display-name"); + assertEquals("vm-display-name", cmd.getDisplayName()); + } + + @Test + public void testSetAccountName() { + cmd.setAccountName("account-name"); + assertEquals("account-name", cmd.getAccountName()); + } + + @Test + public void testSetDomainId() { + cmd.setDomainId(106L); + assertEquals(Long.valueOf(106L), cmd.getDomainId()); + } + + @Test + public void testSetNetworkIds() { + List networkIds = Arrays.asList(11L, 12L); + cmd.setNetworkIds(networkIds); + assertEquals(networkIds, cmd.getNetworkIds()); + } + + @Test + public void testSetBootType() { + cmd.setBootType("UEFI"); + assertEquals(BootType.UEFI, cmd.getBootType()); + } + + @Test + public void testSetBootMode() { + cmd.setBootType("UEFI"); + cmd.setBootMode("SECURE"); + assertEquals(BootMode.SECURE, cmd.getBootMode()); + } + + @Test + public void testSetHypervisor() { + cmd.setHypervisor("KVM"); + assertEquals(HypervisorType.KVM, cmd.getHypervisor()); + } + + @Test + public void testSetUserData() { + cmd.setUserData("dXNlci1kYXRh"); + assertEquals("dXNlci1kYXRh", cmd.getUserData()); + } + + @Test + public void testSetKeyboard() { + cmd.setKeyboard("us"); + assertEquals("us", cmd.getKeyboard()); + } + + @Test + public void testSetProjectId() { + cmd.setProjectId(107L); + assertEquals(Long.valueOf(107L), ReflectionTestUtils.getField(cmd, "projectId")); + } + + @Test + public void testSetDisplayVm() { + cmd.setDisplayVm(Boolean.FALSE); + assertEquals(Boolean.FALSE, cmd.isDisplayVm()); + } + + @Test + public void testSetUserDataId() { + cmd.setUserDataId(108L); + assertEquals(Long.valueOf(108L), cmd.getUserdataId()); + } + + @Test + public void testSetAffinityGroupIds() { + List affinityGroupIds = Arrays.asList(21L, 22L); + cmd.setAffinityGroupIds(affinityGroupIds); + assertEquals(affinityGroupIds, cmd.getAffinityGroupIdList()); + } + + @Test + public void testSetDetails() { + Map details = new HashMap<>(); + details.put("key", "value"); + cmd.setDetails(details); + assertEquals(details, ReflectionTestUtils.getField(cmd, "details")); + } + + @Test + public void testSetExtraConfig() { + cmd.setExtraConfig("cpu-mode=host-passthrough"); + assertEquals("cpu-mode=host-passthrough", cmd.getExtraConfig()); + } + + @Test + public void testSetDynamicScalingEnabled() { + cmd.setDynamicScalingEnabled(Boolean.FALSE); + assertFalse(cmd.isDynamicScalingEnabled()); + } + + @Test + public void testIsBlankInstance() { + assertFalse(cmd.isBlankInstance()); + } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmdTest.java index f5434de3581c..5449be17a576 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/ListVMScheduleCmdTest.java @@ -20,8 +20,9 @@ import com.cloud.exception.InvalidParameterValueException; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -36,7 +37,8 @@ public class ListVMScheduleCmdTest { @Mock - public VMScheduleManager vmScheduleManager; + public ResourceScheduleManager resourceScheduleManager; + @InjectMocks private ListVMScheduleCmd listVMScheduleCmd = new ListVMScheduleCmd(); private AutoCloseable closeable; @@ -58,12 +60,14 @@ public void tearDown() throws Exception { */ @Test public void testEmptyResponse() { - ListResponse response = new ListResponse(); - response.setResponses(new ArrayList()); - Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(response); + ListResponse response = new ListResponse(); + response.setResponses(new ArrayList()); + Mockito.when(resourceScheduleManager.listSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenReturn(response); listVMScheduleCmd.execute(); ListResponse actualResponseObject = (ListResponse) listVMScheduleCmd.getResponseObject(); - Assert.assertEquals(response, actualResponseObject); Assert.assertEquals(0L, actualResponseObject.getResponses().size()); } @@ -74,15 +78,22 @@ public void testEmptyResponse() { */ @Test public void testNonEmptyResponse() { - ListResponse listResponse = new ListResponse(); - VMScheduleResponse response = Mockito.mock(VMScheduleResponse.class); + ListResponse listResponse = new ListResponse(); + ResourceScheduleResponse response = new ResourceScheduleResponse(); + response.setId("schedule-uuid"); + response.setResourceId("vm-uuid"); listResponse.setResponses(Collections.singletonList(response)); - Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(listResponse); + Mockito.when(resourceScheduleManager.listSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenReturn(listResponse); listVMScheduleCmd.execute(); ListResponse actualResponseObject = (ListResponse) listVMScheduleCmd.getResponseObject(); - Assert.assertEquals(listResponse, actualResponseObject); Assert.assertEquals(1L, actualResponseObject.getResponses().size()); - Assert.assertEquals(response, actualResponseObject.getResponses().get(0)); + Assert.assertEquals("schedule-uuid", + org.springframework.test.util.ReflectionTestUtils.getField(actualResponseObject.getResponses().get(0), "id")); + Assert.assertEquals("vm-uuid", + org.springframework.test.util.ReflectionTestUtils.getField(actualResponseObject.getResponses().get(0), "vmId")); } /** @@ -92,7 +103,10 @@ public void testNonEmptyResponse() { */ @Test(expected = InvalidParameterValueException.class) public void testInvalidParameterValueException() { - Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenThrow(InvalidParameterValueException.class); + Mockito.when(resourceScheduleManager.listSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenThrow(InvalidParameterValueException.class); listVMScheduleCmd.execute(); ListResponse actualResponseObject = (ListResponse) listVMScheduleCmd.getResponseObject(); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmdTest.java index 2c6c485f25bf..a24252f4a1d0 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/UpdateVMScheduleCmdTest.java @@ -21,9 +21,11 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.db.EntityManager; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.response.ResourceScheduleResponse; import org.apache.cloudstack.api.response.VMScheduleResponse; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.schedule.ResourceSchedule; +import org.apache.cloudstack.schedule.ResourceScheduleManager; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -35,9 +37,11 @@ public class UpdateVMScheduleCmdTest { @Mock - public VMScheduleManager vmScheduleManager; + public ResourceScheduleManager resourceScheduleManager; + @Mock public EntityManager entityManager; + @InjectMocks private UpdateVMScheduleCmd updateVMScheduleCmd = new UpdateVMScheduleCmd(); @@ -60,10 +64,18 @@ public void tearDown() throws Exception { */ @Test public void testSuccessfulExecution() { - VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class); - Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenReturn(vmScheduleResponse); + ResourceScheduleResponse scheduleResponse = new ResourceScheduleResponse(); + scheduleResponse.setId("schedule-uuid"); + scheduleResponse.setResourceId("vm-uuid"); + Mockito.when(resourceScheduleManager.updateSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenReturn(scheduleResponse); updateVMScheduleCmd.execute(); - Assert.assertEquals(vmScheduleResponse, updateVMScheduleCmd.getResponseObject()); + VMScheduleResponse response = (VMScheduleResponse) updateVMScheduleCmd.getResponseObject(); + Assert.assertNotNull(response); + Assert.assertEquals("schedule-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals("vm-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "vmId")); } /** @@ -73,7 +85,10 @@ public void testSuccessfulExecution() { */ @Test(expected = InvalidParameterValueException.class) public void testInvalidParameterValueException() { - Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenThrow(InvalidParameterValueException.class); + Mockito.when(resourceScheduleManager.updateSchedule( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any() + )).thenThrow(new InvalidParameterValueException("Invalid schedule")); updateVMScheduleCmd.execute(); } @@ -84,11 +99,12 @@ public void testInvalidParameterValueException() { */ @Test public void testSuccessfulGetEntityOwnerId() { - VMSchedule vmSchedule = Mockito.mock(VMSchedule.class); + ResourceSchedule schedule = Mockito.mock(ResourceSchedule.class); VirtualMachine vm = Mockito.mock(VirtualMachine.class); - Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(vmSchedule); - Mockito.when(entityManager.findById(VirtualMachine.class, vmSchedule.getVmId())).thenReturn(vm); + Mockito.when(schedule.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine); + Mockito.when(entityManager.findById(ResourceSchedule.class, updateVMScheduleCmd.getId())).thenReturn(schedule); + Mockito.when(entityManager.findById(VirtualMachine.class, schedule.getResourceId())).thenReturn(vm); long ownerId = updateVMScheduleCmd.getEntityOwnerId(); Assert.assertEquals(vm.getAccountId(), ownerId); @@ -101,8 +117,7 @@ public void testSuccessfulGetEntityOwnerId() { */ @Test(expected = InvalidParameterValueException.class) public void testFailureGetEntityOwnerId() { - VMSchedule vmSchedule = Mockito.mock(VMSchedule.class); - Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(null); - long ownerId = updateVMScheduleCmd.getEntityOwnerId(); + Mockito.when(entityManager.findById(ResourceSchedule.class, updateVMScheduleCmd.getId())).thenReturn(null); + updateVMScheduleCmd.getEntityOwnerId(); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmdTest.java index acb2dc685976..9d56cffd6718 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vpc/UpdateVPCCmdTest.java @@ -93,7 +93,7 @@ public void testExecute() throws ResourceUnavailableException, InsufficientCapac responseGenerator = Mockito.mock(ResponseGenerator.class); cmd._responseGenerator = responseGenerator; Mockito.verify(_vpcService, Mockito.times(0)).updateVpc(Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), - Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyInt(), Mockito.anyString()); + Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyInt(), Mockito.anyString(), Mockito.anyBoolean()); } } diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java b/api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java similarity index 50% rename from server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java rename to api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java index f7610438b78c..09d4eb598ab0 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/response/AttachedIsoResponseTest.java @@ -14,33 +14,33 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +package org.apache.cloudstack.api.response; -package org.apache.cloudstack.storage.heuristics.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class AccountTest { +public final class AttachedIsoResponseTest { @Test - public void toStringTestReturnsValidJson() { - Account variable = new Account(); - variable.setName("test name"); - variable.setId("test id"); - - Domain domainVariable = new Domain(); - domainVariable.setId("domain id"); - domainVariable.setName("domain name"); - variable.setDomain(domainVariable); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "id", "domain"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); + public void testFullConstructorPopulatesAllFields() { + AttachedIsoResponse response = new AttachedIsoResponse("uuid-1", "alpine-iso", "Alpine boot", 3, true); + Assert.assertEquals("uuid-1", response.getId()); + Assert.assertEquals("alpine-iso", response.getName()); + Assert.assertEquals("Alpine boot", response.getDisplayText()); + Assert.assertEquals(Integer.valueOf(3), response.getDeviceSeq()); + Assert.assertTrue(response.getBootable()); } + @Test + public void testNoArgConstructorLeavesFieldsNull() { + AttachedIsoResponse response = new AttachedIsoResponse(); + Assert.assertNull(response.getId()); + Assert.assertNull(response.getName()); + Assert.assertNull(response.getDisplayText()); + Assert.assertNull(response.getDeviceSeq()); + Assert.assertNull(response.getBootable()); + } } diff --git a/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java new file mode 100644 index 000000000000..7811138fffe1 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.api.response; + + +import org.junit.Assert; +import org.junit.Test; + +public class LoginCmdResponseTest { + + @Test + public void testAllGettersAndSetters() { + LoginCmdResponse response = new LoginCmdResponse(); + + response.setUsername("user1"); + response.setUserId("100"); + response.setDomainId("200"); + response.setTimeout(3600); + response.setAccount("account1"); + response.setFirstName("John"); + response.setLastName("Doe"); + response.setType("admin"); + response.setTimeZone("UTC"); + response.setTimeZoneOffset("+00:00"); + response.setRegistered("true"); + response.setSessionKey("session-key"); + response.set2FAenabled("true"); + response.set2FAverfied("false"); + response.setProviderFor2FA("totp"); + response.setIssuerFor2FA("cloudstack"); + response.setManagementServerId("ms-1"); + + Assert.assertEquals("user1", response.getUsername()); + Assert.assertEquals("100", response.getUserId()); + Assert.assertEquals("200", response.getDomainId()); + Assert.assertEquals(Integer.valueOf(3600), response.getTimeout()); + Assert.assertEquals("account1", response.getAccount()); + Assert.assertEquals("John", response.getFirstName()); + Assert.assertEquals("Doe", response.getLastName()); + Assert.assertEquals("admin", response.getType()); + Assert.assertEquals("UTC", response.getTimeZone()); + Assert.assertEquals("+00:00", response.getTimeZoneOffset()); + Assert.assertEquals("true", response.getRegistered()); + Assert.assertEquals("session-key", response.getSessionKey()); + Assert.assertEquals("true", response.is2FAenabled()); + Assert.assertEquals("false", response.is2FAverfied()); + Assert.assertEquals("totp", response.getProviderFor2FA()); + Assert.assertEquals("cloudstack", response.getIssuerFor2FA()); + Assert.assertEquals("ms-1", response.getManagementServerId()); + } + + @Test + public void testPasswordChangeRequired_True() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(true); + Assert.assertTrue(response.getPasswordChangeRequired()); + } + + @Test + public void testPasswordChangeRequired_False() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(false); + Assert.assertFalse(response.getPasswordChangeRequired()); + } + + @Test + public void testPasswordChangeRequired_Null() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(null); + Assert.assertNull("Boolean.parseBoolean(null) should return null", response.getPasswordChangeRequired()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java b/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java new file mode 100644 index 000000000000..bd993380e068 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java @@ -0,0 +1,105 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class DnsRecordTest { + + @Test + public void testDefaultConstructor() { + DnsRecord record = new DnsRecord(); + assertNull(record.getName()); + assertNull(record.getType()); + assertNull(record.getContents()); + assertEquals(0, record.getTtl()); + } + + @Test + public void testParameterizedConstructor() { + List contents = Arrays.asList("192.168.1.1"); + DnsRecord record = new DnsRecord("www", DnsRecord.RecordType.A, contents, 3600); + + assertEquals("www", record.getName()); + assertEquals(DnsRecord.RecordType.A, record.getType()); + assertEquals(contents, record.getContents()); + assertEquals(3600, record.getTtl()); + } + + @Test + public void testSettersAndGetters() { + DnsRecord record = new DnsRecord(); + List contents = Arrays.asList("10.0.0.1", "10.0.0.2"); + + record.setName("mail"); + record.setType(DnsRecord.RecordType.AAAA); + record.setContents(contents); + record.setTtl(7200); + + assertEquals("mail", record.getName()); + assertEquals(DnsRecord.RecordType.AAAA, record.getType()); + assertEquals(contents, record.getContents()); + assertEquals(7200, record.getTtl()); + } + + // RecordType.fromString tests + + @Test + public void testFromStringValid() { + assertEquals(DnsRecord.RecordType.A, DnsRecord.RecordType.fromString("A")); + assertEquals(DnsRecord.RecordType.AAAA, DnsRecord.RecordType.fromString("AAAA")); + assertEquals(DnsRecord.RecordType.CNAME, DnsRecord.RecordType.fromString("CNAME")); + assertEquals(DnsRecord.RecordType.MX, DnsRecord.RecordType.fromString("MX")); + assertEquals(DnsRecord.RecordType.TXT, DnsRecord.RecordType.fromString("TXT")); + assertEquals(DnsRecord.RecordType.SRV, DnsRecord.RecordType.fromString("SRV")); + assertEquals(DnsRecord.RecordType.PTR, DnsRecord.RecordType.fromString("PTR")); + assertEquals(DnsRecord.RecordType.NS, DnsRecord.RecordType.fromString("NS")); + } + + @Test + public void testFromStringCaseInsensitive() { + assertEquals(DnsRecord.RecordType.A, DnsRecord.RecordType.fromString("a")); + assertEquals(DnsRecord.RecordType.CNAME, DnsRecord.RecordType.fromString("cname")); + assertEquals(DnsRecord.RecordType.MX, DnsRecord.RecordType.fromString("mx")); + } + + @Test + public void testFromStringNull() { + assertNull(DnsRecord.RecordType.fromString(null)); + } + + @Test(expected = CloudRuntimeException.class) + public void testFromStringInvalid() { + DnsRecord.RecordType.fromString("INVALID"); + } + + @Test + public void testRecordTypeValues() { + DnsRecord.RecordType[] values = DnsRecord.RecordType.values(); + assertNotNull(values); + assertEquals(8, values.length); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java b/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java new file mode 100644 index 000000000000..9b7346f332cb --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/extension/CustomActionResultResponseTest.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.extension; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +public class CustomActionResultResponseTest { + + private CustomActionResultResponse response; + + @Before + public void setUp() { + response = new CustomActionResultResponse(); + } + + @Test + public void getResultReturnsNullByDefault() { + assertNull(response.getResult()); + } + + @Test + public void getResultReturnsSetValue() { + Map result = Map.of("message", "OK", "details", "All good"); + response.setResult(result); + assertEquals(result, response.getResult()); + assertEquals("OK", response.getResult().get("message")); + } + + @Test + public void isSuccessReturnsFalseWhenSuccessIsNull() { + // success is null by default + assertFalse(response.isSuccess()); + } + + @Test + public void isSuccessReturnsFalseWhenSuccessIsFalse() { + response.setSuccess(false); + assertFalse(response.isSuccess()); + } + + @Test + public void isSuccessReturnsTrueWhenSuccessIsTrue() { + response.setSuccess(true); + assertTrue(response.isSuccess()); + } + + @Test + public void getSuccessReturnsNullByDefault() { + assertNull(response.getSuccess()); + } + + @Test + public void getSuccessReturnsTrueAfterSetSuccessTrue() { + response.setSuccess(true); + assertTrue(response.getSuccess()); + } + + @Test + public void getSuccessReturnsFalseAfterSetSuccessFalse() { + response.setSuccess(false); + assertFalse(response.getSuccess()); + } + + @Test + public void setAndGetResultWithNullResult() { + response.setResult(null); + assertNull(response.getResult()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java index ae4314aa11a8..a6bd49a2d8be 100644 --- a/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java +++ b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java @@ -40,6 +40,12 @@ public void testResourceType() { assertEquals(com.cloud.vm.VirtualMachine.class, vmType.getAssociatedClass()); } + @Test + public void testNetworkResourceType() { + ExtensionCustomAction.ResourceType networkType = ExtensionCustomAction.ResourceType.Network; + assertEquals(com.cloud.network.Network.class, networkType.getAssociatedClass()); + } + @Test public void testParameterTypeSupportsOptions() { assertTrue(ExtensionCustomAction.Parameter.Type.STRING.canSupportsOptions()); diff --git a/client/pom.xml b/client/pom.xml index b8dffe65d4fb..968f735af600 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -121,6 +121,11 @@ cloud-plugin-storage-volume-adaptive ${project.version} + + org.apache.cloudstack + cloud-plugin-storage-volume-ontap + ${project.version} + org.apache.cloudstack cloud-plugin-storage-volume-solidfire @@ -251,6 +256,16 @@ cloud-plugin-metrics ${project.version} + + org.apache.cloudstack + cloud-plugin-kms-database + ${project.version} + + + org.apache.cloudstack + cloud-plugin-kms-pkcs11 + ${project.version} + org.apache.cloudstack cloud-plugin-network-nvp @@ -372,11 +387,6 @@ cloud-plugin-explicit-dedication ${project.version} - - org.apache.cloudstack - cloud-plugin-host-allocator-random - ${project.version} - org.apache.cloudstack cloud-plugin-outofbandmanagement-driver-ipmitool @@ -612,6 +622,16 @@ cloud-plugin-backup-nas ${project.version} + + org.apache.cloudstack + cloud-plugin-integrations-veeam-control-service + ${project.version} + + + org.apache.cloudstack + cloud-plugin-backup-kvm-backup-on-secondary-storage + ${project.version} + org.apache.cloudstack cloud-plugin-integrations-kubernetes-service @@ -662,6 +682,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-dns-powerdns + ${project.version} + + @@ -716,17 +742,17 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on ${cs.bcprov.version} org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on ${cs.bcprov.version} org.bouncycastle - bctls-jdk15on + bctls-jdk18on ${cs.bcprov.version} @@ -906,13 +932,13 @@ org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on false ${project.build.directory}/lib org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on false ${project.build.directory}/lib @@ -936,7 +962,7 @@ org.bouncycastle - bctls-jdk15on + bctls-jdk18on false ${project.build.directory}/lib @@ -971,9 +997,9 @@ org.apache.tomcat.embed:tomcat-embed-core org.apache.geronimo.specs:geronimo-servlet_3.0_spec org.apache.geronimo.specs:geronimo-javamail_1.4_spec - org.bouncycastle:bcprov-jdk15on - org.bouncycastle:bcpkix-jdk15on - org.bouncycastle:bctls-jdk15on + org.bouncycastle:bcprov-jdk18on + org.bouncycastle:bcpkix-jdk18on + org.bouncycastle:bctls-jdk18on com.mysql:mysql-connector-j org.apache.cloudstack:cloud-plugin-storage-volume-storpool org.apache.cloudstack:cloud-plugin-storage-volume-linstor diff --git a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java index fc066e5c5899..c46fb697a3c1 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/CheckConvertInstanceCommand.java @@ -18,6 +18,8 @@ public class CheckConvertInstanceCommand extends Command { boolean checkWindowsGuestConversionSupport = false; + boolean useVddk = false; + String vddkLibDir; public CheckConvertInstanceCommand() { } @@ -26,6 +28,11 @@ public CheckConvertInstanceCommand(boolean checkWindowsGuestConversionSupport) { this.checkWindowsGuestConversionSupport = checkWindowsGuestConversionSupport; } + public CheckConvertInstanceCommand(boolean checkWindowsGuestConversionSupport, boolean useVddk) { + this.checkWindowsGuestConversionSupport = checkWindowsGuestConversionSupport; + this.useVddk = useVddk; + } + @Override public boolean executeInSequence() { return false; @@ -34,4 +41,20 @@ public boolean executeInSequence() { public boolean getCheckWindowsGuestConversionSupport() { return checkWindowsGuestConversionSupport; } + + public boolean isUseVddk() { + return useVddk; + } + + public void setUseVddk(boolean useVddk) { + this.useVddk = useVddk; + } + + public String getVddkLibDir() { + return vddkLibDir; + } + + public void setVddkLibDir(String vddkLibDir) { + this.vddkLibDir = vddkLibDir; + } } diff --git a/core/src/main/java/com/cloud/agent/api/CheckOnHostAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckOnHostAnswer.java index 5a26b22ec6a5..41e266784db8 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckOnHostAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CheckOnHostAnswer.java @@ -38,6 +38,8 @@ public CheckOnHostAnswer(CheckOnHostCommand cmd, Boolean alive, String details) public CheckOnHostAnswer(CheckOnHostCommand cmd, String details) { super(cmd, false, details); + determined = false; + alive = false; } public boolean isDetermined() { @@ -47,5 +49,4 @@ public boolean isDetermined() { public boolean isAlive() { return alive; } - } diff --git a/core/src/main/java/com/cloud/agent/api/CheckOnHostCommand.java b/core/src/main/java/com/cloud/agent/api/CheckOnHostCommand.java index 94239f2900ef..72b5217604dc 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckOnHostCommand.java +++ b/core/src/main/java/com/cloud/agent/api/CheckOnHostCommand.java @@ -24,7 +24,7 @@ public class CheckOnHostCommand extends Command { HostTO host; - boolean reportCheckFailureIfOneStorageIsDown; + boolean reportIfHeartBeatFailedForOneStoragePool; protected CheckOnHostCommand() { } @@ -34,17 +34,17 @@ public CheckOnHostCommand(Host host) { setWait(20); } - public CheckOnHostCommand(Host host, boolean reportCheckFailureIfOneStorageIsDown) { + public CheckOnHostCommand(Host host, boolean reportIfHeartBeatFailedForOneStoragePool) { this(host); - this.reportCheckFailureIfOneStorageIsDown = reportCheckFailureIfOneStorageIsDown; + this.reportIfHeartBeatFailedForOneStoragePool = reportIfHeartBeatFailedForOneStoragePool; } public HostTO getHost() { return host; } - public boolean isCheckFailedOnOneStorage() { - return reportCheckFailureIfOneStorageIsDown; + public boolean shouldReportIfHeartBeatFailedForOneStoragePool() { + return reportIfHeartBeatFailedForOneStoragePool; } @Override diff --git a/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java b/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java new file mode 100644 index 000000000000..00373ec75364 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CleanupConvertedInstanceDisksCommand.java @@ -0,0 +1,57 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; + +/** + * This command is used to cleanup the converted instance disks from the storage pool: vmVolumesStore and the prefix: vmVolumesPrefix. + */ +public class CleanupConvertedInstanceDisksCommand extends Command { + + private DataStoreTO vmVolumesStore; + private String vmVolumesPrefix; + + public CleanupConvertedInstanceDisksCommand(DataStoreTO vmVolumesStore, String vmVolumesPrefix) { + this.vmVolumesStore = vmVolumesStore; + this.vmVolumesPrefix = vmVolumesPrefix; + } + + public DataStoreTO getVmVolumesStore() { + return vmVolumesStore; + } + + public void setVmVolumesStore(DataStoreTO vmVolumesStore) { + this.vmVolumesStore = vmVolumesStore; + } + + public String getVmVolumesPrefix() { + return vmVolumesPrefix; + } + + public void setVmVolumesPrefix(String vmVolumesPrefix) { + this.vmVolumesPrefix = vmVolumesPrefix; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java index 24336747ccf4..38e0dca7736c 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java @@ -31,6 +31,10 @@ public class ConvertInstanceCommand extends Command { private boolean exportOvfToConversionLocation; private int threadsCountToExportOvf = 0; private String extraParams; + private boolean useVddk; + private String vddkLibDir; + private String vddkTransports; + private String vddkThumbprint; public ConvertInstanceCommand() { } @@ -90,6 +94,38 @@ public void setExtraParams(String extraParams) { this.extraParams = extraParams; } + public boolean isUseVddk() { + return useVddk; + } + + public void setUseVddk(boolean useVddk) { + this.useVddk = useVddk; + } + + public String getVddkLibDir() { + return vddkLibDir; + } + + public void setVddkLibDir(String vddkLibDir) { + this.vddkLibDir = vddkLibDir; + } + + public String getVddkTransports() { + return vddkTransports; + } + + public void setVddkTransports(String vddkTransports) { + this.vddkTransports = vddkTransports; + } + + public String getVddkThumbprint() { + return vddkThumbprint; + } + + public void setVddkThumbprint(String vddkThumbprint) { + this.vddkThumbprint = vddkThumbprint; + } + @Override public boolean executeInSequence() { return false; diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java new file mode 100644 index 000000000000..7794b4a53083 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBackupsBetweenSecondaryStoragesCommand.java @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; + +import java.util.List; + +public class MigrateBackupsBetweenSecondaryStoragesCommand extends MigrateBetweenSecondaryStoragesCommand { + + List> backupChain; + + public MigrateBackupsBetweenSecondaryStoragesCommand() { + } + + public MigrateBackupsBetweenSecondaryStoragesCommand(List> backupChain, DataStoreTO srcDataStore, DataStoreTO destDataStore) { + super(srcDataStore, destDataStore); + this.backupChain = backupChain; + } + + public List> getBackupChain() { + return backupChain; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java new file mode 100644 index 000000000000..48dc7e2c85f4 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommand.java @@ -0,0 +1,48 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.DataStoreTO; + +public abstract class MigrateBetweenSecondaryStoragesCommand extends Command { + + DataStoreTO srcDataStore; + DataStoreTO destDataStore; + + public MigrateBetweenSecondaryStoragesCommand() { + } + + public MigrateBetweenSecondaryStoragesCommand(DataStoreTO srcDataStore, DataStoreTO destDataStore) { + this.srcDataStore = srcDataStore; + this.destDataStore = destDataStore; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public DataStoreTO getSrcDataStore() { + return srcDataStore; + } + + public DataStoreTO getDestDataStore() { + return destDataStore; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java new file mode 100644 index 000000000000..fd303093e092 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/MigrateBetweenSecondaryStoragesCommandAnswer.java @@ -0,0 +1,41 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.utils.Pair; + +import java.util.List; + +public class MigrateBetweenSecondaryStoragesCommandAnswer extends Answer { + + List> migratedResourcesIdAndCheckpointPath; + + public MigrateBetweenSecondaryStoragesCommandAnswer() { + } + + public MigrateBetweenSecondaryStoragesCommandAnswer(MigrateBetweenSecondaryStoragesCommand cmd, boolean success, String result, List> migratedResourcesIdAndCheckpointPath) { + super(cmd, success, result); + this.migratedResourcesIdAndCheckpointPath = migratedResourcesIdAndCheckpointPath; + } + + public List> getMigratedResources() { + return migratedResourcesIdAndCheckpointPath; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java index 5ac4e9ae445e..7196247ffc23 100644 --- a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java +++ b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java @@ -26,6 +26,7 @@ import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.storage.Storage; public class MigrateCommand extends Command { private String vmName; @@ -42,6 +43,7 @@ public class MigrateCommand extends Command { private Map dpdkInterfaceMapping = new HashMap<>(); private int newVmCpuShares; + private boolean clvmCrossPoolMigration; Map vlanToPersistenceMap = new HashMap<>(); @@ -149,6 +151,14 @@ public void setNewVmCpuShares(int newVmCpuShares) { this.newVmCpuShares = newVmCpuShares; } + public boolean isClvmCrossPoolMigration() { + return clvmCrossPoolMigration; + } + + public void setClvmCrossPoolMigration(boolean clvmCrossPoolMigration) { + this.clvmCrossPoolMigration = clvmCrossPoolMigration; + } + public static class MigrateDiskInfo { public enum DiskType { FILE, BLOCK; @@ -184,6 +194,8 @@ public String toString() { private final String sourceText; private final String backingStoreText; private boolean isSourceDiskOnStorageFileSystem; + private Storage.StoragePoolType sourcePoolType; + private Storage.StoragePoolType destPoolType; public MigrateDiskInfo(final String serialNumber, final DiskType diskType, final DriverType driverType, final Source source, final String sourceText) { this.serialNumber = serialNumber; @@ -232,6 +244,22 @@ public boolean isSourceDiskOnStorageFileSystem() { public void setSourceDiskOnStorageFileSystem(boolean isDiskOnFileSystemStorage) { this.isSourceDiskOnStorageFileSystem = isDiskOnFileSystemStorage; } + + public Storage.StoragePoolType getSourcePoolType() { + return sourcePoolType; + } + + public void setSourcePoolType(Storage.StoragePoolType sourcePoolType) { + this.sourcePoolType = sourcePoolType; + } + + public Storage.StoragePoolType getDestPoolType() { + return destPoolType; + } + + public void setDestPoolType(Storage.StoragePoolType destPoolType) { + this.destPoolType = destPoolType; + } } @Override diff --git a/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java b/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java index a3401d6faed7..275468214f51 100644 --- a/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java @@ -46,6 +46,10 @@ public ModifyStoragePoolAnswer(ModifyStoragePoolCommand cmd, long capacityBytes, templateInfo = tInfo; } + public ModifyStoragePoolAnswer(final Command command, final boolean success, final String details) { + super(command, success, details); + } + public ModifyStoragePoolAnswer(ModifyStoragePoolCommand cmd, boolean success, String details) { super(cmd, success, details); } diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java new file mode 100644 index 000000000000..24fdf8402029 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java @@ -0,0 +1,42 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +/** + * Answer for PostMigrationCommand. + * Indicates success or failure of post-migration operations on the destination host. + */ +public class PostMigrationAnswer extends Answer { + + protected PostMigrationAnswer() { + } + + public PostMigrationAnswer(PostMigrationCommand cmd, String detail) { + super(cmd, false, detail); + } + + public PostMigrationAnswer(PostMigrationCommand cmd, Exception ex) { + super(cmd, ex); + } + + public PostMigrationAnswer(PostMigrationCommand cmd) { + super(cmd, true, null); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java new file mode 100644 index 000000000000..938000c3593c --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java @@ -0,0 +1,59 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PostMigrationCommand is sent to the destination host after a successful VM migration. + * It performs post-migration tasks such as: + * - Claiming exclusive locks on CLVM volumes (converting from shared to exclusive mode) + * - Other post-migration cleanup operations + */ +public class PostMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PostMigrationCommand() { + } + + public PostMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java new file mode 100644 index 000000000000..3ff30391eaef --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java @@ -0,0 +1,61 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PreMigrationCommand is sent to the source host before VM migration starts. + * It performs pre-migration tasks such as: + * - Converting CLVM volume exclusive locks to shared mode so destination host can access them + * - Other pre-migration preparation operations on the source host + * + * This command runs on the SOURCE host before PrepareForMigrationCommand runs on the DESTINATION host. + */ +public class PreMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PreMigrationCommand() { + } + + public PreMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PropagateResourceEventCommand.java b/core/src/main/java/com/cloud/agent/api/PropagateResourceEventCommand.java index ed337885beed..21c4e7b97d03 100644 --- a/core/src/main/java/com/cloud/agent/api/PropagateResourceEventCommand.java +++ b/core/src/main/java/com/cloud/agent/api/PropagateResourceEventCommand.java @@ -24,6 +24,8 @@ public class PropagateResourceEventCommand extends Command { long hostId; ResourceState.Event event; + boolean forced; + boolean forceDeleteStorage; protected PropagateResourceEventCommand() { @@ -34,6 +36,13 @@ public PropagateResourceEventCommand(long hostId, ResourceState.Event event) { this.event = event; } + public PropagateResourceEventCommand(long hostId, ResourceState.Event event, boolean forced, boolean forceDeleteStorage) { + this.hostId = hostId; + this.event = event; + this.forced = forced; + this.forceDeleteStorage = forceDeleteStorage; + } + public long getHostId() { return hostId; } @@ -42,6 +51,14 @@ public ResourceState.Event getEvent() { return event; } + public boolean isForced() { + return forced; + } + + public boolean isForceDeleteStorage() { + return forceDeleteStorage; + } + @Override public boolean executeInSequence() { // TODO Auto-generated method stub diff --git a/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java b/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java index dc63b1ee746d..e2953e9c7ad7 100644 --- a/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ScaleVmCommand.java @@ -30,6 +30,7 @@ public class ScaleVmCommand extends Command { Integer maxSpeed; long minRam; long maxRam; + private boolean limitCpuUseChange; public VirtualMachineTO getVm() { return vm; @@ -43,7 +44,7 @@ public int getCpus() { return cpus; } - public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpeed, long minRam, long maxRam, boolean limitCpuUse) { + public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpeed, long minRam, long maxRam, boolean limitCpuUse, Double cpuQuotaPercentage, boolean limitCpuUseChange) { super(); this.vmName = vmName; this.cpus = cpus; @@ -52,6 +53,8 @@ public ScaleVmCommand(String vmName, int cpus, Integer minSpeed, Integer maxSpee this.minRam = minRam; this.maxRam = maxRam; this.vm = new VirtualMachineTO(1L, vmName, null, cpus, minSpeed, maxSpeed, minRam, maxRam, null, null, false, limitCpuUse, null); + this.vm.setCpuQuotaPercentage(cpuQuotaPercentage); + this.limitCpuUseChange = limitCpuUseChange; } public void setCpus(int cpus) { @@ -102,6 +105,10 @@ public VirtualMachineTO getVirtualMachine() { return vm; } + public boolean getLimitCpuUseChange() { + return limitCpuUseChange; + } + @Override public boolean executeInSequence() { return true; diff --git a/core/src/main/java/com/cloud/agent/api/StartCommand.java b/core/src/main/java/com/cloud/agent/api/StartCommand.java index 24b0ac3787b5..ea763d6cd4bd 100644 --- a/core/src/main/java/com/cloud/agent/api/StartCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StartCommand.java @@ -22,13 +22,15 @@ import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.host.Host; +import java.util.List; + /** */ public class StartCommand extends Command { VirtualMachineTO vm; String hostIp; boolean executeInSequence = false; - String secondaryStorage; + private List secondaryStorages; public VirtualMachineTO getVirtualMachine() { return vm; @@ -50,18 +52,18 @@ public StartCommand(VirtualMachineTO vm, Host host, boolean executeInSequence) { this.vm = vm; this.hostIp = host.getPrivateIpAddress(); this.executeInSequence = executeInSequence; - this.secondaryStorage = null; + this.secondaryStorages = null; } public String getHostIp() { return this.hostIp; } - public String getSecondaryStorage() { - return this.secondaryStorage; + public List getSecondaryStorages() { + return this.secondaryStorages; } - public void setSecondaryStorage(String secondary) { - this.secondaryStorage = secondary; + public void setSecondaryStorages(List secondary) { + this.secondaryStorages = secondary; } } diff --git a/core/src/main/java/com/cloud/agent/api/UpdateVmNicAnswer.java b/core/src/main/java/com/cloud/agent/api/UpdateVmNicAnswer.java new file mode 100644 index 000000000000..0af6d7fa3a3d --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UpdateVmNicAnswer.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.agent.api; + +public class UpdateVmNicAnswer extends Answer { + public UpdateVmNicAnswer() { + } + + public UpdateVmNicAnswer(UpdateVmNicCommand cmd, boolean success, String result) { + super(cmd, success, result); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/UpdateVmNicCommand.java b/core/src/main/java/com/cloud/agent/api/UpdateVmNicCommand.java new file mode 100644 index 000000000000..a5c5faf32fed --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/UpdateVmNicCommand.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.agent.api; + +public class UpdateVmNicCommand extends Command { + + String nicMacAddress; + String instanceName; + Boolean enabled; + + @Override + public boolean executeInSequence() { + return true; + } + + protected UpdateVmNicCommand() { + } + + public UpdateVmNicCommand(String nicMacAdderss, String instanceName, Boolean enabled) { + this.nicMacAddress = nicMacAdderss; + this.instanceName = instanceName; + this.enabled = enabled; + } + + public String getNicMacAddress() { + return nicMacAddress; + } + + public String getVmName() { + return instanceName; + } + + public Boolean isEnabled() { + return enabled; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java b/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java index 7fb65fe15cf9..67ca447e3853 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/DhcpEntryCommand.java @@ -36,6 +36,7 @@ public class DhcpEntryCommand extends NetworkElementCommand { private boolean isDefault; boolean executeInSequence = false; boolean remove; + Long leaseTime; public boolean isRemove() { return remove; @@ -152,4 +153,12 @@ public boolean isDefault() { public void setDefault(boolean isDefault) { this.isDefault = isDefault; } + + public Long getLeaseTime() { + return leaseTime; + } + + public void setLeaseTime(Long leaseTime) { + this.leaseTime = leaseTime; + } } diff --git a/core/src/main/java/com/cloud/agent/api/routing/LoadBalancerConfigCommand.java b/core/src/main/java/com/cloud/agent/api/routing/LoadBalancerConfigCommand.java index d8cc74817d7b..96d73e11990d 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/LoadBalancerConfigCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/LoadBalancerConfigCommand.java @@ -36,6 +36,7 @@ public class LoadBalancerConfigCommand extends NetworkElementCommand { public String lbStatsAuth = "admin1:AdMiN123"; public String lbStatsUri = "/admin?stats"; public String maxconn = ""; + public Long idleTimeout = 50000L; /* 0=infinite, >0 = timeout in milliseconds */ public String lbProtocol; public boolean keepAliveEnabled = false; NicTO nic; @@ -50,7 +51,7 @@ public LoadBalancerConfigCommand(LoadBalancerTO[] loadBalancers, Long vpcId) { } public LoadBalancerConfigCommand(LoadBalancerTO[] loadBalancers, String publicIp, String guestIp, String privateIp, NicTO nic, Long vpcId, String maxconn, - boolean keepAliveEnabled) { + boolean keepAliveEnabled, Long idleTimeout) { this.loadBalancers = loadBalancers; this.lbStatsPublicIP = publicIp; this.lbStatsPrivateIP = privateIp; @@ -59,6 +60,7 @@ public LoadBalancerConfigCommand(LoadBalancerTO[] loadBalancers, String publicIp this.vpcId = vpcId; this.maxconn = maxconn; this.keepAliveEnabled = keepAliveEnabled; + this.idleTimeout = idleTimeout; } public NicTO getNic() { diff --git a/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java b/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java index 400b6bb80917..20d2ea0a443d 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/NetworkElementCommand.java @@ -37,6 +37,7 @@ public abstract class NetworkElementCommand extends Command { public static final String ZONE_NETWORK_TYPE = "zone.network.type"; public static final String GUEST_BRIDGE = "guest.bridge"; public static final String VPC_PRIVATE_GATEWAY = "vpc.gateway.private"; + public static final String VPC_ID = "vpc.id"; public static final String FIREWALL_EGRESS_DEFAULT = "firewall.egress.default"; public static final String NETWORK_PUB_LAST_IP = "network.public.last.ip"; public static final String HYPERVISOR_HOST_PRIVATE_IP = "hypervisor.private.ip"; diff --git a/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java b/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java index c56f8d20fbe6..ff81ab7749c5 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/SetFirewallRulesCommand.java @@ -32,6 +32,7 @@ */ public class SetFirewallRulesCommand extends NetworkElementCommand { FirewallRuleTO[] rules; + Long vpcId; protected SetFirewallRulesCommand() { } @@ -40,10 +41,19 @@ public SetFirewallRulesCommand(List rules) { this.rules = rules.toArray(new FirewallRuleTO[rules.size()]); } + public SetFirewallRulesCommand(List rules, Long vpcId) { + this.rules = rules.toArray(new FirewallRuleTO[rules.size()]); + this.vpcId = vpcId; + } + public FirewallRuleTO[] getRules() { return rules; } + public Long getVpcId() { + return vpcId; + } + public String[][] generateFwRules() { String[][] result = new String[2][]; Set toAdd = new HashSet(); diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java index 4d61249c7cbc..9a571e34c7e6 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java @@ -20,20 +20,19 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; -import com.cloud.utils.Pair; import java.util.Map; public class CreateDiskOnlyVmSnapshotAnswer extends Answer { - protected Map> mapVolumeToSnapshotSizeAndNewVolumePath; + protected Map mapVolumeToSnapshotSize; - public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map> mapVolumeToSnapshotSizeAndNewVolumePath) { + public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map mapVolumeToSnapshotSize) { super(command, success, details); - this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath; + this.mapVolumeToSnapshotSize = mapVolumeToSnapshotSize; } - public Map> getMapVolumeToSnapshotSizeAndNewVolumePath() { - return mapVolumeToSnapshotSizeAndNewVolumePath; + public Map getMapVolumeToSnapshotSize() { + return mapVolumeToSnapshotSize; } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java index 952bf0c971de..da1b420625f8 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java @@ -21,6 +21,7 @@ import com.cloud.agent.api.VMSnapshotBaseCommand; import com.cloud.agent.api.VMSnapshotTO; +import com.cloud.utils.Pair; import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -30,12 +31,19 @@ public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand { protected VirtualMachine.State vmState; - public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List volumeTOs, String guestOSType, VirtualMachine.State vmState) { - super(vmName, snapshot, volumeTOs, guestOSType); + List> volumeTosAndNewPaths; + + public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List> volumeTosAndNewPaths, String guestOSType, VirtualMachine.State vmState) { + super(vmName, snapshot, null, guestOSType); this.vmState = vmState; + this.volumeTosAndNewPaths = volumeTosAndNewPaths; } public VirtualMachine.State getVmState() { return vmState; } + + public List> getVolumeTosAndNewPaths() { + return volumeTosAndNewPaths; + } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java index 0c6373134b18..1c5eb7b9a9af 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java @@ -140,7 +140,7 @@ public void setTemplateSize(long templateSize) { } public Long getTemplateSize() { - return templateSize; + return templateSize == 0 ? templatePhySicalSize : templateSize; } public void setTemplatePhySicalSize(long templatePhySicalSize) { diff --git a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java index b6396c24d10a..1a47d97d5e25 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java +++ b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java @@ -19,28 +19,28 @@ package com.cloud.agent.api.storage; import com.cloud.agent.api.Command; -import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; import java.util.List; public class MergeDiskOnlyVmSnapshotCommand extends Command { - private List snapshotMergeTreeToList; - private VirtualMachine.State vmState; + private List snapshotMergeTreeToList; + private boolean isVmRunning; private String vmName; - public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, VirtualMachine.State vmState, String vmName) { + public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, boolean isVmRunning, String vmName) { this.snapshotMergeTreeToList = snapshotMergeTreeToList; - this.vmState = vmState; + this.isVmRunning = isVmRunning; this.vmName = vmName; } - public List getSnapshotMergeTreeToList() { + public List getDeltaMergeTreeToList() { return snapshotMergeTreeToList; } - public VirtualMachine.State getVmState() { - return vmState; + public boolean isVmRunning() { + return isVmRunning; } public String getVmName() { diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java index 9c5b657bb4e4..041645046b23 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/DhcpEntryConfigItem.java @@ -35,7 +35,7 @@ public List generateConfig(final NetworkElementCommand cmd) { final DhcpEntryCommand command = (DhcpEntryCommand) cmd; final VmDhcpConfig vmDhcpConfig = new VmDhcpConfig(command.getVmName(), command.getVmMac(), command.getVmIpAddress(), command.getVmIp6Address(), command.getDuid(), command.getDefaultDns(), - command.getDefaultRouter(), command.getStaticRoutes(), command.isDefault(), command.isRemove()); + command.getDefaultRouter(), command.getStaticRoutes(), command.isDefault(), command.isRemove(), command.getLeaseTime()); return generateConfigItems(vmDhcpConfig); } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java index d9cb8b0b2645..1c43cd1823b9 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VmDhcpConfig.java @@ -29,6 +29,7 @@ public class VmDhcpConfig extends ConfigBase { private String defaultGateway; private String staticRoutes; private boolean defaultEntry; + private Long leaseTime; // Indicate if the entry should be removed when set to true private boolean remove; @@ -39,6 +40,11 @@ public VmDhcpConfig() { public VmDhcpConfig(String hostName, String macAddress, String ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String defaultGateway, String staticRoutes, boolean defaultEntry, boolean remove) { + this(hostName, macAddress, ipv4Address, ipv6Address, ipv6Duid, dnsAddresses, defaultGateway, staticRoutes, defaultEntry, remove, null); + } + + public VmDhcpConfig(String hostName, String macAddress, String ipv4Address, String ipv6Address, String ipv6Duid, String dnsAddresses, String defaultGateway, + String staticRoutes, boolean defaultEntry, boolean remove, Long leaseTime) { super(VM_DHCP); this.hostName = hostName; this.macAddress = macAddress; @@ -50,6 +56,7 @@ public VmDhcpConfig(String hostName, String macAddress, String ipv4Address, Stri this.staticRoutes = staticRoutes; this.defaultEntry = defaultEntry; this.remove = remove; + this.leaseTime = leaseTime; } public String getHostName() { @@ -132,4 +139,12 @@ public void setDefaultEntry(boolean defaultEntry) { this.defaultEntry = defaultEntry; } + public Long getLeaseTime() { + return leaseTime; + } + + public void setLeaseTime(Long leaseTime) { + this.leaseTime = leaseTime; + } + } diff --git a/core/src/main/java/com/cloud/network/HAProxyConfigurator.java b/core/src/main/java/com/cloud/network/HAProxyConfigurator.java index 128652fc64fa..7d544c2e49c0 100644 --- a/core/src/main/java/com/cloud/network/HAProxyConfigurator.java +++ b/core/src/main/java/com/cloud/network/HAProxyConfigurator.java @@ -635,6 +635,19 @@ public String[] generateConfiguration(final LoadBalancerConfigCommand lbCmd) { if (lbCmd.keepAliveEnabled) { dSection.set(7, "\tno option httpclose"); } + if (lbCmd.idleTimeout > 0) { + dSection.set(9, "\ttimeout client " + Long.toString(lbCmd.idleTimeout)); + dSection.set(10, "\ttimeout server " + Long.toString(lbCmd.idleTimeout)); + } else if (lbCmd.idleTimeout == 0) { + // .remove() is not allowed, only .set() operations are allowed as the list + // is a fixed size. So lets just mark the entry as blank. + dSection.set(9, ""); + dSection.set(10, ""); + } else { + // Negative idleTimeout values are considered invalid; retain the + // default HAProxy timeout values from defaultsSection for predictability. + logger.warn("Negative idleTimeout ({}) configured; retaining default HAProxy timeouts.", lbCmd.idleTimeout); + } if (logger.isDebugEnabled()) { for (final String s : dSection) { diff --git a/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java b/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java index dd8e2abcd643..31c384eab3a0 100644 --- a/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java +++ b/core/src/main/java/com/cloud/storage/resource/StorageProcessor.java @@ -85,4 +85,8 @@ public interface StorageProcessor { public Answer checkDataStoreStoragePolicyCompliance(CheckDataStoreStoragePolicyComplianceCommand cmd); public Answer syncVolumePath(SyncVolumePathCommand cmd); + + default Answer deleteBackup(DeleteCommand cmd) { + return new Answer(cmd, false, "Operation not implemented"); + } } diff --git a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java index 318c069b0b0b..3d2608c0c4b8 100644 --- a/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java +++ b/core/src/main/java/com/cloud/storage/resource/StorageSubsystemCommandHandlerBase.java @@ -154,6 +154,8 @@ protected Answer execute(DeleteCommand cmd) { answer = processor.deleteVolume(cmd); } else if (data.getObjectType() == DataObjectType.SNAPSHOT) { answer = processor.deleteSnapshot(cmd); + } else if (data.getObjectType() == DataObjectType.BACKUP) { + answer = processor.deleteBackup(cmd); } else { answer = new Answer(cmd, false, "unsupported type"); } diff --git a/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java index 6fe001de72c0..71c329796d1b 100755 --- a/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java @@ -52,6 +52,7 @@ import com.cloud.utils.Pair; import com.cloud.utils.UriUtils; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.HttpClientCloudStackUserAgent; import com.cloud.utils.net.Proxy; /** @@ -125,6 +126,7 @@ private GetMethod createRequest(String downloadUrl) { GetMethod request = new GetMethod(downloadUrl); request.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, myretryhandler); request.setFollowRedirects(followRedirects); + request.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); return request; } diff --git a/core/src/main/java/com/cloud/storage/template/MetalinkTemplateDownloader.java b/core/src/main/java/com/cloud/storage/template/MetalinkTemplateDownloader.java index 95ed0d1e76d0..0dad2564779c 100644 --- a/core/src/main/java/com/cloud/storage/template/MetalinkTemplateDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/MetalinkTemplateDownloader.java @@ -18,8 +18,11 @@ // package com.cloud.storage.template; + import com.cloud.storage.StorageLayer; import com.cloud.utils.UriUtils; +import com.cloud.utils.net.HttpClientCloudStackUserAgent; + import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpMethodRetryHandler; @@ -59,6 +62,7 @@ protected GetMethod createRequest(String downloadUrl) { GetMethod request = new GetMethod(downloadUrl); request.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, myretryhandler); request.setFollowRedirects(followRedirects); + request.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); if (!toFileSet) { String[] parts = downloadUrl.split("/"); String filename = parts[parts.length - 1]; diff --git a/core/src/main/java/com/cloud/storage/template/SimpleHttpMultiFileDownloader.java b/core/src/main/java/com/cloud/storage/template/SimpleHttpMultiFileDownloader.java index 8719947cb4f0..6608754073a1 100644 --- a/core/src/main/java/com/cloud/storage/template/SimpleHttpMultiFileDownloader.java +++ b/core/src/main/java/com/cloud/storage/template/SimpleHttpMultiFileDownloader.java @@ -44,6 +44,7 @@ import org.apache.commons.lang3.StringUtils; import com.cloud.storage.StorageLayer; +import com.cloud.utils.net.HttpClientCloudStackUserAgent; public class SimpleHttpMultiFileDownloader extends ManagedContextRunnable implements TemplateDownloader { private static final MultiThreadedHttpConnectionManager s_httpClientManager = new MultiThreadedHttpConnectionManager(); @@ -95,6 +96,7 @@ private GetMethod createRequest(String downloadUrl) { GetMethod request = new GetMethod(downloadUrl); request.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, retryHandler); request.setFollowRedirects(followRedirects); + request.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); return request; } @@ -141,6 +143,7 @@ private void tryAndGetTotalRemoteSize() { continue; } HeadMethod headMethod = new HeadMethod(downloadUrl); + headMethod.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); try { if (client.executeMethod(headMethod) != HttpStatus.SC_OK) { continue; diff --git a/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java index ffc67b628a7e..abe78ee5553d 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/backup/BackupAnswer.java @@ -29,6 +29,12 @@ public class BackupAnswer extends Answer { private Long virtualSize; private Map volumes; Boolean needsCleanup; + // Set by the NAS backup provider after a checkpoint/bitmap was created during this backup. + // The provider persists it in backup_details under NASBackupChainKeys.BITMAP_NAME. + private String bitmapCreated; + // Set when an incremental was requested but the agent had to fall back to a full + // (e.g. VM was stopped). Provider should record this backup as type=full. + private Boolean incrementalFallback; public BackupAnswer(final Command command, final boolean success, final String details) { super(command, success, details); @@ -68,4 +74,21 @@ public Boolean getNeedsCleanup() { public void setNeedsCleanup(Boolean needsCleanup) { this.needsCleanup = needsCleanup; } + + public String getBitmapCreated() { + return bitmapCreated; + } + + public void setBitmapCreated(String bitmapCreated) { + this.bitmapCreated = bitmapCreated; + } + + public Boolean getIncrementalFallback() { + return incrementalFallback != null && incrementalFallback; + } + + public void setIncrementalFallback(Boolean incrementalFallback) { + this.incrementalFallback = incrementalFallback; + } + } diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java new file mode 100644 index 000000000000..34b65adc2467 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.backup; + +import java.util.List; + +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class CleanupKbossBackupErrorAnswer extends Answer { + private List volumeObjectTos; + private boolean vmRunning; + + public CleanupKbossBackupErrorAnswer(Command cmd, List volumeObjectTos, boolean vmRunning) { + super(cmd, CollectionUtils.isNotEmpty(volumeObjectTos), null); + this.volumeObjectTos = volumeObjectTos; + this.vmRunning = vmRunning; + } + + public List getVolumeObjectTos() { + return volumeObjectTos; + } + + public boolean isVmRunning() { + return vmRunning; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java new file mode 100644 index 000000000000..3257851d11ba --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.KbossTO; + +import java.util.List; + +public class CleanupKbossBackupErrorCommand extends Command { + + private boolean runningVM; + + private String vmName; + + private String imageStoreUrl; + + private List kbossTOS; + + public CleanupKbossBackupErrorCommand(boolean runningVM, String vmName, String imageStoreUrl, List kbossTOS) { + this.runningVM = runningVM; + this.vmName = vmName; + this.imageStoreUrl = imageStoreUrl; + this.kbossTOS = kbossTOS; + } + + public boolean isRunningVM() { + return runningVM; + } + + public String getVmName() { + return vmName; + } + + public String getImageStoreUrl() { + return imageStoreUrl; + } + + public List getKbossTOs() { + return kbossTOS; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java new file mode 100644 index 000000000000..8a345aeba176 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossValidationCommand.java @@ -0,0 +1,49 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +import java.util.Set; + +public class CleanupKbossValidationCommand extends Command { + + private String vmName; + + private Set secondaryStorages; + + public CleanupKbossValidationCommand(String vmName, Set secondaryStorages) { + this.vmName = vmName; + this.secondaryStorages = secondaryStorages; + } + + public String getVmName() { + return vmName; + } + + public Set getSecondaryStorages() { + return secondaryStorages; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java new file mode 100644 index 000000000000..a551f246a0bb --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CompressBackupCommand.java @@ -0,0 +1,78 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; + +import java.util.List; + +public class CompressBackupCommand extends Command { + + private List backupDeltasToCompress; + + private List backupChainImageStoreUrls; + + private long minFreeStorage; + + private Backup.CompressionLibrary compressionLib; + + private int coroutines; + + private int rateLimit; + + public CompressBackupCommand(List backupDeltasToCompress, List backupChainImageStoreUrls, long minFreeStorage, Backup.CompressionLibrary compressionLib, int coroutines, int rateLimit) { + this.backupChainImageStoreUrls = backupChainImageStoreUrls; + this.backupDeltasToCompress = backupDeltasToCompress; + this.minFreeStorage = minFreeStorage; + this.compressionLib = compressionLib; + this.coroutines = coroutines; + this.rateLimit = rateLimit; + } + + public List getBackupDeltasToCompress() { + return backupDeltasToCompress; + } + + public List getBackupChainImageStoreUrls() { + return backupChainImageStoreUrls; + } + + public long getMinFreeStorage() { + return minFreeStorage; + } + + public Backup.CompressionLibrary getCompressionLib() { + return compressionLib; + } + + public int getCoroutines() { + return coroutines; + } + + public int getRateLimit() { + return rateLimit; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java new file mode 100644 index 000000000000..9f230deb8d75 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesAnswer.java @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; + +public class ConsolidateVolumesAnswer extends Answer { + + private List successfullyConsolidatedVolumes; + + public ConsolidateVolumesAnswer(Command command, boolean success, String details, List successfullyConsolidatedVolumes) { + super(command, success, details); + this.successfullyConsolidatedVolumes = successfullyConsolidatedVolumes; + } + + public List getSuccessfullyConsolidatedVolumes() { + return successfullyConsolidatedVolumes; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java new file mode 100644 index 000000000000..7b2bc2245939 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ConsolidateVolumesCommand.java @@ -0,0 +1,56 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; +import java.util.stream.Collectors; + +public class ConsolidateVolumesCommand extends Command { + + private List volumesToConsolidate; + + private List secondaryStorageUuids; + + private String vmName; + + public ConsolidateVolumesCommand(List volumesToConsolidate, List secondaryStorageUuids, String vmName) { + this.volumesToConsolidate = volumesToConsolidate.stream().map(vol -> (VolumeObjectTO)vol.getTO()).collect(Collectors.toList()); + this.secondaryStorageUuids = secondaryStorageUuids; + this.vmName = vmName; + } + + public List getVolumesToConsolidate() { + return volumesToConsolidate; + } + + public List getSecondaryStorageUuids() { + return secondaryStorageUuids; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java new file mode 100644 index 000000000000..34cf6d4ca34c --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java @@ -0,0 +1,56 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class CreateImageTransferAnswer extends Answer { + private String imageTransferId; + private String transferUrl; + + public CreateImageTransferAnswer() { + } + + public CreateImageTransferAnswer(CreateImageTransferCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public CreateImageTransferAnswer(CreateImageTransferCommand cmd, boolean success, String details, + String imageTransferId, String transferUrl) { + super(cmd, success, details); + this.imageTransferId = imageTransferId; + this.transferUrl = transferUrl; + } + + public String getImageTransferId() { + return imageTransferId; + } + + public void setImageTransferId(String imageTransferId) { + this.imageTransferId = imageTransferId; + } + + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java new file mode 100644 index 000000000000..95b56c9a9c38 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java @@ -0,0 +1,94 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class CreateImageTransferCommand extends Command { + private String transferId; + private String exportName; + private String socket; + private String direction; + private String checkpointId; + private String file; + private ImageTransfer.Backend backend; + private int idleTimeoutSeconds; + + public CreateImageTransferCommand() { + } + + private CreateImageTransferCommand(String transferId, String direction, String socket, int idleTimeoutSeconds) { + this.transferId = transferId; + this.direction = direction; + this.socket = socket; + this.idleTimeoutSeconds = idleTimeoutSeconds; + } + + public CreateImageTransferCommand(String transferId, String direction, String exportName, String socket, String checkpointId, int idleTimeoutSeconds) { + this(transferId, direction, socket, idleTimeoutSeconds); + this.backend = ImageTransfer.Backend.nbd; + this.exportName = exportName; + this.checkpointId = checkpointId; + } + + public CreateImageTransferCommand(String transferId, String direction, String socket, String file, int idleTimeoutSeconds) { + this(transferId, direction, socket, idleTimeoutSeconds); + if (direction == ImageTransfer.Direction.download.toString()) { + throw new IllegalArgumentException("File backend is only supported for upload"); + } + this.backend = ImageTransfer.Backend.file; + this.file = file; + } + + public String getExportName() { + return exportName; + } + + public String getSocket() { + return socket; + } + + public String getFile() { + return file; + } + + public ImageTransfer.Backend getBackend() { + return backend; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getDirection() { + return direction; + } + + public String getCheckpointId() { + return checkpointId; + } + + public int getIdleTimeoutSeconds() { + return idleTimeoutSeconds; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java b/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java new file mode 100644 index 000000000000..81cf6c1abfcc --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java @@ -0,0 +1,60 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import java.util.Map; + +import com.cloud.agent.api.Command; + +public class DeleteVmCheckpointCommand extends Command { + private String vmName; + private String checkpointId; + private Map diskPathUuidMap; + private boolean stoppedVM; + + public DeleteVmCheckpointCommand() { + } + + public DeleteVmCheckpointCommand(String vmName, String checkpointId, Map diskPathUuidMap, boolean stoppedVM) { + this.vmName = vmName; + this.checkpointId = checkpointId; + this.diskPathUuidMap = diskPathUuidMap; + this.stoppedVM = stoppedVM; + } + + public String getVmName() { + return vmName; + } + + public String getCheckpointId() { + return checkpointId; + } + + public Map getDiskPathUuidMap() { + return diskPathUuidMap; + } + + public boolean isStoppedVM() { + return stoppedVM; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java b/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java new file mode 100644 index 000000000000..a4cbb69611f7 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/FinalizeBackupCompressionCommand.java @@ -0,0 +1,49 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.util.List; + +public class FinalizeBackupCompressionCommand extends Command { + private boolean cleanup; + + private List backupDeltaTO; + + public FinalizeBackupCompressionCommand(boolean cleanup, List backupDeltaTO) { + this.cleanup = cleanup; + this.backupDeltaTO = backupDeltaTO; + } + + public boolean isCleanup() { + return cleanup; + } + + public List getBackupDeltaTOList() { + return backupDeltaTO; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java new file mode 100644 index 000000000000..84d9b1ff8186 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java @@ -0,0 +1,40 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class FinalizeImageTransferCommand extends Command { + private String transferId; + + public FinalizeImageTransferCommand() { + } + + public FinalizeImageTransferCommand(String transferId) { + this.transferId = transferId; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java b/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java new file mode 100644 index 000000000000..bae2fee95774 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/PrepareValidationCommand.java @@ -0,0 +1,52 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; +import java.util.Set; + +public class PrepareValidationCommand extends Command { + + private List> backupToVolumeList; + + private Set imageStoreSet; + + public PrepareValidationCommand(List> backupToVolumeList, Set imageStoreSet) { + this.backupToVolumeList = backupToVolumeList; + this.imageStoreSet = imageStoreSet; + } + + public List> getBackupToVolumeList() { + return backupToVolumeList; + } + + public Set getImageStoreSet() { + return imageStoreSet; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java index 8e68f4f1e41c..972c2eaf7bb4 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java @@ -34,9 +34,10 @@ public class RestoreBackupCommand extends Command { private List backupVolumesUUIDs; private List restoreVolumePools; private List restoreVolumePaths; + private List restoreVolumeSizes; + private List backupFiles; private String diskType; private Boolean vmExists; - private String restoreVolumeUUID; private VirtualMachine.State vmState; private Integer mountTimeout; @@ -92,6 +93,22 @@ public void setRestoreVolumePaths(List restoreVolumePaths) { this.restoreVolumePaths = restoreVolumePaths; } + public List getRestoreVolumeSizes() { + return restoreVolumeSizes; + } + + public void setRestoreVolumeSizes(List restoreVolumeSizes) { + this.restoreVolumeSizes = restoreVolumeSizes; + } + + public List getBackupFiles() { + return backupFiles; + } + + public void setBackupFiles(List backupFiles) { + this.backupFiles = backupFiles; + } + public Boolean isVmExists() { return vmExists; } @@ -116,14 +133,6 @@ public void setMountOptions(String mountOptions) { this.mountOptions = mountOptions; } - public String getRestoreVolumeUUID() { - return restoreVolumeUUID; - } - - public void setRestoreVolumeUUID(String restoreVolumeUUID) { - this.restoreVolumeUUID = restoreVolumeUUID; - } - public VirtualMachine.State getVmState() { return vmState; } diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java new file mode 100644 index 000000000000..925ceca6b3b3 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupAnswer.java @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +import java.util.Set; + +public class RestoreKbossBackupAnswer extends Answer { + + private Set secondaryStorageUuids; + + public RestoreKbossBackupAnswer(Command command, Set secondaryStorageUuids) { + super(command); + this.secondaryStorageUuids = secondaryStorageUuids; + } + + public RestoreKbossBackupAnswer(Command command, Exception e, Set secondaryStorageUuids) { + super(command, e); + this.secondaryStorageUuids = secondaryStorageUuids; + } + + public Set getSecondaryStorageUuids() { + return secondaryStorageUuids; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java new file mode 100644 index 000000000000..d173aab324a2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreKbossBackupCommand.java @@ -0,0 +1,66 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.Set; + +public class RestoreKbossBackupCommand extends Command { + + private Set deltasToRemove; + + private Set> backupAndVolumePairs; + + private Set secondaryStorageUrls; + + private boolean quickRestore; + + public RestoreKbossBackupCommand(Set deltasToRemove, Set> backupAndVolumePairs, Set secondaryStorageUrls, + boolean quickRestore) { + this.deltasToRemove = deltasToRemove; + this.backupAndVolumePairs = backupAndVolumePairs; + this.secondaryStorageUrls = secondaryStorageUrls; + this.quickRestore = quickRestore; + } + + @Override + public boolean executeInSequence() { + return false; + } + + public Set getDeltasToRemove() { + return deltasToRemove; + } + + public Set> getBackupAndVolumePairs() { + return backupAndVolumePairs; + } + + public Set getSecondaryStorageUrls() { + return secondaryStorageUrls; + } + + public boolean isQuickRestore() { + return quickRestore; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java new file mode 100644 index 000000000000..d7cbf097df90 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java @@ -0,0 +1,44 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StartBackupAnswer extends Answer { + private Long checkpointCreateTime; + + public StartBackupAnswer() { + } + + public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details, Long checkpointCreateTime) { + super(cmd, success, details); + this.checkpointCreateTime = checkpointCreateTime; + } + + public Long getCheckpointCreateTime() { + return checkpointCreateTime; + } + + public void setCheckpointCreateTime(Long checkpointCreateTime) { + this.checkpointCreateTime = checkpointCreateTime; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java new file mode 100644 index 000000000000..6f1ed6834500 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java @@ -0,0 +1,91 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import java.util.Map; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; + +public class StartBackupCommand extends Command { + private String vmName; + private String toCheckpointId; + private String fromCheckpointId; + private Long fromCheckpointCreateTime; + private String socket; + private Map diskPathUuidMap; + private boolean stoppedVM; + @LogLevel(LogLevel.Log4jLevel.Off) + private Map diskPathPassphraseMap; + + public StartBackupCommand() { + } + + public StartBackupCommand(String vmName, String toCheckpointId, String fromCheckpointId, Long fromCheckpointCreateTime, + String socket, Map diskPathUuidMap, Map passphrases, boolean stoppedVM) { + this.vmName = vmName; + this.toCheckpointId = toCheckpointId; + this.fromCheckpointId = fromCheckpointId; + this.fromCheckpointCreateTime = fromCheckpointCreateTime; + this.socket = socket; + this.diskPathUuidMap = diskPathUuidMap; + this.diskPathPassphraseMap = passphrases; + this.stoppedVM = stoppedVM; + } + + public String getVmName() { + return vmName; + } + + public String getToCheckpointId() { + return toCheckpointId; + } + + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public Long getFromCheckpointCreateTime() { + return fromCheckpointCreateTime; + } + + public String getSocket() { + return socket; + } + + public Map getDiskPathUuidMap() { + return diskPathUuidMap; + } + + public boolean isIncremental() { + return fromCheckpointId != null && !fromCheckpointId.isEmpty(); + } + + public boolean isStoppedVM() { + return stoppedVM; + } + + public Map getDiskPathPassphraseMap() { + return diskPathPassphraseMap; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java new file mode 100644 index 000000000000..d8c78d3c8807 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java @@ -0,0 +1,56 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StartNBDServerAnswer extends Answer { + private String imageTransferId; + private String transferUrl; + + public StartNBDServerAnswer() { + } + + public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details, + String imageTransferId, String transferUrl) { + super(cmd, success, details); + this.imageTransferId = imageTransferId; + this.transferUrl = transferUrl; + } + + public String getImageTransferId() { + return imageTransferId; + } + + public void setImageTransferId(String imageTransferId) { + this.imageTransferId = imageTransferId; + } + + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java new file mode 100644 index 000000000000..07e50f11fd37 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java @@ -0,0 +1,78 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; + +public class StartNBDServerCommand extends Command { + private String transferId; + private String exportName; + private String volumePath; + private String socket; + private String direction; + private String fromCheckpointId; + @LogLevel(LogLevel.Log4jLevel.Off) + private byte[] passphrase; + + public StartNBDServerCommand() { + } + + protected StartNBDServerCommand(String transferId, String exportName, String volumePath, String socket, String direction, String fromCheckpointId, byte[] passphrase) { + this.transferId = transferId; + this.socket = socket; + this.exportName = exportName; + this.volumePath = volumePath; + this.direction = direction; + this.fromCheckpointId = fromCheckpointId; + this.passphrase = passphrase; + } + + public String getExportName() { + return exportName; + } + + public String getSocket() { + return socket; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVolumePath() { + return volumePath; + } + + public String getDirection() { + return direction; + } + + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public byte[] getPassphrase() { + return passphrase; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java new file mode 100644 index 000000000000..ce977f31e005 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java @@ -0,0 +1,30 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StopBackupAnswer extends Answer { + + public StopBackupAnswer() { + } + + public StopBackupAnswer(StopBackupCommand cmd, boolean success, String details) { + super(cmd, success, details); + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java new file mode 100644 index 000000000000..d3055021e9de --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java @@ -0,0 +1,52 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class StopBackupCommand extends Command { + private String vmName; + private Long vmId; + private Long backupId; + + public StopBackupCommand() { + } + + public StopBackupCommand(String vmName, Long vmId, Long backupId) { + this.vmName = vmName; + this.vmId = vmId; + this.backupId = backupId; + } + + public String getVmName() { + return vmName; + } + + public Long getVmId() { + return vmId; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java new file mode 100644 index 000000000000..d75168a22eb2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java @@ -0,0 +1,46 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class StopNBDServerCommand extends Command { + private String transferId; + private String direction; + + public StopNBDServerCommand() { + } + + public StopNBDServerCommand(String transferId, String direction) { + this.transferId = transferId; + this.direction = direction; + } + + public String getTransferId() { + return transferId; + } + + public String getDirection() { + return direction; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java index 5402b6b24760..34f8d7b8bcdd 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java @@ -36,6 +36,17 @@ public class TakeBackupCommand extends Command { @LogLevel(LogLevel.Log4jLevel.Off) private String mountOptions; + // Incremental backup fields (NAS provider; null/empty for legacy full-only callers). + private String mode; // "full" or "incremental"; null => legacy behaviour (script default) + private String bitmapNew; // Checkpoint/bitmap name to create with this backup (timestamp-based) + private String bitmapParent; // Incremental: parent bitmap to read changes since + + // Per-volume parent backup file paths (one per VM volume, ordered by deviceId — same + // order as volumePaths). The script rebases each new qcow2 onto the matching parent. + // Backup file UUIDs differ across volumes, so a single parentPath would have rebased + // every data disk onto the root file. New callers MUST populate parentPaths. + private List parentPaths; + public TakeBackupCommand(String vmName, String backupPath) { super(); this.vmName = vmName; @@ -106,6 +117,38 @@ public void setQuiesce(Boolean quiesce) { this.quiesce = quiesce; } + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getBitmapNew() { + return bitmapNew; + } + + public void setBitmapNew(String bitmapNew) { + this.bitmapNew = bitmapNew; + } + + public String getBitmapParent() { + return bitmapParent; + } + + public void setBitmapParent(String bitmapParent) { + this.bitmapParent = bitmapParent; + } + + public List getParentPaths() { + return parentPaths; + } + + public void setParentPaths(List parentPaths) { + this.parentPaths = parentPaths; + } + @Override public boolean executeInSequence() { return true; diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java new file mode 100644 index 000000000000..7effe40686b2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeBackupHashCommand.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.util.List; + +public class TakeBackupHashCommand extends Command { + + private List backupDeltaTOList; + + private String backupUuid; + + public TakeBackupHashCommand(List backupDeltaTOList, String backupUuid) { + this.backupDeltaTOList = backupDeltaTOList; + this.backupUuid = backupUuid; + } + + public List getBackupDeltaTOList() { + return backupDeltaTOList; + } + + public String getBackupUuid() { + return backupUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java new file mode 100644 index 000000000000..1827c766f573 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupAnswer.java @@ -0,0 +1,59 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; + +import java.util.Map; + +public class TakeKbossBackupAnswer extends Answer { + + private Map mapVolumeUuidToNewVolumePath; + private Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize; + private boolean isVmConsistent = true; + + public TakeKbossBackupAnswer(Command command, boolean success, Map mapVolumeUuidToNewVolumePath, + Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize) { + super(command, success, null); + this.mapVolumeUuidToNewVolumePath = mapVolumeUuidToNewVolumePath; + this.mapVolumeUuidToDeltaPathOnSecondaryAndSize = mapVolumeUuidToDeltaPathOnSecondaryAndSize; + } + + public TakeKbossBackupAnswer(Command command, Exception e) { + super(command, e); + if (e instanceof BackupException) { + this.isVmConsistent = ((BackupException)e).isVmConsistent(); + } + } + + public Map getMapVolumeUuidToNewVolumePath() { + return mapVolumeUuidToNewVolumePath; + } + + public Map> getMapVolumeUuidToDeltaPathOnSecondaryAndSize() { + return mapVolumeUuidToDeltaPathOnSecondaryAndSize; + } + + public boolean isVmConsistent() { + return isVmConsistent; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java new file mode 100644 index 000000000000..b145fa6257b1 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/TakeKbossBackupCommand.java @@ -0,0 +1,92 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.KbossTO; + +import java.util.List; + +public class TakeKbossBackupCommand extends Command { + + private boolean quiesceVm; + + private boolean runningVM; + + private boolean endChain; + + private String vmName; + + private String imageStoreUrl; + + private List backupChainImageStoreUrls; + + private List kbossTOS; + + private boolean isolated; + + public TakeKbossBackupCommand(boolean quiesceVm, boolean runningVM, boolean endChain, String vmName, String imageStoreUrl, List backupChainImageStoreUrls, List kbossTOS, boolean isolated) { + this.quiesceVm = quiesceVm; + this.runningVM = runningVM; + this.endChain = endChain; + this.vmName = vmName; + this.imageStoreUrl = imageStoreUrl; + this.backupChainImageStoreUrls = backupChainImageStoreUrls; + this.kbossTOS = kbossTOS; + this.isolated = isolated; + } + + public boolean isQuiesceVm() { + return quiesceVm; + } + + public boolean isRunningVM() { + return runningVM; + } + + public boolean isEndChain() { + return endChain; + } + + public String getVmName() { + return vmName; + } + + public String getImageStoreUrl() { + return imageStoreUrl; + } + + public List getBackupChainImageStoreUrls() { + return backupChainImageStoreUrls; + } + + public List getKbossTOs() { + return kbossTOS; + } + + public boolean isIsolated() { + return isolated; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java new file mode 100644 index 000000000000..fcff6a8cce8c --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmAnswer.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; + +public class ValidateKbossVmAnswer extends Answer { + + private boolean bootValidated; + private String screenshotPath; + private String scriptResult; + + public ValidateKbossVmAnswer(Command command, boolean bootValidated, String screenshotPath, String scriptResult) { + super(command); + this.bootValidated = bootValidated; + this.screenshotPath = screenshotPath; + this.scriptResult = scriptResult; + } + + public boolean isBootValidated() { + return bootValidated; + } + + public String getScreenshotPath() { + return screenshotPath; + } + + public String getScriptResult() { + return scriptResult; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java new file mode 100644 index 000000000000..9e10499f53d9 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/ValidateKbossVmCommand.java @@ -0,0 +1,133 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.VirtualMachineTO; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +public class ValidateKbossVmCommand extends Command { + + private VirtualMachineTO vm; + private BackupDeltaTO backupDeltaTO; + + private String scriptToExecute; + + private String scriptArguments; + private String expectedResult; + + private Integer scriptTimeout; + private Integer bootTimeout; + private Integer screenshotWait; + + private boolean takeScreenshot; + private boolean waitForBoot; + private boolean executeScript; + + public ValidateKbossVmCommand(VirtualMachineTO vm, BackupDeltaTO backupDeltaTO) { + this.vm = vm; + this.backupDeltaTO = backupDeltaTO; + } + + public void setScriptToExecute(String scriptToExecute) { + this.scriptToExecute = scriptToExecute; + } + + public void setScriptArguments(String scriptArguments) { + this.scriptArguments = scriptArguments; + } + + public void setExpectedResult(String expectedResult) { + this.expectedResult = expectedResult; + } + + public void setScriptTimeout(Integer scriptTimeout) { + this.scriptTimeout = scriptTimeout; + } + + public void setTakeScreenshot(boolean takeScreenshot) { + this.takeScreenshot = takeScreenshot; + } + + public void setWaitForBoot(boolean waitForBoot) { + this.waitForBoot = waitForBoot; + } + + public void setExecuteScript(boolean executeScript) { + this.executeScript = executeScript; + } + + public void setBootTimeout(Integer bootTimeout) { + this.bootTimeout = bootTimeout; + } + + public void setScreenshotWait(Integer screenshotWait) { + this.screenshotWait = screenshotWait; + } + + public VirtualMachineTO getVm() { + return vm; + } + + public BackupDeltaTO getBackupDeltaTO() { + return backupDeltaTO; + } + + public String getScriptToExecute() { + return scriptToExecute; + } + + public String getScriptArguments() { + return scriptArguments; + } + + public String getExpectedResult() { + return expectedResult; + } + + public Integer getScriptTimeout() { + return scriptTimeout; + } + + public Integer getBootTimeout() { + return bootTimeout; + } + + public Integer getScreenshotWait() { + return screenshotWait; + } + + public boolean isTakeScreenshot() { + return takeScreenshot; + } + + public boolean isWaitForBoot() { + return waitForBoot; + } + + public boolean isExecuteScript() { + return executeScript; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java b/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java index a1485463eaa0..05619e5632b0 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/DirectTemplateDownloaderImpl.java @@ -21,6 +21,7 @@ import com.cloud.utils.UriUtils; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.utils.security.DigestHelper; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -33,6 +34,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; public abstract class DirectTemplateDownloaderImpl implements DirectTemplateDownloader { @@ -128,15 +130,14 @@ public void setFollowRedirects(boolean followRedirects) { */ protected File createTemporaryDirectoryAndFile(String downloadDir) { createFolder(downloadDir); - return new File(downloadDir + File.separator + getFileNameFromUrl()); + return new File(downloadDir + File.separator + getTemporaryFileName()); } /** - * Return filename from url + * Return filename from the temporary download file */ - public String getFileNameFromUrl() { - String[] urlParts = url.split("/"); - return urlParts[urlParts.length - 1]; + public String getTemporaryFileName() { + return String.format("%s.%s", UUID.randomUUID(), FilenameUtils.getExtension(url)); } @Override diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpDirectTemplateDownloader.java index c4a802ecdbc6..99b84bb645c8 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/HttpDirectTemplateDownloader.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.direct.download; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -32,6 +33,7 @@ import com.cloud.utils.Pair; import com.cloud.utils.UriUtils; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.HttpClientCloudStackUserAgent; import com.cloud.utils.storage.QCOW2Utils; import org.apache.commons.collections.MapUtils; import org.apache.commons.httpclient.HttpClient; @@ -39,6 +41,7 @@ import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.HeadMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.io.IOUtils; public class HttpDirectTemplateDownloader extends DirectTemplateDownloaderImpl { @@ -68,6 +71,7 @@ public HttpDirectTemplateDownloader(String url, Long templateId, String destPool protected GetMethod createRequest(String downloadUrl, Map headers) { GetMethod request = new GetMethod(downloadUrl); request.setFollowRedirects(this.isFollowRedirects()); + request.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); if (MapUtils.isNotEmpty(headers)) { for (String key : headers.keySet()) { request.setRequestHeader(key, headers.get(key)); @@ -111,6 +115,7 @@ protected Pair performDownload() { public boolean checkUrl(String url) { HeadMethod httpHead = new HeadMethod(url); httpHead.setFollowRedirects(this.isFollowRedirects()); + httpHead.getParams().setParameter(HttpMethodParams.USER_AGENT, HttpClientCloudStackUserAgent.CLOUDSTACK_USER_AGENT); try { int responseCode = client.executeMethod(httpHead); if (responseCode != HttpStatus.SC_OK) { diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java index 2050b9ef09f7..854c310cde9a 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/MetalinkDirectTemplateDownloader.java @@ -97,7 +97,7 @@ public Pair downloadTemplate() { DirectTemplateDownloader urlDownloader = createDownloaderForMetalinks(getUrl(), getTemplateId(), getDestPoolPath(), getChecksum(), headers, connectTimeout, soTimeout, null, temporaryDownloadPath); try { - setDownloadedFilePath(downloadDir + File.separator + getFileNameFromUrl()); + setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName()); File f = new File(getDownloadedFilePath()); if (f.exists()) { f.delete(); diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java index 21184ef07fe9..6b0959b78ffb 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/NfsDirectTemplateDownloader.java @@ -69,7 +69,7 @@ public Pair downloadTemplate() { String mount = String.format(mountCommand, srcHost + ":" + srcPath, "/mnt/" + mountSrcUuid); Script.runSimpleBashScript(mount); String downloadDir = getDestPoolPath() + File.separator + getDirectDownloadTempPath(getTemplateId()); - setDownloadedFilePath(downloadDir + File.separator + getFileNameFromUrl()); + setDownloadedFilePath(downloadDir + File.separator + getTemporaryFileName()); Script.runSimpleBashScript("cp /mnt/" + mountSrcUuid + srcPath + " " + getDownloadedFilePath()); Script.runSimpleBashScript("umount /mnt/" + mountSrcUuid); return new Pair<>(true, getDownloadedFilePath()); diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java new file mode 100644 index 000000000000..f3c43c400b2b --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.storage.clvm.command; + +import com.cloud.agent.api.Answer; + +/** + * Answer for ClvmLockTransferCommand, containing lock state information. + * This answer includes the current lock holder information when querying lock state. + */ +public class ClvmLockTransferAnswer extends Answer { + + private String currentLockHostname; + private boolean isActive; + private boolean isOpen; + private String lvAttributes; + + public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + + public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details, + String currentLockHostname, boolean isActive, boolean isOpen, + String lvAttributes) { + super(cmd, result, details); + this.currentLockHostname = currentLockHostname; + this.isActive = isActive; + this.isOpen = isOpen; + this.lvAttributes = lvAttributes; + } + + /** + * Get the hostname from lv_host. Retained for diagnostics only — + * do NOT use this to determine lock holder identity. + */ + public String getCurrentLockHostname() { + return currentLockHostname; + } + + public void setCurrentLockHostname(String currentLockHostname) { + this.currentLockHostname = currentLockHostname; + } + + /** + * Whether the LV is locally active on the queried host (lv_attr[4]=='a'). + * This is the authoritative signal for lock holder discovery via fan-out. + */ + public boolean isActive() { + return isActive; + } + + public void setActive(boolean active) { + isActive = active; + } + + /** + * Whether a process has the device file open on the queried host (lv_attr[5]=='o'). + * true means a VM is actively doing I/O on this host right now — do NOT deactivate. + */ + public boolean isOpen() { + return isOpen; + } + + public void setOpen(boolean open) { + isOpen = open; + } + + public String getLvAttributes() { + return lvAttributes; + } + + public void setLvAttributes(String lvAttributes) { + this.lvAttributes = lvAttributes; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java new file mode 100644 index 000000000000..218fb798869d --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.storage.clvm.command; + +import com.cloud.agent.api.Command; + +/** + * Command to transfer CLVM (Clustered LVM) exclusive lock between hosts. + * This enables lightweight volume migration for CLVM storage pools where volumes + * reside in the same Volume Group (VG) but need to be accessed from different hosts. + * + *

Instead of copying volume data (traditional migration), this command simply + * deactivates the LV on the source host and activates it exclusively on the destination host. + * + *

This is significantly faster (10-100x) than traditional migration and uses no network bandwidth. + */ +public class ClvmLockTransferCommand extends Command { + + /** + * Operation to perform on the CLVM volume. + * Maps to lvchange flags for LVM operations. + */ + public enum Operation { + /** Deactivate the volume on this host (-an) */ + DEACTIVATE("-an", "deactivate"), + + /** Activate the volume exclusively on this host (-aey) */ + ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"), + + /** Activate the volume in shared mode on this host (-asy) */ + ACTIVATE_SHARED("-asy", "activate in shared mode"), + + /** Query the current lock state (lvs -o lv_attr,lv_host) */ + QUERY_LOCK_STATE("query", "query lock state"); + + private final String lvchangeFlag; + private final String description; + + Operation(String lvchangeFlag, String description) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + } + + private String lvPath; + private Operation operation; + private String volumeUuid; + + public ClvmLockTransferCommand() { + // For serialization + } + + public ClvmLockTransferCommand(Operation operation, String lvPath, String volumeUuid) { + this.operation = operation; + this.lvPath = lvPath; + this.volumeUuid = volumeUuid; + setWait(65); + } + + public String getLvPath() { + return lvPath; + } + + public Operation getOperation() { + return operation; + } + + public String getVolumeUuid() { + return volumeUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java b/core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java similarity index 63% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java rename to core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java index 87aae7788e27..6cc48ea296d1 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/BackupDeleteAnswer.java @@ -1,3 +1,4 @@ +// // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information @@ -14,21 +15,22 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +// +package org.apache.cloudstack.storage.command; -package org.apache.cloudstack.quota.activationrule.presetvariables; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +public class BackupDeleteAnswer extends Answer { -@RunWith(MockitoJUnitRunner.class) -public class HostTest { + private long backupId; + + public BackupDeleteAnswer(Command command, boolean success, String details) { + super(command, success, details); + backupId = ((DeleteCommand) command).getData().getId(); + } - @Test - public void setTagsTestAddFieldTagsToCollection() { - Host variable = new Host(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); + public long getBackupId() { + return backupId; } } diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java index 6f82fa97818d..9aa6f26b5d9c 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/DeleteCommand.java @@ -24,6 +24,8 @@ public final class DeleteCommand extends StorageSubSystemCommand { private DataTO data; + private boolean deleteChain; + public DeleteCommand(final DataTO data) { super(); this.data = data; @@ -42,6 +44,14 @@ public DataTO getData() { return data; } + public void setDeleteChain(boolean deleteChain) { + this.deleteChain = deleteChain; + } + + public boolean isDeleteChain() { + return deleteChain; + } + @Override public void setExecuteInSequence(final boolean inSeq) { diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java index 174302252a55..42926d37cdfe 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/RevertSnapshotCommand.java @@ -25,6 +25,8 @@ public final class RevertSnapshotCommand extends StorageSubSystemCommand { private SnapshotObjectTO dataOnPrimaryStorage; private boolean _executeInSequence = false; + private boolean deleteChain; + public RevertSnapshotCommand(SnapshotObjectTO data, SnapshotObjectTO dataOnPrimaryStorage) { super(); this.data = data; @@ -43,6 +45,14 @@ public SnapshotObjectTO getDataOnPrimaryStorage() { return dataOnPrimaryStorage; } + public boolean isDeleteChain() { + return deleteChain; + } + + public void setDeleteChain(boolean deleteChain) { + this.deleteChain = deleteChain; + } + @Override public void setExecuteInSequence(final boolean executeInSequence) { _executeInSequence = executeInSequence; diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java index 253a2607a72c..98db75f39025 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/TemplateOrVolumePostUploadCommand.java @@ -19,6 +19,9 @@ package org.apache.cloudstack.storage.command; +import com.cloud.configuration.Resource; +import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; + public class TemplateOrVolumePostUploadCommand { long entityId; @@ -185,6 +188,11 @@ public void setDescription(String description) { this.description = description; } + public void setDefaultMaxSecondaryStorageInBytes(long defaultMaxSecondaryStorageInBytes) { + this.defaultMaxSecondaryStorageInGB = defaultMaxSecondaryStorageInBytes != Resource.RESOURCE_UNLIMITED ? + ByteScaleUtils.bytesToGibibytes(defaultMaxSecondaryStorageInBytes) : Resource.RESOURCE_UNLIMITED; + } + public void setDefaultMaxSecondaryStorageInGB(long defaultMaxSecondaryStorageInGB) { this.defaultMaxSecondaryStorageInGB = defaultMaxSecondaryStorageInGB; } diff --git a/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java b/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java index 9e6b76e467ff..f78744046f73 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java +++ b/core/src/main/java/org/apache/cloudstack/storage/command/UploadStatusCommand.java @@ -28,6 +28,7 @@ public enum EntityType { } private String entityUuid; private EntityType entityType; + private Boolean abort; protected UploadStatusCommand() { } @@ -37,6 +38,11 @@ public UploadStatusCommand(String entityUuid, EntityType entityType) { this.entityType = entityType; } + public UploadStatusCommand(String entityUuid, EntityType entityType, Boolean abort) { + this(entityUuid, entityType); + this.abort = abort; + } + public String getEntityUuid() { return entityUuid; } @@ -45,6 +51,10 @@ public EntityType getEntityType() { return entityType; } + public Boolean getAbort() { + return abort; + } + @Override public boolean executeInSequence() { return false; diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java new file mode 100644 index 000000000000..662a2d486560 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/to/BackupDeltaTO.java @@ -0,0 +1,102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.to; + +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Storage; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class BackupDeltaTO implements DataTO { + private DataStoreTO dataStoreTO; + + private Hypervisor.HypervisorType hypervisorType; + + private String path; + + private String screenshotPath; + + private Storage.ImageFormat format; + + // When set, represents the Backup ID, not the delta ID. + private long id = 0; + + public BackupDeltaTO(DataStoreTO dataStoreTO, Hypervisor.HypervisorType hypervisorType, String path) { + this.dataStoreTO = dataStoreTO; + this.hypervisorType = hypervisorType; + this.path = path; + this.format = Storage.ImageFormat.QCOW2; + } + + public BackupDeltaTO(long id, DataStoreTO dataStoreTO, Hypervisor.HypervisorType hypervisorType, String path) { + this(dataStoreTO, hypervisorType, path); + this.id = id; + } + + @Override + public DataObjectType getObjectType() { + return DataObjectType.BACKUP; + } + + @Override + public DataStoreTO getDataStore() { + return dataStoreTO; + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return hypervisorType; + } + + @Override + public String getPath() { + return path; + } + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Storage.ImageFormat getFormat() { + return this.format; + } + + public void setScreenshotPath(String screenshotPath) { + this.screenshotPath = screenshotPath; + } + + public String getScreenshotPath() { + return screenshotPath; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this, ToStringStyle.JSON_STYLE).setExcludeFieldNames("id").toString(); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java similarity index 71% rename from core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java rename to core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java index 78f23105e192..143cf6fe22bb 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/DeltaMergeTreeTO.java @@ -16,28 +16,40 @@ * specific language governing permissions and limitations * under the License. */ -package com.cloud.agent.api.storage; +package org.apache.cloudstack.storage.to; import com.cloud.agent.api.to.DataTO; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import java.util.List; -public class SnapshotMergeTreeTO { +public class DeltaMergeTreeTO { + + VolumeObjectTO volumeObjectTO; DataTO parent; DataTO child; List grandChildren; - public SnapshotMergeTreeTO(DataTO parent, DataTO child, List grandChildren) { + public DeltaMergeTreeTO(VolumeObjectTO volumeObjectTO, DataTO parent, DataTO child, List grandChildren) { + this.volumeObjectTO = volumeObjectTO; this.parent = parent; this.child = child; this.grandChildren = grandChildren; } + public VolumeObjectTO getVolumeObjectTO() { + return volumeObjectTO; + } + public DataTO getParent() { return parent; } + public void setParent(DataTO parent) { + this.parent = parent; + } + public DataTO getChild() { return child; } @@ -52,6 +64,6 @@ public void addGrandChild(DataTO grandChild) { @Override public String toString() { - return ReflectionToStringBuilder.toString(this); + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); } } diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java new file mode 100644 index 000000000000..0280ffa27a1f --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java @@ -0,0 +1,101 @@ +package org.apache.cloudstack.storage.to; +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class KbossTO { + + private String pathBackupParentOnSecondary; + private VolumeObjectTO volumeObjectTO; + private String deltaPathOnPrimary; + private String parentDeltaPathOnPrimary; + private String deltaPathOnSecondary; + + private DeltaMergeTreeTO deltaMergeTreeTO; + + private List vmSnapshotDeltaPaths; + + public KbossTO(VolumeObjectTO volumeObjectTO, List snapshotDataStoreVOs) { + this.volumeObjectTO = volumeObjectTO; + this.vmSnapshotDeltaPaths = snapshotDataStoreVOs.stream().map(SnapshotDataStoreVO::getInstallPath).collect(Collectors.toList()); + } + + public KbossTO(VolumeObjectTO volumeObjectTO, String deltaPathOnPrimary, String deltaPathOnSecondary) { + this.volumeObjectTO = volumeObjectTO; + this.deltaPathOnPrimary = deltaPathOnPrimary; + this.deltaPathOnSecondary = deltaPathOnSecondary; + } + + public String getPathBackupParentOnSecondary() { + return pathBackupParentOnSecondary; + } + + public VolumeObjectTO getVolumeObjectTO() { + return volumeObjectTO; + } + + public DeltaMergeTreeTO getDeltaMergeTreeTO() { + return deltaMergeTreeTO; + } + + public List getVmSnapshotDeltaPaths() { + return vmSnapshotDeltaPaths; + } + + public String getDeltaPathOnPrimary() { + return deltaPathOnPrimary; + } + + public String getDeltaPathOnSecondary() { + return deltaPathOnSecondary; + } + + public String getParentDeltaPathOnPrimary() { + return parentDeltaPathOnPrimary; + } + + public void setParentDeltaPathOnPrimary(String parentDeltaPathOnPrimary) { + this.parentDeltaPathOnPrimary = parentDeltaPathOnPrimary; + } + + public void setPathBackupParentOnSecondary(String pathBackupParentOnSecondary) { + this.pathBackupParentOnSecondary = pathBackupParentOnSecondary; + } + + public void setDeltaMergeTreeTO(DeltaMergeTreeTO deltaMergeTreeTO) { + this.deltaMergeTreeTO = deltaMergeTreeTO; + } + + public void setDeltaPathOnPrimary(String deltaPathOnPrimary) { + this.deltaPathOnPrimary = deltaPathOnPrimary; + } + + public void setDeltaPathOnSecondary(String deltaPathOnSecondary) { + this.deltaPathOnSecondary = deltaPathOnSecondary; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java index 827403ac5ef8..df98149faab6 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/VolumeObjectTO.java @@ -32,11 +32,12 @@ import com.cloud.storage.Volume; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Set; -public class VolumeObjectTO extends DownloadableObjectTO implements DataTO { +public class VolumeObjectTO extends DownloadableObjectTO implements DataTO, Serializable { private String uuid; private Volume.Type volumeType; private DataStoreTO dataStore; diff --git a/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml index 175d45e26752..fcbcb18c2bdf 100644 --- a/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml +++ b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml @@ -29,4 +29,9 @@ + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 01c568d78916..cf43b8527a97 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -339,6 +339,10 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> + + + @@ -366,4 +370,9 @@ + + + + + diff --git a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties b/core/src/main/resources/META-INF/cloudstack/dns/module.properties similarity index 94% rename from plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties rename to core/src/main/resources/META-INF/cloudstack/dns/module.properties index dcfe8d3537ff..a2bb467be751 100644 --- a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/module.properties +++ b/core/src/main/resources/META-INF/cloudstack/dns/module.properties @@ -1,3 +1,4 @@ +# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -14,5 +15,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -name=host-allocator-random -parent=allocator +# + +name=dns +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml new file mode 100644 index 000000000000..27cac9400284 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/kms/module.properties b/core/src/main/resources/META-INF/cloudstack/kms/module.properties new file mode 100644 index 000000000000..98e38d7cd8f6 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/kms/module.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +name=kms +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/kms/spring-core-lifecycle-kms-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/kms/spring-core-lifecycle-kms-context-inheritable.xml new file mode 100644 index 000000000000..9226eef8fc1a --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/kms/spring-core-lifecycle-kms-context-inheritable.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java index 6d4c6234c424..4ddadab999a4 100644 --- a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java +++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java @@ -235,7 +235,7 @@ protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() { lbs.toArray(arrayLbs); final NicTO nic = new NicTO(); - final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false); + final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false, 0L); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2"); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java index 4196587cc3f2..ed819bb7c68f 100644 --- a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java +++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java @@ -779,7 +779,7 @@ protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand1() { final LoadBalancerTO[] arrayLbs = new LoadBalancerTO[lbs.size()]; lbs.toArray(arrayLbs); final NicTO nic = new NicTO(); - final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false); + final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, null, "1000", false, 50000L); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2"); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); return cmd; @@ -795,7 +795,7 @@ protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand2() { lbs.toArray(arrayLbs); final NicTO nic = new NicTO(); nic.setIp("10.1.10.2"); - final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, Long.valueOf(1), "1000", false); + final LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(arrayLbs, "64.10.2.10", "10.1.10.2", "192.168.1.2", nic, Long.valueOf(1), "1000", false, 50000L); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, "10.1.10.2"); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); return cmd; diff --git a/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java index 72361c2880ed..073f976719b5 100644 --- a/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java +++ b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java @@ -79,13 +79,14 @@ public void testGenerateConfigurationLoadBalancerConfigCommand() { LoadBalancerTO[] lba = new LoadBalancerTO[1]; lba[0] = lb; HAProxyConfigurator hpg = new HAProxyConfigurator(); - LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false); + LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L); String result = genConfig(hpg, cmd); assertTrue("keepalive disabled should result in 'option httpclose' in the resulting haproxy config", result.contains("\toption httpclose")); - cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true); + cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 0L); result = genConfig(hpg, cmd); assertTrue("keepalive enabled should result in 'no option httpclose' in the resulting haproxy config", result.contains("\tno option httpclose")); + // TODO // create lb command // setup tests for @@ -93,6 +94,27 @@ public void testGenerateConfigurationLoadBalancerConfigCommand() { // httpmode } + /** + * Test method for {@link com.cloud.network.HAProxyConfigurator#generateConfiguration(com.cloud.agent.api.routing.LoadBalancerConfigCommand)}. + */ + @Test + public void testGenerateConfigurationLoadBalancerIdleTimeoutConfigCommand() { + LoadBalancerTO lb = new LoadBalancerTO("1", "10.2.0.1", 80, "http", "bla", false, false, false, null); + LoadBalancerTO[] lba = new LoadBalancerTO[1]; + lba[0] = lb; + HAProxyConfigurator hpg = new HAProxyConfigurator(); + + LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 0L); + String result = genConfig(hpg, cmd); + assertTrue("idleTimeout of 0 should not generate 'timeout server' in the resulting haproxy config", !result.contains("\ttimeout server")); + assertTrue("idleTimeout of 0 should not generate 'timeout client' in the resulting haproxy config", !result.contains("\ttimeout client")); + + cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true, 1234L); + result = genConfig(hpg, cmd); + assertTrue("idleTimeout of 1234 should result in 'timeout server 1234' in the resulting haproxy config", result.contains("\ttimeout server 1234")); + assertTrue("idleTimeout of 1234 should result in 'timeout client 1234' in the resulting haproxy config", result.contains("\ttimeout client 1234")); + } + /** * Test method for {@link com.cloud.network.HAProxyConfigurator#generateConfiguration(com.cloud.agent.api.routing.LoadBalancerConfigCommand)}. */ @@ -106,7 +128,7 @@ public void testGenerateConfigurationLoadBalancerProxyProtocolConfigCommand() { LoadBalancerTO[] lba = new LoadBalancerTO[1]; lba[0] = lb; HAProxyConfigurator hpg = new HAProxyConfigurator(); - LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false); + LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L); String result = genConfig(hpg, cmd); assertTrue("'send-proxy' should result if protocol is 'tcp-proxy'", result.contains("send-proxy")); } @@ -118,7 +140,7 @@ public void generateConfigurationTestWithCidrList() { LoadBalancerTO[] lba = new LoadBalancerTO[1]; lba[0] = lb; HAProxyConfigurator hpg = new HAProxyConfigurator(); - LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false); + LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L); String result = genConfig(hpg, cmd); Assert.assertTrue(result.contains("acl network_allowed src 1.1.1.1 2.2.2.2/24 \n\ttcp-request connection reject if !network_allowed")); } @@ -131,7 +153,7 @@ public void generateConfigurationTestWithSslCert() { LoadBalancerTO[] lba = new LoadBalancerTO[1]; lba[0] = lb; HAProxyConfigurator hpg = new HAProxyConfigurator(); - LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false); + LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false, 0L); String result = genConfig(hpg, cmd); Assert.assertTrue(result.contains("bind 10.2.0.1:443 ssl crt /etc/cloudstack/ssl/10_2_0_1-443.pem")); } diff --git a/debian/changelog b/debian/changelog index 02251137e9d0..41f97748a0b5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,13 +2,19 @@ cloudstack (4.23.0.0-SNAPSHOT) unstable; urgency=low * Update the version to 4.23.0.0-SNAPSHOT - -- the Apache CloudStack project Thu, 30 Oct 2025 19:23:55 +0530 + -- the Apache CloudStack project Fri, 22 May 2026 10:20:00 -0300 + +cloudstack (4.22.1.0) unstable; urgency=low + + * Update the version to 4.22.1.0 -cloudstack (4.23.0.0-SNAPSHOT-SNAPSHOT) unstable; urgency=low + -- the Apache CloudStack project Mon, 11 May 2026 20:26:07 +0530 - * Update the version to 4.23.0.0-SNAPSHOT-SNAPSHOT +cloudstack (4.22.0.0) unstable; urgency=low - -- the Apache CloudStack project Thu, Aug 28 11:58:36 2025 +0530 + * Update the version to 4.22.0.0 + + -- the Apache CloudStack project Thu, 30 Oct 2025 19:23:55 +0530 cloudstack (4.21.0.0) unstable; urgency=low diff --git a/debian/cloudstack-common.postinst b/debian/cloudstack-common.postinst index aa99edaee064..b11e6a3fe502 100644 --- a/debian/cloudstack-common.postinst +++ b/debian/cloudstack-common.postinst @@ -19,14 +19,14 @@ set -e CLOUDUTILS_DIR="/usr/share/pyshared/" -DIST_DIR=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))") -if which pycompile >/dev/null 2>&1; then - pycompile -p cloudstack-common -fi +# distutils was removed in Python 3.12 (Ubuntu 24.04); the Debian/Ubuntu-patched +# sysconfig 'deb_system' scheme gives the same /usr/lib/python3/dist-packages path. +DIST_DIR=$(python3 -c "import sysconfig; print(sysconfig.get_path('platlib', 'deb_system'))") -if which pycompile >/dev/null 2>&1; then - pycompile -p cloudstack-common /usr/share/cloudstack-common +if command -v py3compile >/dev/null 2>&1; then + py3compile -p cloudstack-common 2>/dev/null || echo "Warning: py3compile failed for cloudstack-common" >&2 + py3compile -p cloudstack-common /usr/share/cloudstack-common 2>/dev/null || echo "Warning: py3compile failed for cloudstack-common (/usr/share/cloudstack-common)" >&2 fi cp $CLOUDUTILS_DIR/cloud_utils.py $DIST_DIR diff --git a/debian/control b/debian/control index 2b8ce929c639..cdf663ef8906 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,7 @@ Description: CloudStack server library Package: cloudstack-agent Architecture: all -Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat +Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat, python3-libnbd, socat Recommends: init-system-helpers Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts Description: CloudStack agent diff --git a/debian/rules b/debian/rules index 842fc2408af7..327447823308 100755 --- a/debian/rules +++ b/debian/rules @@ -95,6 +95,7 @@ override_dh_auto_install: # nast hack for a couple of configuration files mv $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server/cloudstack-limits.conf $(DESTDIR)/$(SYSCONFDIR)/security/limits.d/ mv $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server/cloudstack-sudoers $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) + sed -i '/requiretty/d' $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) chmod 0440 $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/$(PACKAGE) install -D client/target/utilities/bin/cloud-update-xenserver-licenses $(DESTDIR)/usr/bin/cloudstack-update-xenserver-licenses diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 702404546894..c7238a513693 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -59,6 +59,8 @@ */ public interface VirtualMachineManager extends Manager { + String KVM_BLANK_VM_TEMPLATE_NAME = "kvm-blank-vm-template"; + ConfigKey ExecuteInSequence = new ConfigKey<>("Advanced", Boolean.class, "execute.in.sequence.hypervisor.commands", "false", "If set to true, start, stop, reboot, copy and migrate commands will be serialized on the agent side. If set to false the commands are executed in parallel. Default value is false.", false); @@ -112,6 +114,8 @@ public interface VirtualMachineManager extends Manager { interface Topics { String VM_POWER_STATE = "vm.powerstate"; + String VM_LIFECYCLE_STATE = "vm.lifecycle.state"; + String VM_ACTION = "vm.action"; } /** @@ -181,15 +185,6 @@ void orchestrateStart(String vmUuid, Map pa void advanceReboot(String vmUuid, Map params) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, OperationTimedoutException; - /** - * Check to see if a virtual machine can be upgraded to the given service offering - * - * @param vm - * @param offering - * @return true if the host can handle the upgrade, false otherwise - */ - boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering); - VirtualMachine findById(long vmId); void storageMigration(String vmUuid, Map volumeToPool); @@ -230,6 +225,8 @@ NicProfile addVmToNetwork(VirtualMachine vm, Network network, NicProfile request Boolean updateDefaultNicForVM(VirtualMachine vm, Nic nic, Nic defaultNic); + boolean updateVmNic(VirtualMachine vm, Nic nic, Boolean enabled) throws ResourceUnavailableException; + /** * @param vm * @param network @@ -319,4 +316,8 @@ void checkDeploymentPlan(VirtualMachine virtualMachine, VirtualMachineTemplate t ServiceOffering serviceOffering, Account systemAccount, DeploymentPlan plan) throws InsufficientServerCapacityException; + boolean isBlankInstanceDefaultTemplate(VirtualMachineTemplate template); + + boolean isBlankInstance(VirtualMachineTemplate template); + } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 947cbd8e6182..109a44488ec4 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -91,6 +91,10 @@ public interface NetworkOrchestrationService { ConfigKey NetworkThrottlingRate = new ConfigKey<>("Network", Integer.class, NetworkThrottlingRateCK, "200", "Default data transfer rate in megabits per second allowed in network.", true, ConfigKey.Scope.Zone); + ConfigKey DhcpLeaseTimeout = new ConfigKey<>("Network", Integer.class, "dhcp.lease.timeout", "0", + "DHCP lease time in seconds for VMs. Use 0 for infinite lease time (default). A non-zero value sets the lease duration in seconds.", + true, ConfigKey.Scope.Zone); + ConfigKey PromiscuousMode = new ConfigKey<>("Advanced", Boolean.class, "network.promiscuous.mode", "false", "Whether to allow or deny promiscuous mode on NICs for applicable network elements such as for vswitch/dvswitch portgroups.", true); @@ -122,6 +126,17 @@ public interface NetworkOrchestrationService { "Load Balancer(haproxy) maximum number of concurrent connections(global max)", true, Scope.Global); + ConfigKey NETWORK_LB_HAPROXY_IDLE_TIMEOUT = new ConfigKey<>( + "Network", + Long.class, + "network.loadbalancer.haproxy.idle.timeout", + "50000", + "Load Balancer(haproxy) idle timeout in milliseconds. Use 0 for infinite.", + true, + Scope.Global); + + ConfigKey VmNetworkThrottlingRate = new ConfigKey("Network", Integer.class, "vm.network.throttling.rate", "200", + "Default data transfer rate in megabits per second allowed in User vm's default network.", true, ConfigKey.Scope.Zone); List setupNetwork(Account owner, NetworkOffering offering, DeploymentPlan plan, String name, String displayText, boolean isDefault) throws ConcurrentOperationException; @@ -130,6 +145,8 @@ List setupNetwork(Account owner, NetworkOffering offering, Ne boolean errorIfAlreadySetup, Long domainId, ACLType aclType, Boolean subdomainAccess, Long vpcId, Boolean isDisplayNetworkEnabled) throws ConcurrentOperationException; + boolean isIsolationMethodNetworkExtension(Long networkOfferingId); + void allocate(VirtualMachineProfile vm, LinkedHashMap> networks, Map> extraDhcpOptions) throws InsufficientCapacityException, ConcurrentOperationException; @@ -212,6 +229,11 @@ Network createGuestNetwork(long networkOfferingId, String name, String displayTe Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6, String ip4Dns1, String ip4Dns2, String ip6Dns1, String ip6Dns2, Pair vrIfaceMTUs, Integer networkCidrSize) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException; + Network createGuestNetwork(long networkOfferingId, String name, String displayText, String gateway, String cidr, String vlanId, boolean bypassVlanOverlapCheck, String networkDomain, Account owner, + Long domainId, PhysicalNetwork physicalNetwork, long zoneId, ACLType aclType, Boolean subdomainAccess, Long vpcId, String ip6Gateway, String ip6Cidr, + Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6, + String ip4Dns1, String ip4Dns2, String ip6Dns1, String ip6Dns2, Pair vrIfaceMTUs, Integer networkCidrSize, boolean keepMacAddressOnPublicNic) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException; + UserDataServiceProvider getPasswordResetProvider(Network network); UserDataServiceProvider getSSHKeyResetProvider(Network network); @@ -310,7 +332,7 @@ void implementNetworkElementsAndResources(DeployDestination dest, ReservationCon void removeDhcpServiceInSubnet(Nic nic); - boolean resourceCountNeedsUpdate(NetworkOffering ntwkOff, ACLType aclType); + boolean isResourceCountUpdateNeeded(NetworkOffering networkOffering); void prepareAllNicsForMigration(VirtualMachineProfile vm, DeployDestination dest); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index 6f8c46304567..141596407fe8 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -120,9 +120,9 @@ VolumeInfo moveVolume(VolumeInfo volume, long destPoolDcId, Long destPoolPodId, void destroyVolume(Volume volume); DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, - Account owner, Long deviceId); + Account owner, Long deviceId, Long kmsKeyId, boolean incrementResourceCount); - VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volume, HypervisorType rootDiskHyperType, StoragePool storagePool) throws NoTransitionException; + VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volume, HypervisorType rootDiskHyperType, StoragePool storagePool, Long clusterId, Long podId) throws NoTransitionException; void release(VirtualMachineProfile profile); @@ -150,7 +150,7 @@ DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Lon * Allocate a volume or multiple volumes in case of template is registered with the 'deploy-as-is' option, allowing multiple disks */ List allocateTemplatedVolumes(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner, Volume volume, Snapshot snapshot); + Account owner, Long kmsKeyId, Volume volume, Snapshot snapshot); String getVmNameFromVolumeId(long volumeId); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java index 6be71b3cb250..887aeaef0736 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java @@ -71,7 +71,7 @@ VirtualMachineEntity createVirtualMachine(@QueryParam("id") String id, @QueryPar @QueryParam("network-nic-map") Map> networkNicMap, @QueryParam("deploymentplan") DeploymentPlan plan, @QueryParam("root-disk-size") Long rootDiskSize, @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, @QueryParam("datadisktemplate-diskoffering-map") Map datadiskTemplateToDiskOfferingMap, @QueryParam("disk-offering-id") Long diskOfferingId, - @QueryParam("root-disk-offering-id") Long rootDiskOfferingId, List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; + @QueryParam("root-disk-offering-id") Long rootDiskOfferingId, @QueryParam("root-disk-kms-key-id") Long rootDiskKmsKeyId, List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; @POST VirtualMachineEntity createVirtualMachineFromScratch(@QueryParam("id") String id, @QueryParam("owner") String owner, @QueryParam("iso-id") String isoId, @@ -80,7 +80,7 @@ VirtualMachineEntity createVirtualMachineFromScratch(@QueryParam("id") String id @QueryParam("compute-tags") List computeTags, @QueryParam("root-disk-tags") List rootDiskTags, @QueryParam("network-nic-map") Map> networkNicMap, @QueryParam("deploymentplan") DeploymentPlan plan, @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, @QueryParam("disk-offering-id") Long diskOfferingId, - @QueryParam("data-disks-offering-info") List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; + @QueryParam("root-disk-kms-key-id") Long rootDiskKmsKeyId, @QueryParam("data-disks-offering-info") List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; @POST NetworkEntity createNetwork(String id, String name, String domainName, String cidr, String gateway); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java index 41c1d9407454..7ff206db3c99 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java @@ -29,6 +29,11 @@ public interface DataStoreProvider { String SAMPLE_IMAGE = "Sample"; String SMB = "NFS"; String DEFAULT_PRIMARY = "DefaultPrimary"; + /** + * Primary storage provider name for NetApp ONTAP ({@code OntapPrimaryDatastoreProvider#getName}). + * Single canonical value; use this instead of duplicating the string across modules. + */ + String ONTAP_PLUGIN_NAME = "NetApp ONTAP"; enum DataStoreProviderType { PRIMARY, IMAGE, ImageCache, OBJECT diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java index 269eb4f1c213..2f8d57171bc9 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/TemplateService.java @@ -67,6 +67,12 @@ public TemplateInfo getTemplate() { void handleTemplateSync(DataStore store); + void enforceSecStorageCopyLimit(long templateId, long zoneId); + + boolean canCopyTemplateToImageStore(long templateId, long zoneId); + + void replicateTemplateUpToCap(long templateId, long zoneId); + void downloadBootstrapSysTemplate(DataStore store); void addSystemVMTemplatesToSecondary(DataStore store); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java index 8b0171870765..4937edd33d1f 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java @@ -103,4 +103,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume { List getCheckpointPaths(); Set getCheckpointImageStoreUrls(); + + /** + * Gets the destination host ID hint for CLVM volume creation. + * This is used to route volume creation commands to the specific host where the VM will be deployed. + * Only applicable for CLVM storage pools to avoid shared mode activation. + * + * @return The host ID where the volume should be created, or null if not set + */ + Long getDestinationHostId(); + + /** + * Sets the destination host ID hint for CLVM volume creation. + * This should be set before volume creation when the destination host is known. + * + * @param hostId The host ID where the volume should be created + */ + void setDestinationHostId(Long hostId); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java index 682473ec94fc..a7d82d0b9628 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java @@ -30,6 +30,7 @@ import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.DiskOffering; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -123,4 +124,71 @@ boolean copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(ObjectInD void checkAndRepairVolumeBasedOnConfig(DataObject dataObject, Host host); void validateChangeDiskOfferingEncryptionType(long existingDiskOfferingId, long newDiskOfferingId); + + /** + * Transfers exclusive lock for a volume on cluster-based storage (e.g., CLVM/CLVM_NG) from one host to another. + * This is used for storage that requires host-level lock management for volumes on shared storage pools. + * For non-CLVM pool types, this method returns false without taking action. + * + * @param volume The volume to transfer lock for + * @param sourceHostId Host currently holding the exclusive lock + * @param destHostId Host to receive the exclusive lock + * @return true if lock transfer succeeded or was not needed, false if it failed + */ + boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId); + + /** + * Finds which host currently has the exclusive lock on a CLVM volume. + * Checks in order: explicit lock tracking, attached VM's host, or first available cluster host. + * + * @param volume The CLVM volume + * @return Host ID that has the exclusive lock, or null if cannot be determined + */ + Long findVolumeLockHost(VolumeInfo volume); + + /** + * Performs lightweight CLVM lock migration for a volume to a target host. + * This transfers the LVM exclusive lock without copying data (CLVM volumes are on shared cluster storage). + * If the volume already has the lock on the destination host, no action is taken. + * + * @param volume The volume to migrate lock for + * @param destHostId Destination host ID + * @return Updated VolumeInfo after lock migration + */ + VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId); + + /** + * Checks if both storage pools are CLVM type (CLVM or CLVM_NG). + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @return true if both pools are CLVM type (CLVM or CLVM_NG) + */ + boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType); + + /** + * Determines if CLVM lock transfer is required when a volume is already on the correct storage pool. + * + * @param volumeToAttach The volume being attached + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM's existing volume + * @param volumePoolId Storage pool ID for the volume + * @param vmPoolId Storage pool ID for the VM's existing volume + * @param vmHostId VM's current host ID (or last host ID if stopped) + * @return true if CLVM lock transfer is needed + */ + boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId); + + /** + * Determines if lightweight CLVM migration is needed instead of full data copy. + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @param volumePoolPath Storage pool path for the volume + * @param vmPoolPath Storage pool path for the VM + * @return true if lightweight migration should be used + */ + boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath); } diff --git a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java index 0aa5805b1601..4d63fae33560 100644 --- a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java +++ b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java @@ -54,6 +54,10 @@ public interface AgentManager { "This timeout overrides the wait global config. This holds a comma separated key value pairs containing timeout (in seconds) for specific commands. " + "For example: DhcpEntryCommand=600, SavePasswordCommand=300, VmDataCommand=300", false); + ConfigKey KVMHostDiscoverySshPort = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class, + "kvm.host.discovery.ssh.port", String.valueOf(Host.DEFAULT_SSH_PORT), "SSH port used for KVM host discovery and any other operations on host (using SSH)." + + " Please note that this is applicable when port is not defined through host url while adding the KVM host.", true, ConfigKey.Scope.Cluster); + enum TapAgentsAction { Add, Del, Contains, } @@ -172,4 +176,6 @@ enum TapAgentsAction { void propagateChangeToAgents(Map params); boolean transferDirectAgentsFromMS(String fromMsUuid, long fromMsId, long timeoutDurationInMs, boolean excludeHostsInMaintenance); + + int getHostSshPort(HostVO host); } diff --git a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java index 4c81c7359f25..abaf6ea967d0 100644 --- a/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java +++ b/engine/components-api/src/main/java/com/cloud/capacity/CapacityManager.java @@ -133,6 +133,20 @@ public interface CapacityManager { "capacity.calculate.workers", "1", "Number of worker threads to be used for capacities calculation", true); + ConfigKey KvmMemoryDynamicScalingCapacity = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, + Integer.class, "kvm.memory.dynamic.scaling.capacity", "0", + "Defines the maximum memory capacity in MiB for which VMs can be dynamically scaled to with KVM. " + + "The 'kvm.memory.dynamic.scaling.capacity' setting's value will be used to define the value of the " + + "'' element of domain XMLs. If it is set to a value less than or equal to '0', then the host's memory capacity will be considered.", + true, ConfigKey.Scope.Cluster); + + ConfigKey KvmCpuDynamicScalingCapacity = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, + Integer.class, "kvm.cpu.dynamic.scaling.capacity", "0", + "Defines the maximum vCPU capacity for which VMs can be dynamically scaled to with KVM. " + + "The 'kvm.cpu.dynamic.scaling.capacity' setting's value will be used to define the value of the " + + "'' element of domain XMLs. If it is set to a value less than or equal to '0', then the host's CPU cores capacity will be considered.", + true, ConfigKey.Scope.Cluster); + public boolean releaseVmCapacity(VirtualMachine vm, boolean moveFromReserved, boolean moveToReservered, Long hostId); void allocateVmCapacity(VirtualMachine vm, boolean fromLastHost); diff --git a/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java b/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java index ddc8153d7398..53bfcce27038 100644 --- a/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java +++ b/engine/components-api/src/main/java/com/cloud/ha/HighAvailabilityManager.java @@ -21,6 +21,7 @@ import com.cloud.deploy.DeploymentPlanner; import com.cloud.host.HostVO; import com.cloud.host.Status; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.utils.component.Manager; import com.cloud.vm.VMInstanceVO; import org.apache.cloudstack.framework.config.ConfigKey; @@ -32,6 +33,8 @@ */ public interface HighAvailabilityManager extends Manager { + List LIBVIRT_STORAGE_POOL_TYPES_WITH_HA_SUPPORT = List.of(StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint); + ConfigKey ForceHA = new ConfigKey<>("Advanced", Boolean.class, "force.ha", "false", "Force High-Availability to happen even if the VM says no.", true, Cluster); @@ -72,10 +75,10 @@ public interface HighAvailabilityManager extends Manager { + " which are registered for the HA event that were successful and are now ready to be purged.", true, Cluster); - public static final ConfigKey KvmHAFenceHostIfHeartbeatFailsOnStorage = new ConfigKey<>("Advanced", Boolean.class, "kvm.ha.fence.on.storage.heartbeat.failure", "false", + ConfigKey KvmHAFenceHostIfHeartbeatFailsOnStorage = new ConfigKey<>("Advanced", Boolean.class, "kvm.ha.fence.on.storage.heartbeat.failure", "false", "Proceed fencing the host even the heartbeat failed for only one storage pool", false, ConfigKey.Scope.Zone); - public enum WorkType { + enum WorkType { Migration, // Migrating VMs off of a host. Stop, // Stops a VM for storage pool migration purposes. This should be obsolete now. CheckStop, // Checks if a VM has been stopped. diff --git a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java index b1cad20b19ec..454cb10a2f2b 100644 --- a/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/IpAddressManager.java @@ -288,4 +288,6 @@ List listAvailablePublicIps(final long dcId, PublicIpQuarantine updatePublicIpAddressInQuarantine(Long quarantineProcessId, Date endDate); void updateSourceNatIpAddress(IPAddressVO requestedIp, List userIps) throws Exception; + + Long getPreferredNetworkIdForPublicIpRuleAssignment(IpAddress ip, Long networkId); } diff --git a/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java b/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java index 669456cbdcc2..d8011e9ade12 100644 --- a/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/lb/LoadBalancingRulesManager.java @@ -39,7 +39,7 @@ public interface LoadBalancingRulesManager { LoadBalancer createPublicLoadBalancer(String xId, String name, String description, int srcPort, int destPort, long sourceIpId, String protocol, String algorithm, - boolean openFirewall, CallContext caller, String lbProtocol, Boolean forDisplay, String cidrList) throws NetworkRuleConflictException; + boolean openFirewall, CallContext caller, String lbProtocol, Boolean forDisplay, String cidrList, Long networkId) throws NetworkRuleConflictException; boolean removeAllLoadBalanacersForIp(long ipId, Account caller, long callerUserId); diff --git a/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java b/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java index 4d8270ca078d..980606176024 100644 --- a/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java +++ b/engine/components-api/src/main/java/com/cloud/network/rules/StaticNatRuleImpl.java @@ -80,10 +80,15 @@ public long getDomainId() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } + @Override + public Long getVpcId() { + return null; + } + @Override public long getId() { return id; diff --git a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java index e7f41d079a74..792a3a6b397f 100644 --- a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java @@ -211,4 +211,9 @@ public interface VpcManager { void reconfigStaticNatForVpcVr(Long vpcId); boolean applyStaticRouteForVpcVpnIfNeeded(Long vpcId, boolean updateAllVpn) throws ResourceUnavailableException; + + /** + * Returns true if the network is part of a VPC, and the VPC is created from conserve mode enabled VPC offering + */ + boolean isNetworkOnVpcEnabledConserveMode(Network network); } diff --git a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java index f4651237f328..4767e86e8ab8 100755 --- a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java +++ b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java @@ -122,6 +122,8 @@ public interface ResourceManager extends ResourceService, Configurable { public boolean executeUserRequest(long hostId, ResourceState.Event event) throws AgentUnavailableException; + boolean executeUserRequest(long hostId, ResourceState.Event event, boolean isForced, boolean isForceDeleteStorage) throws AgentUnavailableException; + boolean resourceStateTransitTo(Host host, Event event, long msId) throws NoTransitionException; boolean umanageHost(long hostId); @@ -167,6 +169,8 @@ public interface ResourceManager extends ResourceService, Configurable { public HostVO findHostByGuid(String guid); + HostVO findHostByGuidPrefix(String guid); + public HostVO findHostByName(String name); HostStats getHostStatistics(Host host); diff --git a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index 3c62738f9ed5..032fcbe76dce 100644 --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@ -195,6 +195,14 @@ public interface StorageManager extends StorageService { true, ConfigKey.Scope.StoragePool, null); + ConfigKey XenserverCreateCloneFull = new ConfigKey<>(Boolean.class, + "xenserver.create.full.clone", + "Storage", + "false", + "If set to true, creates VMs as full clones on XenServer hypervisor (uses VDI.copy instead of VDI.clone, removing the linked-clone parent relationship).", + true, + ConfigKey.Scope.StoragePool, + null); ConfigKey VmwareAllowParallelExecution = new ConfigKey<>(Boolean.class, "vmware.allow.parallel.command.execution", "Advanced", @@ -233,6 +241,9 @@ public interface StorageManager extends StorageService { "while adding a new Secondary Storage. If the copy operation fails, the system falls back to downloading the template from the source URL.", true, ConfigKey.Scope.Zone, null); + ConfigKey AgentMaxDataMigrationWaitTime = new ConfigKey<>("Advanced", Integer.class, "agent.max.data.migration.wait.time", "3600", + "The maximum time (in seconds) that the secondary storage data migration command sent to the KVM Agent will be executed before a timeout occurs.", true, ConfigKey.Scope.Cluster); + /** * should we execute in sequence not involving any storages? * @return true if commands should execute in sequence diff --git a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java index f1891c774edd..8c11fe6c93ad 100644 --- a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java +++ b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java @@ -45,6 +45,8 @@ public interface TemplateManager { static final String AllowPublicUserTemplatesCK = "allow.public.user.templates"; static final String TemplatePreloaderPoolSizeCK = "template.preloader.pool.size"; + static final String PublicTemplateSecStorageCopyCK = "secstorage.public.template.copy.max"; + static final String PrivateTemplateSecStorageCopyCK = "secstorage.private.template.copy.max"; static final ConfigKey AllowPublicUserTemplates = new ConfigKey("Advanced", Boolean.class, AllowPublicUserTemplatesCK, "true", "If false, users will not be able to create public Templates.", true, ConfigKey.Scope.Account); @@ -64,6 +66,33 @@ public interface TemplateManager { true, ConfigKey.Scope.Global); + ConfigKey PublicTemplateSecStorageCopy = new ConfigKey("Advanced", Integer.class, + PublicTemplateSecStorageCopyCK, "0", + "Maximum number of secondary storage pools to which a public template is copied. " + + "0 means copy to all secondary storage pools (default behavior).", + true, ConfigKey.Scope.Zone); + + ConfigKey PrivateTemplateSecStorageCopy = new ConfigKey("Advanced", Integer.class, + PrivateTemplateSecStorageCopyCK, "1", + "Maximum number of secondary storage pools to which a private template is copied. " + + "Default is 1 to preserve existing behavior.", + true, ConfigKey.Scope.Zone); + + ConfigKey VmIsoMaxCount = new ConfigKey("Advanced", + Integer.class, + "vm.iso.max.count", "1", + "Maximum number of ISOs that may be attached to a VM.", + true, + ConfigKey.Scope.Cluster); + + // KVM/libvirt maps deviceSeq=3 to hdc (hda/hdb are taken by the root volume on i440fx/IDE). + // user_vm.iso_id has always pointed at this slot; additional cdroms live in vm_iso_map. + int CDROM_PRIMARY_DEVICE_SEQ = 3; + + // Fallback per-VM cdrom cap when the placement host hasn't advertised host.cdrom.max.count + // (older agent, never-deployed VM, etc.). + int DEFAULT_CDROM_MAX_PER_VM = 1; + static final String VMWARE_TOOLS_ISO = "vmware-tools.iso"; static final String XS_TOOLS_ISO = "xs-tools.iso"; @@ -138,6 +167,12 @@ public interface TemplateManager { List getImageStoreByTemplate(long templateId, Long zoneId); + /** + * Max number of secondary storage copies for the template in this zone; {@code 0} means no limit. + * SYSTEM/ROUTING/BUILTIN templates are always exempt (returns {@code 0}). + */ + int getSecStorageCopyLimit(VMTemplateVO template, long zoneId); + TemplateInfo prepareIso(long isoId, long dcId, Long hostId, Long poolId); diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java new file mode 100644 index 000000000000..b9d2907ef780 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkDeleteBackup.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm; + +public class VmWorkDeleteBackup extends VmWork { + + private long backupId; + + private boolean forced; + + public VmWorkDeleteBackup(long userId, long accountId, long vmId, String handlerName, long backupId, boolean forced) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.forced = forced; + } + + public long getBackupId() { + return backupId; + } + + public boolean isForced() { + return forced; + } +} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java similarity index 55% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java rename to engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java index cdcfc87cd4e4..421430cfbe9d 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreBackup.java @@ -14,27 +14,32 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +package com.cloud.vm; -package org.apache.cloudstack.quota.activationrule.presetvariables; +public class VmWorkRestoreBackup extends VmWork { -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; + private long backupId; -@RunWith(MockitoJUnitRunner.class) -public class ResourceTest { + private boolean quickRestore; - @Test - public void toStringTestReturnAJson() { - Resource variable = new Resource(); + private Long hostId; - String expected = ToStringBuilder.reflectionToString(variable, ToStringStyle.JSON_STYLE); - String result = variable.toString(); + public VmWorkRestoreBackup(long userId, long accountId, long vmId, String handlerName, long backupId, boolean quickRestore, Long hostId) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.quickRestore = quickRestore; + this.hostId = hostId; + } + + public long getBackupId() { + return backupId; + } - Assert.assertEquals(expected, result); + public boolean isQuickRestore() { + return quickRestore; } + public Long getHostId() { + return hostId; + } } diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java new file mode 100644 index 000000000000..a34b11abbdb4 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkRestoreVolumeBackupAndAttach.java @@ -0,0 +1,55 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm; + +import org.apache.cloudstack.backup.Backup; + +public class VmWorkRestoreVolumeBackupAndAttach extends VmWork { + + private long backupId; + + private Backup.VolumeInfo backupVolumeInfo; + + private String hostIp; + + private boolean quickRestore; + + public VmWorkRestoreVolumeBackupAndAttach(long userId, long accountId, long vmId, String handlerName, long backupId, Backup.VolumeInfo backupVolumeInfo, + String hostIp, boolean quickRestore) { + super(userId, accountId, vmId, handlerName); + this.backupId = backupId; + this.backupVolumeInfo = backupVolumeInfo; + this.hostIp = hostIp; + this.quickRestore = quickRestore; + } + + public long getBackupId() { + return backupId; + } + + public Backup.VolumeInfo getBackupVolumeInfo() { + return backupVolumeInfo; + } + + public String getHostIp() { + return hostIp; + } + + public boolean isQuickRestore() { + return quickRestore; + } +} diff --git a/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java new file mode 100644 index 000000000000..57367d368b86 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/vm/VmWorkTakeBackup.java @@ -0,0 +1,50 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm; + +public class VmWorkTakeBackup extends VmWork { + + private long backupId; + + private boolean quiesceVm; + + private boolean isolated; + + public VmWorkTakeBackup(long userId, long accountId, long vmId, long backupId, String handlerName, boolean quiesceVm, boolean isolated) { + super(userId, accountId, vmId, handlerName); + this.quiesceVm = quiesceVm; + this.backupId = backupId; + this.isolated = isolated; + } + + public boolean isQuiesceVm() { + return quiesceVm; + } + + public long getBackupId() { + return backupId; + } + + public boolean isIsolated() { + return isolated; + } + + @Override + public String toString() { + return super.toStringAfterRemoveParams(null, null); + } +} diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index 439bdf92ddd7..1215829d92f8 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -42,6 +42,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.StringUtils; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.command.ReconcileCommandService; @@ -64,7 +65,6 @@ import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.ThreadContext; import com.cloud.agent.AgentManager; @@ -805,8 +805,11 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi String uefiEnabled = detailsMap.get(Host.HOST_UEFI_ENABLE); String virtv2vVersion = detailsMap.get(Host.HOST_VIRTV2V_VERSION); String ovftoolVersion = detailsMap.get(Host.HOST_OVFTOOL_VERSION); + String vddkSupport = detailsMap.get(Host.HOST_VDDK_SUPPORT); + String vddkLibDir = detailsMap.get(Host.HOST_VDDK_LIB_DIR); + String vddkVersion = detailsMap.get(Host.HOST_VDDK_VERSION); logger.debug("Got HOST_UEFI_ENABLE [{}] for host [{}]:", uefiEnabled, host); - if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion)) { + if (ObjectUtils.anyNotNull(uefiEnabled, virtv2vVersion, ovftoolVersion, vddkSupport, vddkLibDir, vddkVersion)) { _hostDao.loadDetails(host); boolean updateNeeded = false; if (StringUtils.isNotBlank(uefiEnabled) && !uefiEnabled.equals(host.getDetails().get(Host.HOST_UEFI_ENABLE))) { @@ -821,6 +824,26 @@ protected AgentAttache notifyMonitorsOfConnection(final AgentAttache attache, fi host.getDetails().put(Host.HOST_OVFTOOL_VERSION, ovftoolVersion); updateNeeded = true; } + if (StringUtils.isNotBlank(vddkSupport) && !vddkSupport.equals(host.getDetails().get(Host.HOST_VDDK_SUPPORT))) { + host.getDetails().put(Host.HOST_VDDK_SUPPORT, vddkSupport); + updateNeeded = true; + } + if (!StringUtils.defaultString(vddkLibDir).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_VDDK_LIB_DIR)))) { + if (StringUtils.isBlank(vddkLibDir)) { + host.getDetails().remove(Host.HOST_VDDK_LIB_DIR); + } else { + host.getDetails().put(Host.HOST_VDDK_LIB_DIR, vddkLibDir); + } + updateNeeded = true; + } + if (!StringUtils.defaultString(vddkVersion).equals(StringUtils.defaultString(host.getDetails().get(Host.HOST_VDDK_VERSION)))) { + if (StringUtils.isBlank(vddkVersion)) { + host.getDetails().remove(Host.HOST_VDDK_VERSION); + } else { + host.getDetails().put(Host.HOST_VDDK_VERSION, vddkVersion); + } + updateNeeded = true; + } if (updateNeeded) { _hostDao.saveDetails(host); } @@ -2111,7 +2134,7 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize, DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait, GranularWaitTimeForCommands, RemoteAgentSslHandshakeTimeout, RemoteAgentMaxConcurrentNewConnections, - RemoteAgentNewConnectionsMonitorInterval }; + RemoteAgentNewConnectionsMonitorInterval, KVMHostDiscoverySshPort }; } protected class SetHostParamsListener implements Listener { @@ -2234,6 +2257,25 @@ public boolean transferDirectAgentsFromMS(String fromMsUuid, long fromMsId, long return true; } + @Override + public int getHostSshPort(HostVO host) { + if (host == null) { + return KVMHostDiscoverySshPort.value(); + } + + if (host.getHypervisorType() != HypervisorType.KVM) { + return Host.DEFAULT_SSH_PORT; + } + + _hostDao.loadDetails(host); + String hostPort = host.getDetail(Host.HOST_SSH_PORT); + if (StringUtils.isBlank(hostPort)) { + return KVMHostDiscoverySshPort.valueIn(host.getClusterId()); + } + + return Integer.parseInt(hostPort); + } + private GlobalLock getHostJoinLock(Long hostId) { return GlobalLock.getInternLock(String.format("%s-%s", "Host-Join", hostId)); } diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java index cfa0949883fd..38a198b73040 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java @@ -1306,11 +1306,20 @@ public String dispatch(final ClusterServicePdu pdu) { boolean result; try { - result = _resourceMgr.executeUserRequest(cmd.getHostId(), cmd.getEvent()); + result = _resourceMgr.executeUserRequest(cmd.getHostId(), cmd.getEvent(), cmd.isForced(), cmd.isForceDeleteStorage()); logger.debug("Result is {}", result); } catch (final AgentUnavailableException ex) { logger.warn("Agent is unavailable", ex); return null; + } catch (final RuntimeException ex) { + logger.error(String.format("Failed to execute propagated event %s for host %d", cmd.getEvent().name(), cmd.getHostId()), ex); + final Answer[] answers = new Answer[1]; + String details = ex.getMessage(); + if (details == null || details.isEmpty()) { + details = ex.toString(); + } + answers[0] = new Answer(cmd, false, details); + return _gson.toJson(answers); } final Answer[] answers = new Answer[1]; diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index e8796fb02529..364db685c9de 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -17,6 +17,7 @@ package com.cloud.vm; +import static com.cloud.configuration.ConfigurationManagerImpl.EXPOSE_ERRORS_TO_USER; import static com.cloud.configuration.ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS; import java.lang.reflect.Field; @@ -49,7 +50,9 @@ import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; - +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.hypervisor.KVMGuru; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -135,6 +138,7 @@ import com.cloud.agent.api.PrepareExternalProvisioningCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.PreMigrationCommand; import com.cloud.agent.api.RebootAnswer; import com.cloud.agent.api.RebootCommand; import com.cloud.agent.api.RecreateCheckpointsCommand; @@ -153,6 +157,8 @@ import com.cloud.agent.api.UnPlugNicCommand; import com.cloud.agent.api.UnmanageInstanceCommand; import com.cloud.agent.api.UnregisterVMCommand; +import com.cloud.agent.api.UpdateVmNicAnswer; +import com.cloud.agent.api.UpdateVmNicCommand; import com.cloud.agent.api.VmDiskStatsEntry; import com.cloud.agent.api.VmNetworkStatsEntry; import com.cloud.agent.api.VmStatsEntry; @@ -264,6 +270,7 @@ import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; @@ -300,14 +307,13 @@ import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.google.gson.Gson; - public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { public static final String VM_WORK_JOB_HANDLER = VirtualMachineManagerImpl.class.getSimpleName(); @@ -359,6 +365,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private VolumeDao _volsDao; @Inject + private VolumeDetailsDao _volsDetailsDao; + @Inject private HighAvailabilityManager _haMgr; @Inject private HostPodDao _podDao; @@ -461,6 +469,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac ExtensionsManager extensionsManager; @Inject ExtensionDetailsDao extensionDetailsDao; + @Inject + ClvmPoolManager clvmPoolManager; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -489,7 +499,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac static final ConfigKey ClusterVMMetaDataSyncInterval = new ConfigKey("Advanced", Integer.class, "vmmetadata.sync.interval", "180", "Cluster VM metadata sync interval in seconds", false); - static final ConfigKey VmJobCheckInterval = new ConfigKey("Advanced", + public static final ConfigKey VmJobCheckInterval = new ConfigKey("Advanced", Long.class, "vm.job.check.interval", "3000", "Interval in milliseconds to check if the job is complete", false); static final ConfigKey VmJobTimeout = new ConfigKey("Advanced", @@ -574,7 +584,13 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t logger.debug("Allocating disks for {}", persistedVm); - allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot); + if (isBlankInstance(template)) { + logger.debug("Template is a dummy template for hypervisor {}, skipping volume allocation", hyperType); + return; + } else { + allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot); + } + // Create new Volume context and inject event resource type, id and details to generate VOLUME.CREATE event for the ROOT disk. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); @@ -585,7 +601,7 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t Long deviceId = dataDiskDeviceIds.get(index++); String volumeName = deviceId == null ? "DATA-" + persistedVm.getId() : "DATA-" + persistedVm.getId() + "-" + String.valueOf(deviceId); volumeMgr.allocateRawVolume(Type.DATADISK, volumeName, dataDiskOfferingInfo.getDiskOffering(), dataDiskOfferingInfo.getSize(), - dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, deviceId); + dataDiskOfferingInfo.getMinIops(), dataDiskOfferingInfo.getMaxIops(), persistedVm, template, owner, deviceId, dataDiskOfferingInfo.getKmsKeyId(), true); } } if (datadiskTemplateToDiskOfferingMap != null && !datadiskTemplateToDiskOfferingMap.isEmpty()) { @@ -595,7 +611,7 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t long diskOfferingSize = diskOffering.getDiskSize() / (1024 * 1024 * 1024); VMTemplateVO dataDiskTemplate = _templateDao.findById(dataDiskTemplateToDiskOfferingMap.getKey()); volumeMgr.allocateRawVolume(Type.DATADISK, "DATA-" + persistedVm.getId() + "-" + String.valueOf( diskNumber), diskOffering, diskOfferingSize, null, null, - persistedVm, dataDiskTemplate, owner, diskNumber); + persistedVm, dataDiskTemplate, owner, diskNumber, null, true); diskNumber++; } } @@ -625,12 +641,12 @@ private void allocateRootVolume(VMInstanceVO vm, VirtualMachineTemplate template String rootVolumeName = String.format("ROOT-%s", vm.getId()); if (template.getFormat() == ImageFormat.ISO) { volumeMgr.allocateRawVolume(Type.ROOT, rootVolumeName, rootDiskOfferingInfo.getDiskOffering(), rootDiskOfferingInfo.getSize(), - rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), vm, template, owner, null); + rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), vm, template, owner, null, rootDiskOfferingInfo.getKmsKeyId(), true); } else if (Arrays.asList(ImageFormat.BAREMETAL, ImageFormat.EXTERNAL).contains(template.getFormat())) { logger.debug("{} has format [{}]. Skipping ROOT volume [{}] allocation.", template, template.getFormat(), rootVolumeName); } else { volumeMgr.allocateTemplatedVolumes(Type.ROOT, rootVolumeName, rootDiskOfferingInfo.getDiskOffering(), rootDiskSizeFinal, - rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), template, vm, owner, volume, snapshot); + rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), template, vm, owner, rootDiskOfferingInfo.getKmsKeyId(), volume, snapshot); } } finally { // Remove volumeContext and pop vmContext back @@ -931,10 +947,22 @@ public void start(final String vmUuid, final Map params, final DeploymentPlan planToDeploy, final DeploymentPlanner planner) { try { advanceStart(vmUuid, params, planToDeploy, planner); - } catch (ConcurrentOperationException | InsufficientCapacityException e) { - throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to [%s].", vmUuid, e.getMessage()), e).add(VirtualMachine.class, vmUuid); + } catch (ConcurrentOperationException e) { + final CallContext cctxt = CallContext.current(); + final Account account = cctxt.getCallingAccount(); + if (canExposeError(account)) { + throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to [%s].", vmUuid, e.getMessage()), e).add(VirtualMachine.class, vmUuid); + } + throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to concurrent operation.", vmUuid), e).add(VirtualMachine.class, vmUuid); + } catch (final InsufficientCapacityException e) { + final CallContext cctxt = CallContext.current(); + final Account account = cctxt.getCallingAccount(); + if (canExposeError(account)) { + throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to [%s].", vmUuid, e.getMessage()), e).add(VirtualMachine.class, vmUuid); + } + throw new CloudRuntimeException(String.format("Unable to start a VM [%s] due to insufficient capacity.", vmUuid), e).add(VirtualMachine.class, vmUuid); } catch (final ResourceUnavailableException e) { - if (e.getScope() != null && e.getScope().equals(VirtualRouter.class)){ + if (e.getScope() != null && e.getScope().equals(VirtualRouter.class)) { Account callingAccount = CallContext.current().getCallingAccount(); String errorSuffix = (callingAccount != null && callingAccount.getType() == Account.Type.ADMIN) ? String.format("Failure: %s", e.getMessage()) : @@ -1001,7 +1029,7 @@ public Ternary doInTransaction(final if (stateTransitTo(vm, Event.StartRequested, null, work.getId())) { logger.debug("Successfully transitioned to start state for {} reservation id = {}", vm, work.getId()); if (VirtualMachine.Type.User.equals(vm.type) && ResourceCountRunningVMsonly.value()) { - _resourceLimitMgr.incrementVmResourceCount(owner.getAccountId(), vm.isDisplay(), offering, template); + _resourceLimitMgr.incrementVmResourceCount(owner.getAccountId(), vm.isDisplay(), offering, template, null); } return new Ternary<>(vm, context, work); } @@ -1365,8 +1393,10 @@ public void orchestrateStart(final String vmUuid, final Map vols = _volsDao.findReadyRootVolumesByInstance(vm.getId()); @@ -1454,8 +1485,13 @@ public void orchestrateStart(final String vmUuid, final Map> getVolumesToDisconnect(VirtualMachine vm) { protected boolean sendStop(final VirtualMachineGuru guru, final VirtualMachineProfile profile, final boolean force, final boolean checkBeforeCleanup) { final VirtualMachine vm = profile.getVirtualMachine(); Map vlanToPersistenceMap = getVlanToPersistenceMapForVM(vm.getId()); - StopCommand stpCmd = new StopCommand(vm, getExecuteInSequence(vm.getHypervisorType()), checkBeforeCleanup); updateStopCommandForExternalHypervisorType(vm.getHypervisorType(), profile, stpCmd); if (MapUtils.isNotEmpty(vlanToPersistenceMap)) { @@ -2595,7 +2654,7 @@ public Boolean doInTransaction(TransactionStatus status) throws NoTransitionExce if (result && VirtualMachine.Type.User.equals(vm.type) && ResourceCountRunningVMsonly.value()) { ServiceOfferingVO offering = _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()); VMTemplateVO template = _templateDao.findByIdIncludingRemoved(vm.getTemplateId()); - _resourceLimitMgr.decrementVmResourceCount(vm.getAccountId(), vm.isDisplay(), offering, template); + _resourceLimitMgr.decrementVmResourceCount(vm.getAccountId(), vm.isDisplay(), offering, template, null); } return result; } @@ -3107,6 +3166,9 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy updateOverCommitRatioForVmProfile(profile, dest.getHost().getClusterId()); final VirtualMachineTO to = toVmTO(profile); + + executePreMigrationCommand(vm, to, srcHostId); + final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); setVmNetworkDetails(vm, to); @@ -3238,6 +3300,7 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e); } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -3277,6 +3340,30 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy } } + private void executePostMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long dstHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String dstHostUuid = _hostDao.findById(dstHostId).getUuid(); + try { + logger.info("Executing post-migration tasks for VM {} with CLVM volumes on destination host {}", vm.getInstanceName(), dstHostUuid); + final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName()); + final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand); + + if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) { + final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned"; + logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, details); + } else { + logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostUuid); + } + } catch (Exception e) { + logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, e.getMessage(), e); + } + updateClvmLockHostForVmVolumes(vm.getId(), dstHostId); + } + /** * Create and set parameters for the {@link MigrateCommand} used in the migration and scaling of VMs. */ @@ -3323,6 +3410,27 @@ private void updateVmPod(VMInstanceVO vm, long dstHostId) { _vmDao.persist(newVm); } + /** + * Updates CLVM_LOCK_HOST_ID for all CLVM volumes attached to a VM after VM migration. + * This ensures that subsequent operations on CLVM volumes are routed to the correct host. + * + * @param vmId The ID of the VM that was migrated + * @param destHostId The destination host ID where the VM now resides + */ + private void updateClvmLockHostForVmVolumes(long vmId, long destHostId) { + List volumes = _volsDao.findByInstance(vmId); + if (CollectionUtils.isEmpty(volumes)) { + return; + } + + for (VolumeVO volume : volumes) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } + } + } + /** * We create the mapping of volumes and storage pool to migrate the VMs according to the information sent by the user. * If the user did not enter a complete mapping, the volumes that were left behind will be auto mapped using {@link #createStoragePoolMappingsForVolumes(VirtualMachineProfile, DataCenterDeployment, Map, List)} @@ -3443,7 +3551,8 @@ protected void createStoragePoolMappingsForVolumes(VirtualMachineProfile profile protected boolean shouldMapVolume(VirtualMachineProfile profile, StoragePoolVO currentPool) { boolean isManaged = currentPool.isManaged(); boolean isNotKvm = HypervisorType.KVM != profile.getHypervisorType(); - return isNotKvm || isManaged; + boolean isClvm = ClvmPoolManager.isClvmPoolType(currentPool.getPoolType()); + return isNotKvm || isManaged || isClvm; } /** @@ -3981,19 +4090,6 @@ protected void runInContext() { } } - @Override - public boolean isVirtualMachineUpgradable(final VirtualMachine vm, final ServiceOffering offering) { - boolean isMachineUpgradable = true; - for (final HostAllocator allocator : hostAllocators) { - isMachineUpgradable = allocator.isVirtualMachineUpgradable(vm, offering); - if (!isMachineUpgradable) { - break; - } - } - - return isMachineUpgradable; - } - @Override public void reboot(final String vmUuid, final Map params) throws InsufficientCapacityException, ResourceUnavailableException { try { @@ -4433,11 +4529,6 @@ public void checkIfCanUpgrade(final VirtualMachine vmInstance, final ServiceOffe throw new InvalidParameterValueException("isSystem property is different for current service offering and new service offering"); } - if (!isVirtualMachineUpgradable(vmInstance, newServiceOffering)) { - throw new InvalidParameterValueException("Unable to upgrade virtual machine, not enough resources available " + "for an offering of " + - newServiceOffering.getCpu() + " cpu(s) at " + newServiceOffering.getSpeed() + " Mhz, and " + newServiceOffering.getRamSize() + " MB of memory"); - } - final List currentTags = StringUtils.csvTagsToList(currentDiskOffering.getTags()); final List newTags = StringUtils.csvTagsToList(newDiskOffering.getTags()); if (VolumeApiServiceImpl.MatchStoragePoolTagsWithDiskOffering.valueIn(vmInstance.getDataCenterId())) { @@ -4897,6 +4988,12 @@ private void orchestrateMigrateForScale(final String vmUuid, final long srcHostI volumeMgr.prepareForMigration(profile, dest); final VirtualMachineTO to = toVmTO(profile); + + // Step 1: Send PreMigrationCommand to source host to convert CLVM volumes to shared mode + // This must happen BEFORE PrepareForMigrationCommand on destination to avoid lock conflicts + executePreMigrationCommand(vm, to, srcHostId); + + // Step 2: Send PrepareForMigrationCommand to destination host final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); ItWorkVO work = new ItWorkVO(UUID.randomUUID().toString(), _nodeId, State.Migrating, vm.getType(), vm.getId()); @@ -4981,6 +5078,7 @@ private void orchestrateMigrateForScale(final String vmUuid, final long srcHostI } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -5144,7 +5242,7 @@ public VMInstanceVO reConfigureVm(final String vmUuid, final ServiceOffering old try { result = retrieveResultFromJobOutcomeAndThrowExceptionIfNeeded(outcome); } catch (Exception ex) { - throw new RuntimeException("Unhandled exception", ex); + throw new RuntimeException("Unable to reconfigure VM.", ex); } if (result != null) { @@ -5157,22 +5255,29 @@ public VMInstanceVO reConfigureVm(final String vmUuid, final ServiceOffering old private VMInstanceVO orchestrateReConfigureVm(String vmUuid, ServiceOffering oldServiceOffering, ServiceOffering newServiceOffering, boolean reconfiguringOnExistingHost) throws ResourceUnavailableException, ConcurrentOperationException { - final VMInstanceVO vm = _vmDao.findByUuid(vmUuid); + VMInstanceVO vm = _vmDao.findByUuid(vmUuid); HostVO hostVo = _hostDao.findById(vm.getHostId()); - Long clustedId = hostVo.getClusterId(); - Float memoryOvercommitRatio = CapacityManager.MemOverprovisioningFactor.valueIn(clustedId); - Float cpuOvercommitRatio = CapacityManager.CpuOverprovisioningFactor.valueIn(clustedId); - boolean divideMemoryByOverprovisioning = HypervisorGuruBase.VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor.valueIn(clustedId); - boolean divideCpuByOverprovisioning = HypervisorGuruBase.VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor.valueIn(clustedId); + Long clusterId = hostVo.getClusterId(); + Float memoryOvercommitRatio = CapacityManager.MemOverprovisioningFactor.valueIn(clusterId); + Float cpuOvercommitRatio = CapacityManager.CpuOverprovisioningFactor.valueIn(clusterId); + boolean divideMemoryByOverprovisioning = HypervisorGuruBase.VmMinMemoryEqualsMemoryDividedByMemOverprovisioningFactor.valueIn(clusterId); + boolean divideCpuByOverprovisioning = HypervisorGuruBase.VmMinCpuSpeedEqualsCpuSpeedDividedByCpuOverprovisioningFactor.valueIn(clusterId); int minMemory = (int)(newServiceOffering.getRamSize() / (divideMemoryByOverprovisioning ? memoryOvercommitRatio : 1)); int minSpeed = (int)(newServiceOffering.getSpeed() / (divideCpuByOverprovisioning ? cpuOvercommitRatio : 1)); - ScaleVmCommand scaleVmCommand = - new ScaleVmCommand(vm.getInstanceName(), newServiceOffering.getCpu(), minSpeed, - newServiceOffering.getSpeed(), minMemory * 1024L * 1024L, newServiceOffering.getRamSize() * 1024L * 1024L, newServiceOffering.getLimitCpuUse()); + Double cpuQuotaPercentage = null; + if (newServiceOffering.getLimitCpuUse() && vm.getHypervisorType().equals(HypervisorType.KVM)) { + KVMGuru kvmGuru = (KVMGuru) _hvGuruMgr.getGuru(vm.getHypervisorType()); + cpuQuotaPercentage = kvmGuru.getCpuQuotaPercentage(minSpeed, hostVo.getSpeed()); + } + + boolean limitCpuUseChange = oldServiceOffering.getLimitCpuUse() != newServiceOffering.getLimitCpuUse(); + ScaleVmCommand scaleVmCommand = new ScaleVmCommand(vm.getInstanceName(), newServiceOffering.getCpu(), minSpeed, newServiceOffering.getSpeed(), + minMemory * 1024L * 1024L, newServiceOffering.getRamSize() * 1024L * 1024L, + newServiceOffering.getLimitCpuUse(), cpuQuotaPercentage, limitCpuUseChange); scaleVmCommand.getVirtualMachine().setId(vm.getId()); scaleVmCommand.getVirtualMachine().setUuid(vm.getUuid()); @@ -5201,16 +5306,20 @@ private VMInstanceVO orchestrateReConfigureVm(String vmUuid, ServiceOffering old throw new CloudRuntimeException("Unable to scale vm due to " + (reconfigureAnswer == null ? "" : reconfigureAnswer.getDetails())); } - upgradeVmDb(vm.getId(), newServiceOffering, oldServiceOffering); + if (reconfiguringOnExistingHost) { + _capacityMgr.releaseVmCapacity(vm, false, false, vm.getHostId()); + } + + boolean vmUpgraded = upgradeVmDb(vm.getId(), newServiceOffering, oldServiceOffering); + if (vmUpgraded) { + vm = _vmDao.findById(vm.getId()); + } if (vm.getType().equals(VirtualMachine.Type.User)) { _userVmMgr.generateUsageEvent(vm, vm.isDisplayVm(), EventTypes.EVENT_VM_DYNAMIC_SCALE); } if (reconfiguringOnExistingHost) { - vm.setServiceOfferingId(oldServiceOffering.getId()); - _capacityMgr.releaseVmCapacity(vm, false, false, vm.getHostId()); - vm.setServiceOfferingId(newServiceOffering.getId()); _capacityMgr.allocateVmCapacity(vm, false); } @@ -5239,9 +5348,20 @@ private void removeCustomOfferingDetails(long vmId) { private void saveCustomOfferingDetails(long vmId, ServiceOffering serviceOffering) { Map details = vmInstanceDetailsDao.listDetailsKeyPairs(vmId); - details.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); - details.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); - details.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); + + // We need to restore only the customizable parameters. If we save a parameter that is not customizable and attempt + // to restore a VM snapshot, com.cloud.vm.UserVmManagerImpl.validateCustomParameters will fail. + ServiceOffering unfilledOffering = _serviceOfferingDao.findByIdIncludingRemoved(serviceOffering.getId()); + if (unfilledOffering.getCpu() == null) { + details.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); + } + if (unfilledOffering.getSpeed() == null) { + details.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); + } + if (unfilledOffering.getRamSize() == null) { + details.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); + } + List detailList = new ArrayList<>(); for (Map.Entry entry: details.entrySet()) { VMInstanceDetailVO detailVO = new VMInstanceDetailVO(vmId, entry.getKey(), entry.getValue(), true); @@ -6270,6 +6390,80 @@ private Pair orchestrateUpdateDefaultNic(final VmWorkUpd _jobMgr.marshallResultObject(result)); } + @Override + public boolean updateVmNic(VirtualMachine vm, Nic nic, Boolean enabled) { + Outcome outcome = updateVmNicThroughJobQueue(vm, nic, enabled); + + retrieveVmFromJobOutcome(outcome, vm.getUuid(), "updateVmNic"); + + try { + Object jobResult = retrieveResultFromJobOutcomeAndThrowExceptionIfNeeded(outcome); + if (jobResult instanceof Boolean) { + return BooleanUtils.isTrue((Boolean) jobResult); + } + } catch (ResourceUnavailableException | InsufficientCapacityException ex) { + throw new CloudRuntimeException(String.format("Exception while updating VM [%s] NIC. Check the logs for more information.", vm.getUuid())); + } + throw new CloudRuntimeException("Unexpected job execution result."); + } + + private boolean orchestrateUpdateVmNic(final VirtualMachine vm, final Nic nic, final Boolean enabled) throws ResourceUnavailableException { + if (vm.getState() == State.Running) { + try { + UpdateVmNicCommand updateVmNicCmd = new UpdateVmNicCommand(nic.getMacAddress(), vm.getName(), enabled); + Commands cmds = new Commands(Command.OnError.Stop); + cmds.addCommand("updatevmnic", updateVmNicCmd); + + _agentMgr.send(vm.getHostId(), cmds); + + UpdateVmNicAnswer updateVmNicAnswer = cmds.getAnswer(UpdateVmNicAnswer.class); + if (updateVmNicAnswer == null || !updateVmNicAnswer.getResult()) { + logger.warn("Unable to update VM %s NIC [{}].", vm.getName(), nic.getUuid()); + return false; + } + } catch (final OperationTimedoutException e) { + throw new AgentUnavailableException(String.format("Unable to update NIC %s for VM %s.", nic.getUuid(), vm.getUuid()), vm.getHostId(), e); + } + } + + NicVO nicVo = _nicsDao.findById(nic.getId()); + nicVo.setEnabled(enabled); + _nicsDao.persist(nicVo); + + return true; + } + + public Outcome updateVmNicThroughJobQueue(final VirtualMachine vm, final Nic nic, final Boolean isNicEnabled) { + Long vmId = vm.getId(); + String commandName = VmWorkUpdateNic.class.getName(); + Pair pendingWorkJob = retrievePendingWorkJob(vmId, commandName); + + VmWorkJobVO workJob = pendingWorkJob.first(); + + if (workJob == null) { + Pair newVmWorkJobAndInfo = createWorkJobAndWorkInfo(commandName, vmId); + + workJob = newVmWorkJobAndInfo.first(); + VmWorkUpdateNic workInfo = new VmWorkUpdateNic(newVmWorkJobAndInfo.second(), nic.getId(), isNicEnabled); + + setCmdInfoAndSubmitAsyncJob(workJob, workInfo, vmId); + } + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new VmJobVirtualMachineOutcome(workJob, vmId); + } + + @ReflectionUse + private Pair orchestrateUpdateVmNic(final VmWorkUpdateNic work) throws Exception { + VMInstanceVO vm = findVmById(work.getVmId()); + final NicVO nic = _entityMgr.findById(NicVO.class, work.getNicId()); + if (nic == null) { + throw new CloudRuntimeException(String.format("Unable to find NIC with ID %s.", work.getNicId())); + } + final boolean result = orchestrateUpdateVmNic(vm, nic, work.isEnabled()); + return new Pair<>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(result)); + } + private Pair findClusterAndHostIdForVmFromVolumes(long vmId) { Long clusterId = null; Long hostId = null; @@ -6320,6 +6514,37 @@ private Pair findClusterAndHostIdForVm(VirtualMachine vm) { return findClusterAndHostIdForVm(vm, false); } + private boolean hasClvmVolumes(long vmId) { + List volumes = _volsDao.findByInstance(vmId); + return volumes.stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(pool -> pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())); + } + + private void executePreMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long srcHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String vmInstanceName = vm.getInstanceName(); + final String srcHostUuid = _hostDao.findById(srcHostId).getUuid(); + logger.info("Sending PreMigrationCommand to source host {} for VM {} with CLVM volumes", srcHostUuid, vmInstanceName); + final PreMigrationCommand preMigCmd = new PreMigrationCommand(to, vmInstanceName); + Answer preMigAnswer = null; + try { + preMigAnswer = _agentMgr.send(srcHostId, preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + final String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + final String msg = "Failed to prepare source host for migration: " + details; + logger.error("Failed to prepare source host {} for migration of VM {}: {}", srcHostUuid, vmInstanceName, details); + throw new CloudRuntimeException(msg); + } + logger.info("Successfully prepared source host {} for migration of VM {}", srcHostUuid, vmInstanceName); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + logger.error("Failed to send PreMigrationCommand to source host {}: {}", srcHostUuid, e.getMessage(), e); + throw new CloudRuntimeException("Failed to prepare source host for migration: " + e.getMessage(), e); + } + } + @Override public Pair findClusterAndHostIdForVm(long vmId) { VMInstanceVO vm = _vmDao.findById(vmId); @@ -6593,4 +6818,18 @@ public void checkDeploymentPlan(VirtualMachine virtualMachine, VirtualMachineTem vmProfile), DataCenter.class, plan.getDataCenterId(), areAffinityGroupsAssociated(vmProfile)); } } + + @Override + public boolean isBlankInstanceDefaultTemplate(VirtualMachineTemplate template) { + return KVM_BLANK_VM_TEMPLATE_NAME.equals(template.getUniqueName()); + } + + @Override + public boolean isBlankInstance(VirtualMachineTemplate template) { + if (isBlankInstanceDefaultTemplate(template)) { + return true; + } + return Boolean.TRUE.equals( + MapUtils.getBoolean(CallContext.current().getContextParameters(), ApiConstants.BLANK_INSTANCE)); + } } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java b/engine/orchestration/src/main/java/com/cloud/vm/VmWorkUpdateNic.java similarity index 62% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java rename to engine/orchestration/src/main/java/com/cloud/vm/VmWorkUpdateNic.java index 1e62235e71cf..1c63cf34a192 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VmWorkUpdateNic.java @@ -14,21 +14,25 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +package com.cloud.vm; -package org.apache.cloudstack.quota.activationrule.presetvariables; +public class VmWorkUpdateNic extends VmWork { + private static final long serialVersionUID = -8957066627929113278L; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; + Long nicId; + Boolean enabled; -@RunWith(MockitoJUnitRunner.class) -public class AccountTest { + public VmWorkUpdateNic(VmWork vmWork, Long nicId, Boolean enabled) { + super(vmWork); + this.nicId = nicId; + this.enabled = enabled; + } + + public Long getNicId() { + return nicId; + } - @Test - public void setRoleTestAddFieldRoleToCollection() { - Account variable = new Account(); - variable.setRole(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("role")); + public Boolean isEnabled() { + return enabled; } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java index 8639f006383f..9f6d02cc1234 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.engine.orchestration; import com.cloud.storage.Snapshot; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.template.VirtualMachineTemplate; import java.net.URL; @@ -164,7 +165,7 @@ public void destroyVolume(String volumeEntity) { public VirtualMachineEntity createVirtualMachine(String id, String owner, String templateId, String hostName, String displayName, String hypervisor, int cpu, int speed, long memory, Long diskSize, List computeTags, List rootDiskTags, Map> networkNicMap, DeploymentPlan plan, Long rootDiskSize, Map> extraDhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Long dataDiskOfferingId, Long rootDiskOfferingId, - List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { + Long rootDiskKmsKeyId, List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { // VirtualMachineEntityImpl vmEntity = new VirtualMachineEntityImpl(id, owner, hostName, displayName, cpu, speed, memory, computeTags, rootDiskTags, networks, // vmEntityManager); @@ -198,6 +199,7 @@ public VirtualMachineEntity createVirtualMachine(String id, String owner, String } rootDiskOfferingInfo.setDiskOffering(rootDiskOffering); rootDiskOfferingInfo.setSize(rootDiskSize); + rootDiskOfferingInfo.setKmsKeyId(rootDiskKmsKeyId); if (rootDiskOffering.isCustomizedIops() != null && rootDiskOffering.isCustomizedIops()) { Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); @@ -280,7 +282,7 @@ public VirtualMachineEntity createVirtualMachine(String id, String owner, String @Override public VirtualMachineEntity createVirtualMachineFromScratch(String id, String owner, String isoId, String hostName, String displayName, String hypervisor, String os, int cpu, int speed, long memory, Long diskSize, List computeTags, List rootDiskTags, Map> networkNicMap, DeploymentPlan plan, - Map> extraDhcpOptionMap, Long diskOfferingId, List dataDiskInfoList, Volume volume, Snapshot snapshot) + Map> extraDhcpOptionMap, Long diskOfferingId, Long rootDiskKmsKeyId, List dataDiskInfoList, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { // VirtualMachineEntityImpl vmEntity = new VirtualMachineEntityImpl(id, owner, hostName, displayName, cpu, speed, memory, computeTags, rootDiskTags, networks, vmEntityManager); @@ -292,9 +294,11 @@ public VirtualMachineEntity createVirtualMachineFromScratch(String id, String ow ServiceOfferingVO computeOffering = _serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + VMTemplateVO iso = _templateDao.findByIdIncludingRemoved(Long.valueOf(isoId)); + DiskOfferingInfo rootDiskOfferingInfo = new DiskOfferingInfo(); - if (diskOfferingId == null) { + if (diskOfferingId == null && !_itMgr.isBlankInstance(iso)) { throw new InvalidParameterValueException("Installing from ISO requires a disk offering to be specified for the root disk."); } DiskOfferingVO diskOffering = _diskOfferingDao.findById(diskOfferingId); @@ -314,6 +318,7 @@ public VirtualMachineEntity createVirtualMachineFromScratch(String id, String ow rootDiskOfferingInfo.setDiskOffering(diskOffering); rootDiskOfferingInfo.setSize(size); + rootDiskOfferingInfo.setKmsKeyId(rootDiskKmsKeyId); if (diskOffering.isCustomizedIops() != null && diskOffering.isCustomizedIops()) { Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); @@ -345,7 +350,7 @@ public VirtualMachineEntity createVirtualMachineFromScratch(String id, String ow HypervisorType hypervisorType = HypervisorType.valueOf(hypervisor); - _itMgr.allocate(vm.getInstanceName(), _templateDao.findByIdIncludingRemoved(new Long(isoId)), computeOffering, rootDiskOfferingInfo, dataDiskOfferings, dataDiskDeviceIds, + _itMgr.allocate(vm.getInstanceName(), iso, computeOffering, rootDiskOfferingInfo, dataDiskOfferings, dataDiskDeviceIds, networkIpMap, plan, hypervisorType, extraDhcpOptionMap, null, volume, snapshot); return vmEntity; diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/DataMigrationUtility.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/DataMigrationUtility.java index 5a8dc3038aa8..1441a9957772 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/DataMigrationUtility.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/DataMigrationUtility.java @@ -28,9 +28,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupJoinVO; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; @@ -42,6 +46,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.ImageStoreService; +import org.apache.cloudstack.storage.backup.BackupObject; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; @@ -89,12 +94,15 @@ public class DataMigrationUtility { HostDao hostDao; @Inject SnapshotDao snapshotDao; + + @Inject + InternalBackupJoinDao internalBackupJoinDao; /** * This function verifies if the given image store contains data objects that are not in any of the following states: * "Ready" "Allocated", "Destroying", "Destroyed", "Failed". If this is the case, and if the migration policy is complete, * the migration is terminated. */ - public boolean filesReadyToMigrate(Long srcDataStoreId, List templates, List snapshots, List volumes) { + public boolean filesReadyToMigrate(Long srcDataStoreId, List templates, List snapshots, List volumes, List backups) { State[] validStates = {State.Ready, State.Allocated, State.Destroying, State.Destroyed, State.Failed}; boolean isReady = true; for (TemplateDataStoreVO template : templates) { @@ -109,14 +117,48 @@ public boolean filesReadyToMigrate(Long srcDataStoreId, List backups) { + List invalidBackupStates = Arrays.asList(Backup.Status.BackingUp, Backup.Status.Restoring); + List invalidBackupCompressionStatus = Arrays.asList(Backup.CompressionStatus.Compressing, Backup.CompressionStatus.FinalizingCompression); + + List> backupChains; + Set backupIdsAlreadyInChain = new HashSet<>(); + + for (InternalBackupJoinVO backup : backups) { + if (backup.getStatus() == Backup.Status.BackedUp && !backupIdsAlreadyInChain.contains(backup.getId())) { + backupChains = createBackupChain(backup); + backupChains.forEach(list -> backupIdsAlreadyInChain.add(list.stream().map(BackupObject::getId).findFirst().get())); + + for (List backupVolumeChain : backupChains) { + BackupObject backupObject = backupVolumeChain.get(0); + + if (invalidBackupStates.contains(backupObject.getStatus())) { + logger.debug("Migration is not possible because backup {} is in {} state.", backupObject.getUuid(), backupObject.getStatus()); + return false; + } + + if (invalidBackupCompressionStatus.contains(backupObject.getCompressionStatus())) { + logger.debug("Migration is not possible because backup {} is currently being compressed. Current compression status: {}.", backupObject.getUuid(), backupObject.getCompressionStatus()); + return false; + } + } + } + } + + return true; + } + private boolean filesReadyToMigrate(Long srcDataStoreId) { List templates = templateDataStoreDao.listByStoreId(srcDataStoreId); List snapshots = snapshotDataStoreDao.listByStoreId(srcDataStoreId, DataStoreRole.Image); List volumes = volumeDataStoreDao.listByStoreId(srcDataStoreId); - return filesReadyToMigrate(srcDataStoreId, templates, snapshots, volumes); + List backups = internalBackupJoinDao.listByImageStoreId(srcDataStoreId); + + return filesReadyToMigrate(srcDataStoreId, templates, snapshots, volumes, backups); } protected void checkIfCompleteMigrationPossible(ImageStoreService.MigrationPolicy policy, Long srcDataStoreId) { @@ -175,19 +217,58 @@ protected List getSortedValidSourcesList(DataStore srcDataStore, Map return files; } - protected List getSortedValidSourcesList(DataStore srcDataStore, Map, Long>> snapshotChains, - Map, Long>> childTemplates) { + Map, Long>> childTemplates, Map>, Long>> backupChains) { List files = new ArrayList<>(); files.addAll(getAllReadyTemplates(srcDataStore, childTemplates)); files.addAll(getAllReadySnapshotsAndChains(srcDataStore, snapshotChains)); files.addAll(getAllReadyVolumes(srcDataStore)); + files.addAll(getAllReadyBackupsAndChains(srcDataStore, backupChains)); files = sortFilesOnSize(files, snapshotChains); return files; } + protected List getAllReadyBackupsAndChains(DataStore srcDataStore, Map>, Long>> backupChains) { + List backups = internalBackupJoinDao.listByImageStoreId(srcDataStore.getId()); + return getAllReadyBackupsAndChains(backupChains, backups); + } + + private List getAllReadyBackupsAndChains(Map>, Long>> backupsChains, List backups) { + Set backupIdsToMigrate = backups.stream().map(InternalBackupJoinVO::getId).collect(Collectors.toSet()); + List> backupChains; + Set backupIdsAlreadyInChain = new HashSet<>(); + List files = new LinkedList<>(); + + for (InternalBackupJoinVO backup : backups) { + long backupId = backup.getId(); + + if (backup.getStatus() == Backup.Status.BackedUp && !backupIdsAlreadyInChain.contains(backupId)) { + backupChains = createBackupChain(backup); + backupChains.forEach(list -> backupIdsAlreadyInChain.add(list.stream().map(BackupObject::getId).findFirst().get())); + BackupObject parent = backupChains.get(0).get(0); + files.add(parent); + backupsChains.put(parent, new Pair<>(backupChains, backupChains.stream().map(list -> getTotalChainSize(list.stream() + .filter(back -> backupIdsToMigrate.contains(parent.getId())).collect(Collectors.toList())) + ).reduce(Long::sum).get())); + } + } + + return (List) (List) files; + } + + private List> createBackupChain(InternalBackupJoinVO backup) { + List> chain = new LinkedList<>(); + BackupObject backupObject = BackupObject.getBackupObject(backup); + + chain.addAll(backupObject.getParents(backup.getParentId())); + chain.add(internalBackupJoinDao.listById(backup.getId()).stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + chain.addAll(backupObject.getChildren()); + + return chain; + } + protected List sortFilesOnSize(List files, Map, Long>> snapshotChains) { Collections.sort(files, new Comparator() { @Override diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 31144296f602..84a397349cec 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -58,6 +58,7 @@ import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -86,6 +87,7 @@ import com.cloud.api.query.vo.DomainRouterJoinVO; import com.cloud.bgp.BGPService; import com.cloud.configuration.ConfigurationManager; +import com.cloud.configuration.Resource; import com.cloud.configuration.Resource.ResourceType; import com.cloud.dc.ASNumberVO; import com.cloud.dc.ClusterVO; @@ -175,6 +177,10 @@ import com.cloud.network.dao.RemoteAccessVpnDao; import com.cloud.network.dao.RemoteAccessVpnVO; import com.cloud.network.dao.RouterNetworkDao; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionHelper; +import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement; + import com.cloud.network.element.AggregatedCommandExecutor; import com.cloud.network.element.ConfigDriveNetworkElement; import com.cloud.network.element.DhcpServiceProvider; @@ -214,6 +220,7 @@ import com.cloud.offerings.dao.NetworkOfferingDetailsDao; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; import com.cloud.resource.ResourceManager; +import com.cloud.resourcelimit.CheckedReservation; import com.cloud.server.ManagementServer; import com.cloud.user.Account; import com.cloud.user.ResourceLimitService; @@ -310,6 +317,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @Inject NetworkOfferingDetailsDao _ntwkOffDetailsDao; @Inject + NetworkOfferingServiceMapDao networkOfferingServiceMapDao; + @Inject AccountGuestVlanMapDao _accountGuestVlanMapDao; @Inject DataCenterVnetDao _datacenterVnetDao; @@ -365,6 +374,10 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra private BGPService bgpService; @Inject private Ipv6GuestPrefixSubnetNetworkMapDao ipv6GuestPrefixSubnetNetworkMapDao; + @Inject + protected ExtensionHelper extensionHelper; + @Inject + private NetworkExtensionElement networkExtensionElement; @Override public List getNetworkGurus() { @@ -447,6 +460,8 @@ public void setDhcpProviders(final List dhcpProviders) { ClusterDao clusterDao; @Inject RoutedIpv4Manager routedIpv4Manager; + @Inject + private ReservationDao reservationDao; protected StateMachine2 _stateMachine; ScheduledExecutorService _executor; @@ -456,6 +471,28 @@ public void setDhcpProviders(final List dhcpProviders) { HashMap _lastNetworkIdsToFree = new HashMap<>(); + /** + * Returns the full list of network elements to iterate when implementing, + * shutting down, or otherwise orchestrating a network. + * + *

The base list ({@link #networkElements}, wired by Spring) is extended + * at runtime with one transient {@link NetworkExtensionElement} per + * registered {@code NetworkOrchestrator} extension. This keeps the + * Spring bean list free from {@code NetworkExtensionElement} and allows + * dynamic discovery of extensions without a restart.

+ */ + protected List getNetworkElementsIncludingExtensions() { + List extensions = extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator); + if (CollectionUtils.isEmpty(extensions)) { + return networkElements; + } + List combined = new ArrayList<>(networkElements); + for (Extension ext : extensions) { + combined.add(networkExtensionElement.withProviderName(ext.getName())); + } + return combined; + } + private void updateRouterDefaultDns(final VirtualMachineProfile vmProfile, final NicProfile nicProfile) { if (!Type.DomainRouter.equals(vmProfile.getType()) || !nicProfile.isDefaultNic()) { return; @@ -556,6 +593,7 @@ public boolean configure(final String name, final Map params) th defaultVPCOffProviders.put(Service.StaticNat, defaultProviders); defaultVPCOffProviders.put(Service.PortForwarding, defaultProviders); defaultVPCOffProviders.put(Service.Vpn, defaultProviders); + defaultVPCOffProviders.put(Service.Firewall, defaultProviders); Transaction.execute(new TransactionCallbackNoReturn() { @Override @@ -771,7 +809,7 @@ public List setupNetwork(final Account owner, final NetworkOf long related = -1; - for (final NetworkGuru guru : networkGurus) { + for (final NetworkGuru guru : getDesignNetworkGurus(offering)) { final Network network = guru.design(offering, plan, predefined, name, vpcId, owner); if (network == null) { continue; @@ -847,6 +885,46 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { } } + private List getDesignNetworkGurus(final NetworkOffering offering) { + if (!isIsolationMethodNetworkExtension(offering.getId())) { + return networkGurus; + } + + List extensionGurus = networkGurus.stream() + .filter(guru -> ExtensionHelper.NETWORK_EXTENSION_GURU_NAME.equals(guru.getName())) + .collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(extensionGurus)) { + return extensionGurus; + } + + logger.warn("Network offering {} requests {}={}, but {} is not registered; using default guru ordering", + offering.getUuid(), ExtensionHelper.NETWORK_ISOLATION_METHOD_DETAIL_KEY, + ExtensionHelper.NETWORK_EXTENSION_ISOLATION_METHOD, ExtensionHelper.NETWORK_EXTENSION_GURU_NAME); + return networkGurus; + } + + @Override + public boolean isIsolationMethodNetworkExtension(final Long networkOfferingId) { + if (networkOfferingId == null) { + return false; + } + + List providers = networkOfferingServiceMapDao.getDistinctProviders(networkOfferingId); + if (CollectionUtils.isEmpty(providers)) { + return false; + } + + for (String providerName : providers) { + if (!extensionHelper.isNetworkExtensionProvider(providerName)) { + continue; + } + if (extensionHelper.usesNetworkExtensionIsolation(providerName)) { + return true; + } + } + return false; + } + @NotNull private static NetworkVO getNetworkVO(long id, final NetworkOffering offering, final DeploymentPlan plan, final Network predefined, Network network, final NetworkGuru guru, final Account owner, @@ -858,6 +936,7 @@ private static NetworkVO getNetworkVO(long id, final NetworkOffering offering, f vpcId, offering.isRedundantRouter(), predefined.getExternalId()); vo.setDisplayNetwork(isDisplayNetworkEnabled == null || isDisplayNetworkEnabled); vo.setStrechedL2Network(offering.isSupportingStrechedL2()); + vo.setKeepMacAddressOnPublicNic(predefined.getKeepMacAddressOnPublicNic()); return vo; } @@ -1587,6 +1666,9 @@ public Pair implementNetwork(final long networkId, final // implement network elements and re-apply all the network rules implementNetworkElementsAndResources(dest, context, network, offering); + // reload network after implementing the network + network = _networksDao.findById(networkId); + long dcId = dest.getDataCenter().getId(); if (networkMeetsPersistenceCriteria(network, offering, false)) { setupPersistentNetwork(network, offering, dcId); @@ -1680,7 +1762,8 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f } } - for (final NetworkElement element : networkElements) { + List allNetworkElements = getNetworkElementsIncludingExtensions(); + for (final NetworkElement element : allNetworkElements) { if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) { ((AggregatedCommandExecutor) element).prepareAggregatedExecution(network, dest); } @@ -1697,7 +1780,7 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f ex.addProxyObject(_entityMgr.findById(DataCenter.class, network.getDataCenterId()).getUuid()); throw ex; } - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : allNetworkElements) { if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) { if (!((AggregatedCommandExecutor) element).completeAggregatedExecution(network, dest)) { logger.warn("Failed to re-program the network as a part of network {} implement due to aggregated commands execution failure!", network); @@ -1711,7 +1794,7 @@ public void implementNetworkElementsAndResources(final DeployDestination dest, f } reconfigureAndApplyStaticRouteForVpcVpn(network); } finally { - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : allNetworkElements) { if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) { ((AggregatedCommandExecutor) element).cleanupAggregatedExecution(network, dest); } @@ -1732,7 +1815,7 @@ private void reconfigureAndApplyStaticRouteForVpcVpn(Network network) { private void implementNetworkElements(final DeployDestination dest, final ReservationContext context, final Network network, final NetworkOffering offering, final List providersToImplement) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - for (NetworkElement element : networkElements) { + for (NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { // The physicalNetworkId will not get translated into a uuid by the response serializer, @@ -2025,7 +2108,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) { @Override public void configureUpdateInSequence(Network network) { List providers = getNetworkProviders(network.getId()); - for (NetworkElement element : networkElements) { + for (NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providers.contains(element.getProvider())) { if (element instanceof RedundantResource) { ((RedundantResource) element).configureResource(network); @@ -2038,7 +2121,7 @@ public void configureUpdateInSequence(Network network) { public int getResourceCount(Network network) { List providers = getNetworkProviders(network.getId()); int resourceCount = 0; - for (NetworkElement element : networkElements) { + for (NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providers.contains(element.getProvider())) { //currently only one element implements the redundant resource interface if (element instanceof RedundantResource) { @@ -2069,7 +2152,7 @@ public void configureExtraDhcpOptions(Network network, long nicId) { @Override public void finalizeUpdateInSequence(Network network, boolean success) { List providers = getNetworkProviders(network.getId()); - for (NetworkElement element : networkElements) { + for (NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providers.contains(element.getProvider())) { //currently only one element implements the redundant resource interface if (element instanceof RedundantResource) { @@ -2096,7 +2179,7 @@ public void setHypervisorHostname(VirtualMachineProfile vm, DeployDestination de } private void setHypervisorHostnameInNetwork(VirtualMachineProfile vm, DeployDestination dest, Network network, NicProfile profile, boolean migrationSuccessful) { - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (_networkModel.areServicesSupportedInNetwork(network.getId(), Service.UserData) && element instanceof UserDataServiceProvider && (element instanceof ConfigDriveNetworkElement && !migrationSuccessful || element instanceof VirtualRouterElement && migrationSuccessful)) { String errorMsg = String.format("Failed to add hypervisor host name while applying the userdata during the migration of VM %s, " + @@ -2224,7 +2307,7 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy updateNic(nic, network, 1); final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: " @@ -2279,7 +2362,7 @@ public void prepareNicForMigration(final VirtualMachineProfile vm, final DeployD } final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: " @@ -2323,10 +2406,11 @@ public void prepareAllNicsForMigration(final VirtualMachineProfile vm, final Dep } } final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { - throw new CloudRuntimeException(String.format("Service provider %s either doesn't exist or is not enabled in physical network: %s", element.getProvider().getName(), _physicalNetworkDao.findById(network.getPhysicalNetworkId()))); + throw new CloudRuntimeException(String.format("Service provider %s either doesn't exist or is not enabled in physical network: %s", + element.getProvider().getName(), _physicalNetworkDao.findById(network.getPhysicalNetworkId()))); } if (element instanceof NetworkMigrationResponder) { if (!((NetworkMigrationResponder) element).prepareMigration(profile, network, vm, dest, context)) { @@ -2404,7 +2488,7 @@ public void commitNicForMigration(final VirtualMachineProfile src, final Virtual } final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: " @@ -2440,7 +2524,7 @@ public void rollbackNicForMigration(final VirtualMachineProfile src, final Virtu } final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: " @@ -2527,7 +2611,7 @@ public Pair doInTransaction(final TransactionStatus status) final Network network = networkToRelease.first(); final NicProfile profile = networkToRelease.second(); final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { logger.debug("Asking {} to release {}", element.getName(), profile); //NOTE: Context appear to never be used in release method @@ -2590,7 +2674,7 @@ protected void removeNic(final VirtualMachineProfile vm, final NicVO nic) { */ if (nic.getReservationStrategy() == Nic.ReservationStrategy.Create) { final List providersToImplement = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToImplement.contains(element.getProvider())) { logger.debug("Asking {} to release {}, according to the reservation strategy {}.", element.getName(), nic, nic.getReservationStrategy()); try { @@ -2633,6 +2717,10 @@ && isDhcpAccrossMultipleSubnetsSupported(dhcpServiceProvider)) { final NetworkGuru guru = AdapterBase.getAdapterByName(networkGurus, network.getGuruName()); guru.deallocate(network, profile, vm); + if (nic.getReservationStrategy() == Nic.ReservationStrategy.Create) { + applyProfileToNicForRelease(nic, profile); + _nicDao.update(nic.getId(), nic); + } if (BooleanUtils.isNotTrue(preserveNics)) { _nicDao.remove(nic.getId()); } @@ -2714,7 +2802,7 @@ public Network createPrivateNetwork(final long networkOfferingId, final String n return createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, null, owner, null, pNtwk, pNtwk.getDataCenterId(), ACLType.Account, null, vpcId, null, null, true, null, null, null, true, null, null, - null, null, null, null, null, null); + null, null, null, null, null, null, true); } @Override @@ -2725,10 +2813,25 @@ public Network createGuestNetwork(final long networkOfferingId, final String nam final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6, String ip4Dns1, String ip4Dns2, String ip6Dns1, String ip6Dns2, Pair vrIfaceMTUs, Integer networkCidrSize) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { + return createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, + networkDomain, owner, domainId, pNtwk, zoneId, aclType, subdomainAccess, vpcId, ip6Gateway, ip6Cidr, + isDisplayNetworkEnabled, isolatedPvlan, isolatedPvlanType, externalId, false, routerIp, routerIpv6, + ip4Dns1, ip4Dns2, ip6Dns1, ip6Dns2, vrIfaceMTUs, networkCidrSize, true); + } + + @Override + @DB + public Network createGuestNetwork(final long networkOfferingId, final String name, final String displayText, final String gateway, final String cidr, String vlanId, + boolean bypassVlanOverlapCheck, String networkDomain, final Account owner, final Long domainId, final PhysicalNetwork pNtwk, + final long zoneId, final ACLType aclType, Boolean subdomainAccess, final Long vpcId, final String ip6Gateway, final String ip6Cidr, + final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, + String routerIp, String routerIpv6, String ip4Dns1, String ip4Dns2, String ip6Dns1, String ip6Dns2, + Pair vrIfaceMTUs, Integer networkCidrSize, boolean keepMacAddressOnPublicNic) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { // create Isolated/Shared/L2 network return createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, networkDomain, owner, domainId, pNtwk, zoneId, aclType, subdomainAccess, vpcId, ip6Gateway, ip6Cidr, - isDisplayNetworkEnabled, isolatedPvlan, isolatedPvlanType, externalId, false, routerIp, routerIpv6, ip4Dns1, ip4Dns2, ip6Dns1, ip6Dns2, vrIfaceMTUs, networkCidrSize); + isDisplayNetworkEnabled, isolatedPvlan, isolatedPvlanType, externalId, false, routerIp, routerIpv6, + ip4Dns1, ip4Dns2, ip6Dns1, ip6Dns2, vrIfaceMTUs, networkCidrSize, keepMacAddressOnPublicNic); } @DB @@ -2737,7 +2840,8 @@ private Network createGuestNetwork(final long networkOfferingId, final String na final long zoneId, final ACLType aclType, Boolean subdomainAccess, final Long vpcId, final String ip6Gateway, final String ip6Cidr, final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, final Boolean isPrivateNetwork, String routerIp, String routerIpv6, final String ip4Dns1, final String ip4Dns2, - final String ip6Dns1, final String ip6Dns2, Pair vrIfaceMTUs, Integer networkCidrSize) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { + final String ip6Dns1, final String ip6Dns2, Pair vrIfaceMTUs, Integer networkCidrSize, + boolean keepMacAddressOnPublicNic) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { final NetworkOfferingVO ntwkOff = _networkOfferingDao.findById(networkOfferingId); final DataCenterVO zone = _dcDao.findById(zoneId); @@ -2747,12 +2851,6 @@ private Network createGuestNetwork(final long networkOfferingId, final String na return null; } - final boolean updateResourceCount = resourceCountNeedsUpdate(ntwkOff, aclType); - //check resource limits - if (updateResourceCount) { - _resourceLimitMgr.checkResourceLimit(owner, ResourceType.network, isDisplayNetworkEnabled); - } - // Validate network offering if (ntwkOff.getState() != NetworkOffering.State.Enabled) { // see NetworkOfferingVO @@ -2771,218 +2869,219 @@ private Network createGuestNetwork(final long networkOfferingId, final String na boolean ipv6 = false; - if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { - ipv6 = true; - } - // Validate zone - if (zone.getNetworkType() == NetworkType.Basic) { - // In Basic zone the network should have aclType=Domain, domainId=1, subdomainAccess=true - if (aclType == null || aclType != ACLType.Domain) { - throw new InvalidParameterValueException("Only AclType=Domain can be specified for network creation in Basic zone"); - } - - // Only one guest network is supported in Basic zone - final List guestNetworks = _networksDao.listByZoneAndTrafficType(zone.getId(), TrafficType.Guest); - if (!guestNetworks.isEmpty()) { - throw new InvalidParameterValueException("Can't have more than one Guest network in zone with network type " + NetworkType.Basic); + try (CheckedReservation networkReservation = new CheckedReservation(owner, domainId, Resource.ResourceType.network, null, null, 1L, reservationDao, _resourceLimitMgr)) { + if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { + ipv6 = true; } + // Validate zone + if (zone.getNetworkType() == NetworkType.Basic) { + // In Basic zone the network should have aclType=Domain, domainId=1, subdomainAccess=true + if (aclType == null || aclType != ACLType.Domain) { + throw new InvalidParameterValueException("Only AclType=Domain can be specified for network creation in Basic zone"); + } - // if zone is basic, only Shared network offerings w/o source nat service are allowed - if (!(ntwkOff.getGuestType() == GuestType.Shared && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat))) { - throw new InvalidParameterValueException("For zone of type " + NetworkType.Basic + " only offerings of " + "guestType " + GuestType.Shared + " with disabled " - + Service.SourceNat.getName() + " service are allowed"); - } + // Only one guest network is supported in Basic zone + final List guestNetworks = _networksDao.listByZoneAndTrafficType(zone.getId(), TrafficType.Guest); + if (!guestNetworks.isEmpty()) { + throw new InvalidParameterValueException("Can't have more than one Guest network in zone with network type " + NetworkType.Basic); + } - if (domainId == null || domainId != Domain.ROOT_DOMAIN) { - throw new InvalidParameterValueException("Guest network in Basic zone should be dedicated to ROOT domain"); - } + // if zone is basic, only Shared network offerings w/o source nat service are allowed + if (!(ntwkOff.getGuestType() == GuestType.Shared && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat))) { + throw new InvalidParameterValueException("For zone of type " + NetworkType.Basic + " only offerings of " + "guestType " + GuestType.Shared + " with disabled " + + Service.SourceNat.getName() + " service are allowed"); + } - if (subdomainAccess == null) { - subdomainAccess = true; - } else if (!subdomainAccess) { - throw new InvalidParameterValueException("Subdomain access should be set to true for the" + " guest network in the Basic zone"); - } + if (domainId == null || domainId != Domain.ROOT_DOMAIN) { + throw new InvalidParameterValueException("Guest network in Basic zone should be dedicated to ROOT domain"); + } - if (vlanId == null) { - vlanId = Vlan.UNTAGGED; - } else { - if (!vlanId.equalsIgnoreCase(Vlan.UNTAGGED)) { - throw new InvalidParameterValueException("Only vlan " + Vlan.UNTAGGED + " can be created in " + "the zone of type " + NetworkType.Basic); + if (subdomainAccess == null) { + subdomainAccess = true; + } else if (!subdomainAccess) { + throw new InvalidParameterValueException("Subdomain access should be set to true for the" + " guest network in the Basic zone"); } - } - } else if (zone.getNetworkType() == NetworkType.Advanced) { - if (zone.isSecurityGroupEnabled()) { - if (isolatedPvlan != null) { - throw new InvalidParameterValueException("Isolated Private VLAN is not supported with security group!"); + if (vlanId == null) { + vlanId = Vlan.UNTAGGED; + } else { + if (!vlanId.equalsIgnoreCase(Vlan.UNTAGGED)) { + throw new InvalidParameterValueException("Only vlan " + Vlan.UNTAGGED + " can be created in " + "the zone of type " + NetworkType.Basic); + } } - // Only Account specific Isolated network with sourceNat service disabled are allowed in security group - // enabled zone - if ((ntwkOff.getGuestType() != GuestType.Shared) && (ntwkOff.getGuestType() != GuestType.L2)) { - throw new InvalidParameterValueException("Only shared or L2 guest network can be created in security group enabled zone"); + + } else if (zone.getNetworkType() == NetworkType.Advanced) { + if (zone.isSecurityGroupEnabled()) { + if (isolatedPvlan != null) { + throw new InvalidParameterValueException("Isolated Private VLAN is not supported with security group!"); + } + // Only Account specific Isolated network with sourceNat service disabled are allowed in security group + // enabled zone + if ((ntwkOff.getGuestType() != GuestType.Shared) && (ntwkOff.getGuestType() != GuestType.L2)) { + throw new InvalidParameterValueException("Only shared or L2 guest network can be created in security group enabled zone"); + } + if (_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat)) { + throw new InvalidParameterValueException("Service SourceNat is not allowed in security group enabled zone"); + } } - if (_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat)) { - throw new InvalidParameterValueException("Service SourceNat is not allowed in security group enabled zone"); + + //don't allow eip/elb networks in Advance zone + if (ntwkOff.isElasticIp() || ntwkOff.isElasticLb()) { + throw new InvalidParameterValueException("Elastic IP and Elastic LB services are supported in zone of type " + NetworkType.Basic); } } - //don't allow eip/elb networks in Advance zone - if (ntwkOff.isElasticIp() || ntwkOff.isElasticLb()) { - throw new InvalidParameterValueException("Elastic IP and Elastic LB services are supported in zone of type " + NetworkType.Basic); + if (ipv6 && !GuestType.Shared.equals(ntwkOff.getGuestType())) { + _networkModel.checkIp6CidrSizeEqualTo64(ip6Cidr); } - } - - if (ipv6 && !GuestType.Shared.equals(ntwkOff.getGuestType())) { - _networkModel.checkIp6CidrSizeEqualTo64(ip6Cidr); - } - //TODO(VXLAN): Support VNI specified - // VlanId can be specified only when network offering supports it - final boolean vlanSpecified = vlanId != null; - if (vlanSpecified != ntwkOff.isSpecifyVlan()) { - if (vlanSpecified) { - if (!isSharedNetworkWithoutSpecifyVlan(ntwkOff) && !isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { - throw new InvalidParameterValueException("Can't specify vlan; corresponding offering says specifyVlan=false"); + //TODO(VXLAN): Support VNI specified + // VlanId can be specified only when network offering supports it + final boolean vlanSpecified = vlanId != null; + if (vlanSpecified != ntwkOff.isSpecifyVlan()) { + if (vlanSpecified) { + if (!isSharedNetworkWithoutSpecifyVlan(ntwkOff) && !isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { + throw new InvalidParameterValueException("Can't specify vlan; corresponding offering says specifyVlan=false"); + } + } else { + throw new InvalidParameterValueException("Vlan has to be specified; corresponding offering says specifyVlan=true"); } - } else { - throw new InvalidParameterValueException("Vlan has to be specified; corresponding offering says specifyVlan=true"); } - } - if (vlanSpecified) { - URI uri = encodeVlanIdIntoBroadcastUri(vlanId, pNtwk); - // Aux: generate secondary URI for secondary VLAN ID (if provided) for performing checks - URI secondaryUri = StringUtils.isNotBlank(isolatedPvlan) ? BroadcastDomainType.fromString(isolatedPvlan) : null; - if (isSharedNetworkWithoutSpecifyVlan(ntwkOff) || isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { - bypassVlanOverlapCheck = true; - } - //don't allow to specify vlan tag used by physical network for dynamic vlan allocation - if (!(bypassVlanOverlapCheck && (ntwkOff.getGuestType() == GuestType.Shared || isPrivateNetwork)) - && _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(uri)).size() > 0) { - throw new InvalidParameterValueException("The VLAN tag to use for new guest network, " + vlanId + " is already being used for dynamic vlan allocation for the guest network in zone " - + zone.getName()); - } - if (secondaryUri != null && !(bypassVlanOverlapCheck && ntwkOff.getGuestType() == GuestType.Shared) && - _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(secondaryUri)).size() > 0) { - throw new InvalidParameterValueException(String.format( - "The VLAN tag for isolated PVLAN %s is already being used for dynamic vlan allocation for the guest network in zone %s", - isolatedPvlan, zone)); - } - if (!UuidUtils.isUuid(vlanId)) { - // For Isolated and L2 networks, don't allow to create network with vlan that already exists in the zone - if (!hasGuestBypassVlanOverlapCheck(bypassVlanOverlapCheck, ntwkOff, isPrivateNetwork)) { - if (_networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), null).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network vlans in zone %s", - vlanId, zone)); - } else if (secondaryUri != null && _networksDao.listByZoneAndUriAndGuestType(zoneId, secondaryUri.toString(), null).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network vlans in zone %s", - isolatedPvlan, zone)); - } else { - final List dcVnets = _datacenterVnetDao.findVnet(zoneId, BroadcastDomainType.getValue(uri)); - //for the network that is created as part of private gateway, - //the vnet is not coming from the data center vnet table, so the list can be empty - if (!dcVnets.isEmpty()) { - final DataCenterVnetVO dcVnet = dcVnets.get(0); - // Fail network creation if specified vlan is dedicated to a different account - if (dcVnet.getAccountGuestVlanMapId() != null) { - final Long accountGuestVlanMapId = dcVnet.getAccountGuestVlanMapId(); - final AccountGuestVlanMapVO map = _accountGuestVlanMapDao.findById(accountGuestVlanMapId); - if (map.getAccountId() != owner.getAccountId()) { - throw new InvalidParameterValueException("Vlan " + vlanId + " is dedicated to a different account"); - } - // Fail network creation if owner has a dedicated range of vlans but the specified vlan belongs to the system pool - } else { - final List maps = _accountGuestVlanMapDao.listAccountGuestVlanMapsByAccount(owner.getAccountId()); - if (maps != null && !maps.isEmpty()) { - final int vnetsAllocatedToAccount = _datacenterVnetDao.countVnetsAllocatedToAccount(zoneId, owner.getAccountId()); - final int vnetsDedicatedToAccount = _datacenterVnetDao.countVnetsDedicatedToAccount(zoneId, owner.getAccountId()); - if (vnetsAllocatedToAccount < vnetsDedicatedToAccount) { - throw new InvalidParameterValueException("Specified vlan " + vlanId + " doesn't belong" + " to the vlan range dedicated to the owner " - + owner.getAccountName()); + if (vlanSpecified) { + URI uri = encodeVlanIdIntoBroadcastUri(vlanId, pNtwk); + // Aux: generate secondary URI for secondary VLAN ID (if provided) for performing checks + URI secondaryUri = StringUtils.isNotBlank(isolatedPvlan) ? BroadcastDomainType.fromString(isolatedPvlan) : null; + if (isSharedNetworkWithoutSpecifyVlan(ntwkOff) || isPrivateGatewayWithoutSpecifyVlan(ntwkOff)) { + bypassVlanOverlapCheck = true; + } + //don't allow to specify vlan tag used by physical network for dynamic vlan allocation + if (!(bypassVlanOverlapCheck && (ntwkOff.getGuestType() == GuestType.Shared || isPrivateNetwork)) + && _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(uri)).size() > 0) { + throw new InvalidParameterValueException("The VLAN tag to use for new guest network, " + vlanId + " is already being used for dynamic vlan allocation for the guest network in zone " + + zone.getName()); + } + if (secondaryUri != null && !(bypassVlanOverlapCheck && ntwkOff.getGuestType() == GuestType.Shared) && + _dcDao.findVnet(zoneId, pNtwk.getId(), BroadcastDomainType.getValue(secondaryUri)).size() > 0) { + throw new InvalidParameterValueException(String.format( + "The VLAN tag for isolated PVLAN %s is already being used for dynamic vlan allocation for the guest network in zone %s", + isolatedPvlan, zone)); + } + if (!UuidUtils.isUuid(vlanId)) { + // For Isolated and L2 networks, don't allow to create network with vlan that already exists in the zone + if (!hasGuestBypassVlanOverlapCheck(bypassVlanOverlapCheck, ntwkOff, isPrivateNetwork)) { + if (_networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), null).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network vlans in zone %s", + vlanId, zone)); + } else if (secondaryUri != null && _networksDao.listByZoneAndUriAndGuestType(zoneId, secondaryUri.toString(), null).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network vlans in zone %s", + isolatedPvlan, zone)); + } else { + final List dcVnets = _datacenterVnetDao.findVnet(zoneId, BroadcastDomainType.getValue(uri)); + //for the network that is created as part of private gateway, + //the vnet is not coming from the data center vnet table, so the list can be empty + if (!dcVnets.isEmpty()) { + final DataCenterVnetVO dcVnet = dcVnets.get(0); + // Fail network creation if specified vlan is dedicated to a different account + if (dcVnet.getAccountGuestVlanMapId() != null) { + final Long accountGuestVlanMapId = dcVnet.getAccountGuestVlanMapId(); + final AccountGuestVlanMapVO map = _accountGuestVlanMapDao.findById(accountGuestVlanMapId); + if (map.getAccountId() != owner.getAccountId()) { + throw new InvalidParameterValueException("Vlan " + vlanId + " is dedicated to a different account"); + } + // Fail network creation if owner has a dedicated range of vlans but the specified vlan belongs to the system pool + } else { + final List maps = _accountGuestVlanMapDao.listAccountGuestVlanMapsByAccount(owner.getAccountId()); + if (maps != null && !maps.isEmpty()) { + final int vnetsAllocatedToAccount = _datacenterVnetDao.countVnetsAllocatedToAccount(zoneId, owner.getAccountId()); + final int vnetsDedicatedToAccount = _datacenterVnetDao.countVnetsDedicatedToAccount(zoneId, owner.getAccountId()); + if (vnetsAllocatedToAccount < vnetsDedicatedToAccount) { + throw new InvalidParameterValueException("Specified vlan " + vlanId + " doesn't belong" + " to the vlan range dedicated to the owner " + + owner.getAccountName()); + } } } } } - } - } else { - // don't allow to creating shared network with given Vlan ID, if there already exists a isolated network or - // shared network with same Vlan ID in the zone - if (!bypassVlanOverlapCheck && _networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), GuestType.Isolated).size() > 0) { - throw new InvalidParameterValueException(String.format( - "There is an existing isolated/shared network that overlaps with vlan id:%s in zone %s", vlanId, zone)); + } else { + // don't allow to creating shared network with given Vlan ID, if there already exists a isolated network or + // shared network with same Vlan ID in the zone + if (!bypassVlanOverlapCheck && _networksDao.listByZoneAndUriAndGuestType(zoneId, uri.toString(), GuestType.Isolated).size() > 0) { + throw new InvalidParameterValueException(String.format( + "There is an existing isolated/shared network that overlaps with vlan id:%s in zone %s", vlanId, zone)); + } } } - } - } + } - // If networkDomain is not specified, take it from the global configuration - if (_networkModel.areServicesSupportedByNetworkOffering(networkOfferingId, Service.Dns)) { - final Map dnsCapabilities = _networkModel.getNetworkOfferingServiceCapabilities(_entityMgr.findById(NetworkOffering.class, networkOfferingId), - Service.Dns); - final String isUpdateDnsSupported = dnsCapabilities.get(Capability.AllowDnsSuffixModification); - if (isUpdateDnsSupported == null || !Boolean.valueOf(isUpdateDnsSupported)) { - if (networkDomain != null) { - // TBD: NetworkOfferingId and zoneId. Send uuids instead. - throw new InvalidParameterValueException(String.format( - "Domain name change is not supported by network offering id=%d in zone %s", - networkOfferingId, zone)); - } - } else { - if (networkDomain == null) { - // 1) Get networkDomain from the corresponding account/domain/zone - if (aclType == ACLType.Domain) { - networkDomain = _networkModel.getDomainNetworkDomain(domainId, zoneId); - } else if (aclType == ACLType.Account) { - networkDomain = _networkModel.getAccountNetworkDomain(owner.getId(), zoneId); + // If networkDomain is not specified, take it from the global configuration + if (_networkModel.areServicesSupportedByNetworkOffering(networkOfferingId, Service.Dns)) { + final Map dnsCapabilities = _networkModel.getNetworkOfferingServiceCapabilities(_entityMgr.findById(NetworkOffering.class, networkOfferingId), + Service.Dns); + final String isUpdateDnsSupported = dnsCapabilities.get(Capability.AllowDnsSuffixModification); + if (isUpdateDnsSupported == null || !Boolean.valueOf(isUpdateDnsSupported)) { + if (networkDomain != null) { + // TBD: NetworkOfferingId and zoneId. Send uuids instead. + throw new InvalidParameterValueException(String.format( + "Domain name change is not supported by network offering id=%d in zone %s", + networkOfferingId, zone)); } - - // 2) If null, generate networkDomain using domain suffix from the global config variables + } else { if (networkDomain == null) { - networkDomain = "cs" + Long.toHexString(owner.getId()) + GuestDomainSuffix.valueIn(zoneId); - } + // 1) Get networkDomain from the corresponding account/domain/zone + if (aclType == ACLType.Domain) { + networkDomain = _networkModel.getDomainNetworkDomain(domainId, zoneId); + } else if (aclType == ACLType.Account) { + networkDomain = _networkModel.getAccountNetworkDomain(owner.getId(), zoneId); + } - } else { - // validate network domain - if (!NetUtils.verifyDomainName(networkDomain)) { - throw new InvalidParameterValueException("Invalid network domain. Total length shouldn't exceed 190 chars. Each domain " - + "label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', " - + "and the hyphen ('-'); can't start or end with \"-\""); + // 2) If null, generate networkDomain using domain suffix from the global config variables + if (networkDomain == null) { + networkDomain = "cs" + Long.toHexString(owner.getId()) + GuestDomainSuffix.valueIn(zoneId); + } + + } else { + // validate network domain + if (!NetUtils.verifyDomainName(networkDomain)) { + throw new InvalidParameterValueException("Invalid network domain. Total length shouldn't exceed 190 chars. Each domain " + + "label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', " + + "and the hyphen ('-'); can't start or end with \"-\""); + } } } } - } - // In Advance zone Cidr for Shared networks and Isolated networks w/o source nat service can't be NULL - 2.2.x - // limitation, remove after we introduce support for multiple ip ranges - // with different Cidrs for the same Shared network - final boolean cidrRequired = zone.getNetworkType() == NetworkType.Advanced - && ntwkOff.getTrafficType() == TrafficType.Guest - && (ntwkOff.getGuestType() == GuestType.Shared || (ntwkOff.getGuestType() == GuestType.Isolated - && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat) - && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.Gateway))); - if (cidr == null && ip6Cidr == null && cidrRequired) { - if (ntwkOff.getGuestType() == GuestType.Shared) { - throw new InvalidParameterValueException(String.format("Gateway/netmask are required when creating %s networks.", Network.GuestType.Shared)); - } else { - throw new InvalidParameterValueException("gateway/netmask are required when create network of" + " type " + GuestType.Isolated + " with service " + Service.SourceNat.getName() + " disabled"); + // In Advance zone Cidr for Shared networks and Isolated networks w/o source nat service can't be NULL - 2.2.x + // limitation, remove after we introduce support for multiple ip ranges + // with different Cidrs for the same Shared network + final boolean cidrRequired = zone.getNetworkType() == NetworkType.Advanced + && ntwkOff.getTrafficType() == TrafficType.Guest + && (ntwkOff.getGuestType() == GuestType.Shared || (ntwkOff.getGuestType() == GuestType.Isolated + && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.SourceNat) + && !_networkModel.areServicesSupportedByNetworkOffering(ntwkOff.getId(), Service.Gateway))); + if (cidr == null && ip6Cidr == null && cidrRequired) { + if (ntwkOff.getGuestType() == GuestType.Shared) { + throw new InvalidParameterValueException(String.format("Gateway/netmask are required when creating %s networks.", Network.GuestType.Shared)); + } else { + throw new InvalidParameterValueException("gateway/netmask are required when create network of" + " type " + GuestType.Isolated + " with service " + Service.SourceNat.getName() + " disabled"); + } } - } - checkL2OfferingServices(ntwkOff); + checkL2OfferingServices(ntwkOff); - // No cidr can be specified in Basic zone - if (zone.getNetworkType() == NetworkType.Basic && cidr != null) { - throw new InvalidParameterValueException("StartIp/endIp/gateway/netmask can't be specified for zone of type " + NetworkType.Basic); - } + // No cidr can be specified in Basic zone + if (zone.getNetworkType() == NetworkType.Basic && cidr != null) { + throw new InvalidParameterValueException("StartIp/endIp/gateway/netmask can't be specified for zone of type " + NetworkType.Basic); + } - // Check if cidr is RFC1918 compliant if the network is Guest Isolated for IPv4 - if (cidr != null && (ntwkOff.getGuestType() == Network.GuestType.Isolated && ntwkOff.getTrafficType() == TrafficType.Guest) && - !NetUtils.validateGuestCidr(cidr, !ConfigurationManager.AllowNonRFC1918CompliantIPs.value())) { + // Check if cidr is RFC1918 compliant if the network is Guest Isolated for IPv4 + if (cidr != null && (ntwkOff.getGuestType() == Network.GuestType.Isolated && ntwkOff.getTrafficType() == TrafficType.Guest) && + !NetUtils.validateGuestCidr(cidr, !ConfigurationManager.AllowNonRFC1918CompliantIPs.value())) { throw new InvalidParameterValueException("Virtual Guest Cidr " + cidr + " is not RFC 1918 or 6598 compliant"); - } + } final String networkDomainFinal = networkDomain; final String vlanIdFinal = vlanId; @@ -2998,75 +3097,75 @@ public Network doInTransaction(final TransactionStatus status) { final NetworkVO userNetwork = new NetworkVO(); userNetwork.setNetworkDomain(networkDomainFinal); - if (cidr != null && gateway != null) { - userNetwork.setCidr(cidr); - userNetwork.setGateway(gateway); - } - - if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { - userNetwork.setIp6Cidr(ip6Cidr); - userNetwork.setIp6Gateway(ip6Gateway); - } + if (cidr != null && gateway != null) { + userNetwork.setCidr(cidr); + userNetwork.setGateway(gateway); + } - if (externalId != null) { - userNetwork.setExternalId(externalId); - } + if (StringUtils.isNoneBlank(ip6Gateway, ip6Cidr)) { + userNetwork.setIp6Cidr(ip6Cidr); + userNetwork.setIp6Gateway(ip6Gateway); + } - if (StringUtils.isNotBlank(routerIp)) { - userNetwork.setRouterIp(routerIp); - } + if (externalId != null) { + userNetwork.setExternalId(externalId); + } - if (StringUtils.isNotBlank(routerIpv6)) { - userNetwork.setRouterIpv6(routerIpv6); - } + if (StringUtils.isNotBlank(routerIp)) { + userNetwork.setRouterIp(routerIp); + } - if (vrIfaceMTUs != null) { - if (vrIfaceMTUs.first() != null && vrIfaceMTUs.first() > 0) { - userNetwork.setPublicMtu(vrIfaceMTUs.first()); - } else { - userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); + if (StringUtils.isNotBlank(routerIpv6)) { + userNetwork.setRouterIpv6(routerIpv6); } - if (vrIfaceMTUs.second() != null && vrIfaceMTUs.second() > 0) { - userNetwork.setPrivateMtu(vrIfaceMTUs.second()); + if (vrIfaceMTUs != null) { + if (vrIfaceMTUs.first() != null && vrIfaceMTUs.first() > 0) { + userNetwork.setPublicMtu(vrIfaceMTUs.first()); + } else { + userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); + } + + if (vrIfaceMTUs.second() != null && vrIfaceMTUs.second() > 0) { + userNetwork.setPrivateMtu(vrIfaceMTUs.second()); + } else { + userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); + } } else { + userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); } - } else { - userNetwork.setPublicMtu(Integer.valueOf(NetworkService.VRPublicInterfaceMtu.defaultValue())); - userNetwork.setPrivateMtu(Integer.valueOf(NetworkService.VRPrivateInterfaceMtu.defaultValue())); - } - if (!GuestType.L2.equals(userNetwork.getGuestType())) { - if (StringUtils.isNotBlank(ip4Dns1)) { - userNetwork.setDns1(ip4Dns1); - } - if (StringUtils.isNotBlank(ip4Dns2)) { - userNetwork.setDns2(ip4Dns2); - } - if (StringUtils.isNotBlank(ip6Dns1)) { - userNetwork.setIp6Dns1(ip6Dns1); - } - if (StringUtils.isNotBlank(ip6Dns2)) { - userNetwork.setIp6Dns2(ip6Dns2); + if (!GuestType.L2.equals(userNetwork.getGuestType())) { + if (StringUtils.isNotBlank(ip4Dns1)) { + userNetwork.setDns1(ip4Dns1); + } + if (StringUtils.isNotBlank(ip4Dns2)) { + userNetwork.setDns2(ip4Dns2); + } + if (StringUtils.isNotBlank(ip6Dns1)) { + userNetwork.setIp6Dns1(ip6Dns1); + } + if (StringUtils.isNotBlank(ip6Dns2)) { + userNetwork.setIp6Dns2(ip6Dns2); + } } - } - if (vlanIdFinal != null) { - if (isolatedPvlan == null) { - URI uri = null; - if (UuidUtils.isUuid(vlanIdFinal)) { - //Logical router's UUID provided as VLAN_ID - userNetwork.setVlanIdAsUUID(vlanIdFinal); //Set transient field - } else { - uri = encodeVlanIdIntoBroadcastUri(vlanIdFinal, pNtwk); - } + if (vlanIdFinal != null) { + if (isolatedPvlan == null) { + URI uri = null; + if (UuidUtils.isUuid(vlanIdFinal)) { + //Logical router's UUID provided as VLAN_ID + userNetwork.setVlanIdAsUUID(vlanIdFinal); //Set transient field + } else { + uri = encodeVlanIdIntoBroadcastUri(vlanIdFinal, pNtwk); + } - if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString()).size() > 0) { - throw new InvalidParameterValueException(String.format( - "Network with vlan %s already exists or overlaps with other network pvlans in zone %s", - vlanIdFinal, zone)); - } + if (_networksDao.listByPhysicalNetworkPvlan(physicalNetworkId, uri.toString()).size() > 0) { + throw new InvalidParameterValueException(String.format( + "Network with vlan %s already exists or overlaps with other network pvlans in zone %s", + vlanIdFinal, zone)); + } userNetwork.setBroadcastUri(uri); if (!vlanIdFinal.equalsIgnoreCase(Vlan.UNTAGGED)) { @@ -3090,6 +3189,7 @@ public Network doInTransaction(final TransactionStatus status) { } } userNetwork.setNetworkCidrSize(networkCidrSize); + userNetwork.setKeepMacAddressOnPublicNic(keepMacAddressOnPublicNic); final List networks = setupNetwork(owner, ntwkOff, userNetwork, plan, name, displayText, true, domainId, aclType, subdomainAccessFinal, vpcId, isDisplayNetworkEnabled); Network network; @@ -3110,8 +3210,8 @@ public Network doInTransaction(final TransactionStatus status) { } } - if (updateResourceCount) { - _resourceLimitMgr.incrementResourceCount(owner.getId(), ResourceType.network, isDisplayNetworkEnabled); + if (isResourceCountUpdateNeeded(ntwkOff)) { + changeAccountResourceCountOrRecalculateDomainResourceCount(owner.getAccountId(), domainId, isDisplayNetworkEnabled, true); } UsageEventUtils.publishNetworkCreation(network); @@ -3119,9 +3219,10 @@ public Network doInTransaction(final TransactionStatus status) { } }); - CallContext.current().setEventDetails("Network Id: " + network.getId()); + CallContext.current().setEventDetails("Network ID: " + network.getUuid()); CallContext.current().putContextParameter(Network.class, network.getUuid()); return network; + } } @Override @@ -3304,7 +3405,7 @@ public boolean shutdownNetworkElementsAndResources(final ReservationContext cont // 2) Shutdown all the network elements boolean success = true; - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToShutdown.contains(element.getProvider())) { try { logger.debug("Sending network shutdown to {}", element.getName()); @@ -3415,7 +3516,7 @@ public boolean destroyNetwork(final long networkId, final ReservationContext con // get providers to destroy final List providersToDestroy = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (providersToDestroy.contains(element.getProvider())) { try { logger.debug("Sending destroy to {}", element); @@ -3487,9 +3588,8 @@ public List doInTransaction(TransactionStatus status) { } final NetworkOffering ntwkOff = _entityMgr.findById(NetworkOffering.class, networkFinal.getNetworkOfferingId()); - final boolean updateResourceCount = resourceCountNeedsUpdate(ntwkOff, networkFinal.getAclType()); - if (updateResourceCount) { - _resourceLimitMgr.decrementResourceCount(networkFinal.getAccountId(), ResourceType.network, networkFinal.getDisplayNetwork()); + if (isResourceCountUpdateNeeded(ntwkOff)) { + changeAccountResourceCountOrRecalculateDomainResourceCount(networkFinal.getAccountId(), networkFinal.getDomainId(), networkFinal.getDisplayNetwork(), false); } } return deletedVlans.second(); @@ -3512,6 +3612,23 @@ public List doInTransaction(TransactionStatus status) { return success; } + /** + * If it is a shared network with {@link ACLType#Domain}, it will belong to account {@link Account#ACCOUNT_ID_SYSTEM} and the resources will be not incremented for the + * domain. Therefore, we force the recalculation of the domain's resource count in this case. Otherwise, it will change the count for the account owner. + * @param incrementAccountResourceCount If true, the account resource count will be incremented by 1; otherwise, it will decremented by 1. + */ + private void changeAccountResourceCountOrRecalculateDomainResourceCount(Long accountId, Long domainId, boolean displayNetwork, boolean incrementAccountResourceCount) { + if (Account.ACCOUNT_ID_SYSTEM == accountId && ObjectUtils.isNotEmpty(domainId)) { + _resourceLimitMgr.recalculateDomainResourceCount(domainId, ResourceType.network, null); + } else { + if (incrementAccountResourceCount) { + _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.network, displayNetwork); + } else { + _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.network, displayNetwork); + } + } + } + private void publishDeletedVlanRanges(List deletedVlanRangeToPublish) { if (CollectionUtils.isNotEmpty(deletedVlanRangeToPublish)) { for (VlanVO vlan : deletedVlanRangeToPublish) { @@ -3521,10 +3638,8 @@ private void publishDeletedVlanRanges(List deletedVlanRangeToPublish) { } @Override - public boolean resourceCountNeedsUpdate(final NetworkOffering ntwkOff, final ACLType aclType) { - //Update resource count only for Isolated account specific non-system networks - final boolean updateResourceCount = ntwkOff.getGuestType() == GuestType.Isolated && !ntwkOff.isSystemOnly() && aclType == ACLType.Account; - return updateResourceCount; + public boolean isResourceCountUpdateNeeded(NetworkOffering networkOffering) { + return !networkOffering.isSystemOnly(); } protected Pair> deleteVlansInNetwork(final NetworkVO network, final long userId, final Account callerAccount) { @@ -3772,7 +3887,7 @@ public boolean areRoutersRunning(final List routers) { public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProfile, NicProfile nicProfile) { final List networkProviders = getNetworkProviders(network.getId()); - for (final NetworkElement element : networkElements) { + for (final NetworkElement element : getNetworkElementsIncludingExtensions()) { if (networkProviders.contains(element.getProvider())) { if (!_networkModel.isProviderEnabledInPhysicalNetwork(_networkModel.getPhysicalNetworkId(network), element.getProvider().getName())) { throw new CloudRuntimeException("Service provider " + element.getProvider().getName() + " either doesn't exist or is not enabled in physical network id: " @@ -3789,6 +3904,17 @@ public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProf } } } + if (vmProfile.getType() == Type.User && element.getProvider() != null) { + if (_networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dns) + && _networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dns, element.getProvider()) && element instanceof DnsServiceProvider) { + final DnsServiceProvider sp = (DnsServiceProvider) element; + try { + sp.removeDnsEntry(network, nicProfile, vmProfile); + } catch (ResourceUnavailableException e) { + logger.error("Failed to remove dns entry due to: ", e); + } + } + } } } } @@ -3808,7 +3934,7 @@ public void cleanupNicDhcpDnsEntry(Network network, VirtualMachineProfile vmProf * @throws InsufficientCapacityException */ private boolean rollingRestartRouters(final NetworkVO network, final NetworkOffering offering, final DeployDestination dest, final ReservationContext context) throws ResourceUnavailableException, ConcurrentOperationException, InsufficientCapacityException { - if (!NetworkOrchestrationService.RollingRestartEnabled.value()) { + if (!isRollingRestartSupport(network)) { if (shutdownNetworkElementsAndResources(context, true, network)) { implementNetworkElementsAndResources(dest, context, network, offering); return true; @@ -3856,6 +3982,20 @@ private boolean rollingRestartRouters(final NetworkVO network, final NetworkOffe return areRoutersRunning(routerDao.findByNetwork(network.getId())); } + private boolean isRollingRestartSupport(final NetworkVO network) { + if (!NetworkOrchestrator.RollingRestartEnabled.value()) { + return false; + } + List services = _ntwkSrvcDao.getServicesInNetwork(network.getId()); + for (NetworkServiceMapVO service : services) { + NetworkElement element = _networkModel.getElementImplementingProvider(service.getProvider()); + if (element == null || !element.rollingRestartSupported()) { + return false; + } + } + return true; + } + private void setRestartRequired(final NetworkVO network, final boolean restartRequired) { logger.debug("Marking network {} with restartRequired={}", network, restartRequired); network.setRestartRequired(restartRequired); @@ -4417,6 +4557,12 @@ public Map finalizeServicesAndProvidersForNetwork(final NetworkO if (provider == null) { provider = _networkModel.getDefaultUniqueProviderForService(service).getName(); + } else { + final Provider resolvedProvider = _networkModel.resolveProvider(provider); + if (resolvedProvider == null) { + throw new InvalidParameterValueException("Invalid provider " + provider + " configured for service " + service); + } + provider = resolvedProvider.getName(); } // check that provider is supported @@ -4442,7 +4588,10 @@ private List getNetworkProviders(final long networkId) { final List providerNames = _ntwkSrvcDao.getDistinctProviders(networkId); final List providers = new ArrayList<>(); for (final String providerName : providerNames) { - providers.add(Network.Provider.getProvider(providerName)); + final Provider provider = _networkModel.resolveProvider(providerName); + if (provider != null) { + providers.add(provider); + } } return providers; @@ -4608,7 +4757,7 @@ private Map> getServiceProvidersMap(final long networkId) if (providers == null) { providers = new HashSet<>(); } - providers.add(Provider.getProvider(nsm.getProvider())); + providers.add(_networkModel.resolveProvider(nsm.getProvider())); map.put(Service.getService(nsm.getService()), providers); } return map; @@ -4893,10 +5042,10 @@ public void unmanageNics(VirtualMachineProfile vm) { @Override public void expungeLbVmRefs(List vmIds, Long batchSize) { - if (CollectionUtils.isEmpty(networkElements) || CollectionUtils.isEmpty(vmIds)) { + if (CollectionUtils.isEmpty(vmIds)) { return; } - for (NetworkElement element : networkElements) { + for (NetworkElement element : getNetworkElementsIncludingExtensions()) { if (element instanceof LoadBalancingServiceProvider) { LoadBalancingServiceProvider lbProvider = (LoadBalancingServiceProvider)element; lbProvider.expungeLbVmRefs(vmIds, batchSize); @@ -4917,8 +5066,9 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes, - GuestDomainSuffix, NetworkThrottlingRate, MinVRVersion, + GuestDomainSuffix, NetworkThrottlingRate, VmNetworkThrottlingRate, MinVRVersion, DhcpLeaseTimeout, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, - TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN}; + TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, + NETWORK_LB_HAPROXY_IDLE_TIMEOUT}; } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java index 933b4e0c5ce6..403e83bccf99 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/StorageOrchestrator.java @@ -25,9 +25,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -36,10 +40,22 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.MigrateBackupsBetweenSecondaryStoragesCommand; +import com.cloud.agent.api.MigrateBetweenSecondaryStoragesCommandAnswer; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.template.TemplateManager; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; import org.apache.cloudstack.api.response.MigrationResponse; +import org.apache.cloudstack.backup.BackupDetailVO; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.engine.orchestration.service.StorageOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -57,6 +73,7 @@ import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.ImageStoreService.MigrationPolicy; +import org.apache.cloudstack.storage.backup.BackupObject; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; @@ -115,6 +132,12 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra TemplateDataFactory templateDataFactory; @Inject DataCenterDao dcDao; + @Inject + AgentManager agentManager; + @Inject + HostDao hostDao; + @Inject + BackupDetailsDao backupDetailDao; ConfigKey ImageStoreImbalanceThreshold = new ConfigKey<>("Advanced", Double.class, @@ -128,6 +151,7 @@ public class StorageOrchestrator extends ManagerBase implements StorageOrchestra private final Map zoneExecutorMap = new HashMap<>(); private final Map zonePendingWorkCountMap = new HashMap<>(); + private final Map zoneKvmIncrementalExecutorMap = new ConcurrentHashMap<>(); @Override public String getConfigComponentName() { @@ -171,7 +195,9 @@ public MigrationResponse migrateData(Long srcDataStoreId, List destDatasto DataStore srcDatastore = dataStoreManager.getDataStore(srcDataStoreId, DataStoreRole.Image); Map, Long>> snapshotChains = new HashMap<>(); Map, Long>> childTemplates = new HashMap<>(); - files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates); + Map>, Long>> backupChains = new HashMap<>(); + files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates, backupChains); + if (files.isEmpty()) { return new MigrationResponse(String.format("No files in Image store: %s to migrate", srcDatastore), migrationPolicy.toString(), true); @@ -227,7 +253,7 @@ public MigrationResponse migrateData(Long srcDataStoreId, List destDatasto } if (shouldMigrate(chosenFileForMigration, srcDatastore.getId(), destDatastoreId, storageCapacities, snapshotChains, childTemplates, migrationPolicy)) { - storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destDatastoreId, futures); + storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, backupChains, srcDatastore, destDatastoreId, futures); } else { if (migrationPolicy == MigrationPolicy.BALANCE) { continue; @@ -256,7 +282,7 @@ public MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreI List templates = templateDataStoreDao.listByStoreIdAndTemplateIds(srcImgStoreId, templateIdList); List snapshots = snapshotDataStoreDao.listByStoreAndSnapshotIds(srcImgStoreId, DataStoreRole.Image, snapshotIdList); - if (!migrationHelper.filesReadyToMigrate(srcImgStoreId, templates, snapshots, Collections.emptyList())) { + if (!migrationHelper.filesReadyToMigrate(srcImgStoreId, templates, snapshots, Collections.emptyList(), Collections.emptyList())) { throw new CloudRuntimeException("Migration failed as there are data objects which are not Ready - i.e, they may be in Migrating, creating, copying, etc. states"); } files = migrationHelper.getSortedValidSourcesList(srcDatastore, snapshotChains, childTemplates, templates, snapshots); @@ -291,7 +317,7 @@ public MigrationResponse migrateResources(Long srcImgStoreId, Long destImgStoreI } if (storageCapacityBelowThreshold(storageCapacities, destImgStoreId)) { - storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, srcDatastore, destImgStoreId, futures); + storageCapacities = migrateAway(chosenFileForMigration, storageCapacities, snapshotChains, childTemplates, null, srcDatastore, destImgStoreId, futures); } else { message = "Migration failed. Destination store doesn't have enough capacity for migration"; success = false; @@ -355,15 +381,89 @@ protected Map> migrateAway( Map, Long>> snapshotChains, Map, Long>> templateChains, + Map>, Long>> backupChains, DataStore srcDatastore, Long destDatastoreId, List> futures) { Long fileSize = migrationHelper.getFileSize(chosenFileForMigration, snapshotChains, templateChains); storageCapacities = assumeMigrate(storageCapacities, srcDatastore.getId(), destDatastoreId, fileSize); + DataStore destDataStore = dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image); + + boolean isKvmIncrementalBackup = backupChains != null && chosenFileForMigration instanceof BackupObject && backupChains.containsKey(chosenFileForMigration); + + if (isKvmIncrementalBackup) { + MigrateKvmIncrementalBackupTask task = new MigrateKvmIncrementalBackupTask(chosenFileForMigration, backupChains, srcDatastore, destDataStore); + futures.add(submitKvmIncrementalMigration(srcDatastore.getScope().getScopeId(), task)); + logger.debug("Incremental backup migration {} submitted to incremental pool.", chosenFileForMigration.getUuid()); + } else { + createMigrateDataTask(chosenFileForMigration, snapshotChains, templateChains, srcDatastore, destDataStore, futures); + } + + return storageCapacities; + } + + private void migrateKvmIncrementalBackupChain(DataObject chosenFileForMigration, Map>, Long>> backupChains, DataStore srcDatastore, DataStore destDataStore) { + Transaction.execute((TransactionCallback) status -> { + MigrateBetweenSecondaryStoragesCommandAnswer answer = null; + + try { + List> backupChain = backupChains.get(chosenFileForMigration).first(); + MigrateBackupsBetweenSecondaryStoragesCommand migrateBetweenSecondaryStoragesCmd = new MigrateBackupsBetweenSecondaryStoragesCommand(backupChain.stream().map(list -> list.stream().map(BackupObject::getTO).collect(Collectors.toList())) + .collect(Collectors.toList()), srcDatastore.getTO(), destDataStore.getTO()); + + HostVO host = getAvailableHost(((BackupObject) chosenFileForMigration).getZoneId()); + if (host == null) { + throw new CloudRuntimeException("No hosts found to send migrate command."); + } + + migrateBetweenSecondaryStoragesCmd.setWait(StorageManager.AgentMaxDataMigrationWaitTime.valueIn(host.getClusterId())); + answer = (MigrateBetweenSecondaryStoragesCommandAnswer) agentManager.send(host.getId(), migrateBetweenSecondaryStoragesCmd); + if (answer == null || !answer.getResult()) { + logger.warn("Unable to migrate backups [{}].", backupChain); + throw new CloudRuntimeException("Unable to migrate KVM incremental backups to another secondary storage"); + } + + } catch (final OperationTimedoutException | AgentUnavailableException e) { + throw new CloudRuntimeException("Error while migrating KVM incremental backup chain. Check the logs for more information.", e); + } finally { + if (answer != null) { + updateBackupsReference(destDataStore, answer); + } + } + return answer.getResult(); + }); + } + + private void updateBackupsReference(DataStore destDataStore, MigrateBetweenSecondaryStoragesCommandAnswer answer) { + for (Pair backupIdAndUpdatedCheckpointPath : answer.getMigratedResources()) { + Long backupId = backupIdAndUpdatedCheckpointPath.first(); + BackupDetailVO backupDetail = backupDetailDao.findDetail(backupId, BackupDetailsDao.IMAGE_STORE_ID); + String destDataStoreId = String.valueOf(destDataStore.getId()); + + if (backupDetail == null) { + logger.warn("No details found for backup [{}]. Creating new entry with image store ID [{}].", backupId, destDataStoreId); + backupDetailDao.addDetail(backupId, BackupDetailsDao.IMAGE_STORE_ID, destDataStoreId, false); + continue; + } + + backupDetail.setValue(destDataStoreId); + backupDetailDao.update(backupDetail.getId(), backupDetail); + } + } - MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, dataStoreManager.getDataStore(destDatastoreId, DataStoreRole.Image)); - if (chosenFileForMigration instanceof SnapshotInfo ) { + private HostVO getAvailableHost(long zoneId) throws AgentUnavailableException, OperationTimedoutException { + List hosts = hostDao.listByDataCenterIdAndHypervisorType(zoneId, Hypervisor.HypervisorType.KVM); + if (CollectionUtils.isNotEmpty(hosts)) { + return hosts.get(new Random().nextInt(hosts.size())); + } + + return null; + } + + private void createMigrateDataTask(DataObject chosenFileForMigration, Map, Long>> snapshotChains, Map, Long>> templateChains, DataStore srcDatastore, DataStore destDataStore, List> futures) { + MigrateDataTask task = new MigrateDataTask(chosenFileForMigration, srcDatastore, destDataStore); + if (chosenFileForMigration instanceof SnapshotInfo) { task.setSnapshotChains(snapshotChains); } if (chosenFileForMigration instanceof TemplateInfo) { @@ -371,7 +471,6 @@ protected Map> migrateAway( } futures.add(submit(srcDatastore.getScope().getScopeId(), task)); logger.debug("Migration of {}: {} is initiated.", chosenFileForMigration.getType().name(), chosenFileForMigration.getUuid()); - return storageCapacities; } protected Future submit(Long zoneId, Callable task) { @@ -390,6 +489,13 @@ protected Future submit(Long zoneId, Callable task) { } + protected synchronized Future submitKvmIncrementalMigration(Long zoneId, Callable task) { + if (!zoneKvmIncrementalExecutorMap.containsKey(zoneId)) { + zoneKvmIncrementalExecutorMap.put(zoneId, Executors.newSingleThreadExecutor()); + } + return zoneKvmIncrementalExecutorMap.get(zoneId).submit(task); + } + protected void scaleExecutorIfNecessary(Long zoneId) { long activeSsvms = migrationHelper.activeSSVMCount(zoneId); long totalJobs = activeSsvms * numConcurrentCopyTasksPerSSVM; @@ -666,4 +772,32 @@ public TemplateApiResult call() { return result; } } + + private class MigrateKvmIncrementalBackupTask implements Callable { + private final DataObject chosenFile; + private final Map>, Long>> backupChains; + private final DataStore srcDataStore; + private final DataStore destDataStore; + + public MigrateKvmIncrementalBackupTask(DataObject chosenFile, Map>, Long>> backupChains, DataStore srcDataStore, DataStore destDataStore) { + this.chosenFile = chosenFile; + this.backupChains = backupChains; + this.srcDataStore = srcDataStore; + this.destDataStore = destDataStore; + } + + @Override + public DataObjectResult call() { + try { + migrateKvmIncrementalBackupChain(chosenFile, backupChains, srcDataStore, destDataStore); + return new DataObjectResult(chosenFile); + } catch (Exception e) { + logger.warn("Failed migrating incremental backup {} due to {}.", chosenFile.getUuid(), e); + DataObjectResult result = new DataObjectResult(chosenFile); + result.setResult(e.toString()); + return result; + } + } + } + } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index a07fd13e1da0..2fad96ec1da2 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -38,8 +38,11 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.agent.AgentManager; import com.cloud.deploy.DeploymentClusterPlanner; import com.cloud.exception.ResourceAllocationException; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.resourcelimit.ReservationHelper; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.dao.VMTemplateDao; @@ -50,6 +53,7 @@ import org.apache.cloudstack.api.command.admin.vm.MigrateVMCmd; import org.apache.cloudstack.api.command.admin.volume.MigrateVolumeCmdByAdmin; import org.apache.cloudstack.api.command.user.volume.MigrateVolumeCmd; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; @@ -82,9 +86,17 @@ import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; +import org.apache.cloudstack.resourcelimit.Reserver; import org.apache.cloudstack.secret.PassphraseVO; import org.apache.cloudstack.secret.dao.PassphraseDao; import org.apache.cloudstack.snapshot.SnapshotHelper; +import org.apache.cloudstack.kms.KMSManager; +import org.apache.cloudstack.kms.KMSKeyVO; +import org.apache.cloudstack.kms.KMSWrappedKeyVO; +import org.apache.cloudstack.kms.dao.KMSKeyDao; +import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao; +import org.apache.cloudstack.framework.kms.KMSException; +import org.apache.cloudstack.framework.kms.WrappedKey; import org.apache.cloudstack.storage.command.CommandResult; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -273,12 +285,25 @@ public enum UserVmCloneType { ConfigurationDao configurationDao; @Inject VMInstanceDao vmInstanceDao; + @Inject + ClvmPoolManager clvmPoolManager; + @Inject + AgentManager _agentMgr; @Inject protected SnapshotHelper snapshotHelper; @Inject private DataStoreProviderManager dataStoreProviderMgr; + @Inject + private KMSManager kmsManager; + @Inject + private KMSKeyDao kmsKeyDao; + @Inject + private KMSWrappedKeyDao kmsWrappedKeyDao; + + @Inject + private InternalBackupService internalBackupService; private final StateMachine2 _volStateMachine; protected List _storagePoolAllocators; @@ -315,7 +340,7 @@ public VolumeInfo moveVolume(VolumeInfo volumeInfo, long destPoolDcId, Long dest // Find a destination storage pool with the specified criteria DiskOffering diskOffering = _entityMgr.findById(DiskOffering.class, volumeInfo.getDiskOfferingId()); DiskProfile dskCh = new DiskProfile(volumeInfo.getId(), volumeInfo.getVolumeType(), volumeInfo.getName(), diskOffering.getId(), diskOffering.getDiskSize(), diskOffering.getTagsArray(), - diskOffering.isUseLocalStorage(), diskOffering.isRecreatable(), null, (diskOffering.getEncrypt() || volumeInfo.getPassphraseId() != null)); + diskOffering.isUseLocalStorage(), diskOffering.isRecreatable(), null, (diskOffering.getEncrypt() || volumeInfo.getPassphraseId() != null || volumeInfo.getKmsKeyId() != null)); dskCh.setHyperType(dataDiskHyperType); storageMgr.setDiskProfileThrottling(dskCh, null, diskOffering); @@ -350,9 +375,13 @@ public VolumeVO allocateDuplicateVolumeVO(Volume oldVol, DiskOffering diskOfferi newVol.setInstanceId(oldVol.getInstanceId()); newVol.setRecreatable(oldVol.isRecreatable()); newVol.setFormat(oldVol.getFormat()); - if ((diskOffering == null || diskOffering.getEncrypt()) && oldVol.getPassphraseId() != null) { - PassphraseVO passphrase = passphraseDao.persist(new PassphraseVO(true)); - newVol.setPassphraseId(passphrase.getId()); + if ((diskOffering == null || diskOffering.getEncrypt())) { + if (oldVol.getKmsKeyId() != null) { + newVol.setKmsKeyId(oldVol.getKmsKeyId()); + } else if (oldVol.getPassphraseId() != null) { + PassphraseVO passphrase = passphraseDao.persist(new PassphraseVO(true)); + newVol.setPassphraseId(passphrase.getId()); + } } return _volsDao.persist(newVol); @@ -507,7 +536,9 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use DiskOffering diskOffering = _entityMgr.findById(DiskOffering.class, volume.getDiskOfferingId()); if (diskOffering.getEncrypt()) { VolumeVO vol = (VolumeVO) volume; - volume = setPassphraseForVolumeEncryption(vol); + // Retrieve KMS key from volume's kmsKeyId if provided + KMSKeyVO kmsKey = getKmsKeyFromVolume(vol); + volume = setPassphraseForVolumeEncryption(vol, kmsKey, volume.getAccountId()); } DataCenter dc = _entityMgr.findById(DataCenter.class, volume.getDataCenterId()); DiskProfile dskCh = new DiskProfile(volume, diskOffering, snapshot.getHypervisorType()); @@ -644,7 +675,7 @@ public VolumeInfo createVolumeFromSnapshot(Volume volume, Snapshot snapshot, Use } protected DiskProfile createDiskCharacteristics(VolumeInfo volumeInfo, VirtualMachineTemplate template, DataCenter dc, DiskOffering diskOffering) { - boolean requiresEncryption = diskOffering.getEncrypt() || volumeInfo.getPassphraseId() != null; + boolean requiresEncryption = diskOffering.getEncrypt() || volumeInfo.getPassphraseId() != null || volumeInfo.getKmsKeyId() != null; if (volumeInfo.getVolumeType() == Type.ROOT && Storage.ImageFormat.ISO != template.getFormat()) { String templateToString = getReflectOnlySelectedFields(template); String zoneToString = getReflectOnlySelectedFields(dc); @@ -724,7 +755,9 @@ public VolumeInfo createVolume(VolumeInfo volumeInfo, VirtualMachine vm, Virtual if (diskOffering.getEncrypt()) { VolumeVO vol = _volsDao.findById(volumeInfo.getId()); - setPassphraseForVolumeEncryption(vol); + // Retrieve KMS key from volume's kmsKeyId if provided + KMSKeyVO kmsKey = getKmsKeyFromVolume(vol); + setPassphraseForVolumeEncryption(vol, kmsKey, vol.getAccountId()); volumeInfo = volFactory.getVolume(volumeInfo.getId()); } } @@ -745,6 +778,17 @@ public VolumeInfo createVolume(VolumeInfo volumeInfo, VirtualMachine vm, Virtual logger.debug("Trying to create volume [{}] on storage pool [{}].", volumeToString, poolToString); DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); + + // For CLVM pools, set the lock host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + if (ClvmPoolManager.isClvmPoolType(pool.getPoolType()) && hostId != null) { + logger.info("CLVM pool detected. Setting lock host {} for volume {} to route creation to correct host", + hostId, volumeInfo.getUuid()); + volumeInfo.setDestinationHostId(hostId); + + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + } + for (int i = 0; i < 2; i++) { // retry one more time in case of template reload is required for Vmware case AsyncCallFuture future = null; @@ -786,6 +830,122 @@ private String getVolumeIdentificationInfos(Volume volume) { return String.format("uuid: %s, name: %s", volume.getUuid(), volume.getName()); } + /** + * Updates the CLVM_LOCK_HOST_ID for a migrated volume if applicable. + * For CLVM volumes that are attached to a VM, this updates the lock host tracking + * to point to the VM's current host after volume migration. + * + * @param migratedVolume The volume that was migrated + * @param destPool The destination storage pool + * @param operationType Description of the operation (e.g., "migrated", "live-migrated") for logging + */ + private void updateClvmLockHostAfterMigration(Volume migratedVolume, StoragePool destPool, String operationType) { + if (migratedVolume == null || destPool == null) { + return; + } + + StoragePoolVO pool = _storagePoolDao.findById(destPool.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return; + } + + if (migratedVolume.getInstanceId() == null) { + return; + } + + VMInstanceVO vm = vmInstanceDao.findById(migratedVolume.getInstanceId()); + if (vm == null || vm.getHostId() == null) { + return; + } + + clvmPoolManager.setClvmLockHostId(migratedVolume.getId(), vm.getHostId()); + logger.debug("Updated CLVM_LOCK_HOST_ID for {} volume {} to host {} where VM {} is running", + operationType, migratedVolume.getUuid(), vm.getHostId(), vm.getInstanceName()); + } + + /** + * Retrieves the CLVM lock host ID from any existing volume of the specified VM. + * This is useful when attaching a new volume to a stopped VM - we want to maintain + * consistency by using the same host that manages the VM's other CLVM volumes. + * + * @param vmId The ID of the VM + * @return The host ID if found, null otherwise + */ + private Long getClvmLockHostFromVmVolumes(Long vmId) { + if (vmId == null) { + return null; + } + + List vmVolumes = _volsDao.findByInstance(vmId); + if (vmVolumes == null || vmVolumes.isEmpty()) { + return null; + } + + for (VolumeVO volume : vmVolumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + if (lockHostId != null) { + logger.debug("Found actual CLVM lock host {} from volume {} of VM {} via LVM query", + lockHostId, volume.getUuid(), vmId); + return lockHostId; + } + } + + return null; + } + + private void transferClvmLocksForVmStart(List volumes, Long destHostId, VMInstanceVO vm) { + if (volumes == null || volumes.isEmpty() || destHostId == null) { + return; + } + + for (VolumeVO volume : volumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + + Long currentLockHost = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (currentLockHost == null) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } else if (!currentLockHost.equals(destHostId)) { + logger.info("CLVM volume {} is locked on host {} but VM {} starting on host {}. Transferring lock.", + volume.getUuid(), currentLockHost, vm.getInstanceName(), destHostId); + + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), pool, currentLockHost, destHostId)) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volume.getUuid(), currentLockHost, destHostId)); + } + } + } + } + public String getRandomVolumeName() { return UUID.randomUUID().toString(); } @@ -861,8 +1021,10 @@ protected DiskProfile toDiskProfile(Volume vol, DiskOffering offering) { @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", create = true) @Override - public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, - Long deviceId) { + public DiskProfile allocateRawVolume( + Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, + VirtualMachineTemplate template, Account owner, Long deviceId, Long kmsKeyId, boolean incrementResourceCount + ) { if (size == null) { size = offering.getDiskSize(); } else { @@ -895,13 +1057,18 @@ public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offeri vol.setDisplayVolume(userVm.isDisplayVm()); } + // Set KMS key ID if provided + if (kmsKeyId != null) { + vol.setKmsKeyId(kmsKeyId); + } + vol.setFormat(getSupportedImageFormatForCluster(vm.getHypervisorType())); vol = _volsDao.persist(vol); saveVolumeDetails(offering.getId(), vol.getId()); // Save usage event and update resource count for user vm volumes - if (vm.getType() == VirtualMachine.Type.User) { + if (vm.getType() == VirtualMachine.Type.User && incrementResourceCount) { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_CREATE, vol.getAccountId(), vol.getDataCenterId(), vol.getId(), vol.getName(), offering.getId(), null, size, Volume.class.getName(), vol.getUuid(), vol.getInstanceId(), vol.isDisplayVolume()); _resourceLimitMgr.incrementVolumeResourceCount(vm.getAccountId(), vol.isDisplayVolume(), vol.getSize(), offering); @@ -914,7 +1081,7 @@ public DiskProfile allocateRawVolume(Type type, String name, DiskOffering offeri } private DiskProfile allocateTemplatedVolume(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner, long deviceId, String configurationId, Volume volume, Snapshot snapshot) { + Account owner, long deviceId, String configurationId, Long kmsKeyId, Volume volume, Snapshot snapshot) { assert (template.getFormat() != ImageFormat.ISO) : "ISO is not a template."; if (volume != null) { @@ -964,6 +1131,11 @@ private DiskProfile allocateTemplatedVolume(Type type, String name, DiskOffering vol.setDisplayVolume(userVm.isDisplayVm()); } + // Set KMS key ID if provided + if (kmsKeyId != null) { + vol.setKmsKeyId(kmsKeyId); + } + vol = _volsDao.persist(vol); saveVolumeDetails(offering.getId(), vol.getId()); @@ -1053,7 +1225,7 @@ public void saveVolumeDetails(Long diskOfferingId, Long volumeId) { @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating ROOT volume", create = true) @Override public List allocateTemplatedVolumes(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner, Volume volume, Snapshot snapshot) { + Account owner, Long kmsKeyId, Volume volume, Snapshot snapshot) { String templateToString = getReflectOnlySelectedFields(template); int volumesNumber = 1; @@ -1100,7 +1272,7 @@ public List allocateTemplatedVolumes(Type type, String name, DiskOf } logger.info("Adding disk object [{}] to VM [{}]", volumeName, vm); DiskProfile diskProfile = allocateTemplatedVolume(type, volumeName, offering, volumeSize, minIops, maxIops, - template, vm, owner, deviceId, configurationId, volume, snapshot); + template, vm, owner, deviceId, configurationId, kmsKeyId, volume, snapshot); profiles.add(diskProfile); } @@ -1126,7 +1298,7 @@ private void updateRootDiskVolumeEventDetails(Type type, VirtualMachine vm, List callContext.setEventResourceId(volumeIds.get(0)); } String volumeUuids = volumeIds.stream().map(volumeId -> this._uuidMgr.getUuid(Volume.class, volumeId)).collect(Collectors.joining(", ")); - callContext.setEventDetails("Volume Type: " + type + "Volume Id: " + volumeUuids + " Vm Id: " + this._uuidMgr.getUuid(VirtualMachine.class, vm.getId())); + callContext.setEventDetails("Volume Type: " + type + "Volume ID: " + volumeUuids + " Instance ID: " + vm.getUuid()); } } @@ -1190,24 +1362,42 @@ private VolumeInfo copyVolume(StoragePool rootDiskPool, VolumeInfo volumeInfo, V } @Override - public VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volumeInfo, HypervisorType rootDiskHyperType, StoragePool storagePool) throws NoTransitionException { + public VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo volumeInfo, HypervisorType rootDiskHyperType, StoragePool storagePool, Long clusterId, Long podId) + throws NoTransitionException { String volumeToString = getReflectOnlySelectedFields(volumeInfo.getVolume()); VirtualMachineTemplate rootDiskTmplt = _entityMgr.findById(VirtualMachineTemplate.class, vm.getTemplateId()); DataCenter dcVO = _entityMgr.findById(DataCenter.class, vm.getDataCenterId()); - logger.trace("storage-pool {}/{} is associated with pod {}",storagePool.getName(), storagePool.getUuid(), storagePool.getPodId()); - Long podId = storagePool.getPodId() != null ? storagePool.getPodId() : vm.getPodIdToDeployIn(); + + if (storagePool != null) { + logger.trace("storage-pool {}/{} is associated with pod {}", storagePool.getName(), storagePool.getUuid(), storagePool.getPodId()); + podId = storagePool.getPodId() != null ? storagePool.getPodId() : vm.getPodIdToDeployIn(); + clusterId = storagePool.getClusterId(); + logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); + } + Pod pod = _entityMgr.findById(Pod.class, podId); ServiceOffering svo = _entityMgr.findById(ServiceOffering.class, vm.getServiceOfferingId()); DiskOffering diskVO = _entityMgr.findById(DiskOffering.class, volumeInfo.getDiskOfferingId()); - Long clusterId = storagePool.getClusterId(); - logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); + Long hostId = vm.getHostId(); - if (hostId == null && storagePool.isLocal()) { - List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); - if (poolHosts.size() > 0) { - hostId = poolHosts.get(0).getHostId(); + if (hostId == null && storagePool != null && (storagePool.isLocal() || ClvmPoolManager.isClvmPoolType(storagePool.getPoolType()))) { + if (ClvmPoolManager.isClvmPoolType(storagePool.getPoolType())) { + hostId = getClvmLockHostFromVmVolumes(vm.getId()); + if (hostId != null) { + logger.debug("Using CLVM lock host {} from VM {}'s existing volumes for new volume creation", + hostId, vm.getUuid()); + } + } + + if (hostId == null) { + List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); + if (!poolHosts.isEmpty()) { + hostId = poolHosts.get(0).getHostId(); + logger.debug("Selected host {} from storage pool {} for stopped VM {} volume creation", + hostId, storagePool.getUuid(), vm.getUuid()); + } } } @@ -1366,7 +1556,7 @@ private void destroyVolumeInContext(Volume volume) { // Create new context and inject correct event resource type, id and details, // otherwise VOLUME.DESTROY event will be associated with VirtualMachine and contain VM id and other information. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); - volumeContext.setEventDetails("Volume Type: " + volume.getVolumeType() + " Volume Id: " + volume.getUuid() + " Vm Id: " + _uuidMgr.getUuid(VirtualMachine.class, volume.getInstanceId())); + volumeContext.setEventDetails("Volume Type: " + volume.getVolumeType() + " Volume ID: " + volume.getUuid() + " Instance ID: " + _uuidMgr.getUuid(VirtualMachine.class, volume.getInstanceId())); volumeContext.setEventResourceType(ApiCommandResourceType.Volume); volumeContext.setEventResourceId(volume.getId()); try { @@ -1452,6 +1642,10 @@ public Volume migrateVolume(Volume volume, StoragePool destPool) throws StorageU _snapshotDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); _snapshotDataStoreDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); } + internalBackupService.updateVolumeId(vol.getId(), result.getVolume().getId()); + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "migrated"); } return result.getVolume(); } catch (InterruptedException | ExecutionException e) { @@ -1477,6 +1671,10 @@ public Volume liveMigrateVolume(Volume volume, StoragePool destPool) { logger.error("Volume [{}] migration failed due to [{}].", volToString, result.getResult()); return null; } + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after live migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "live-migrated"); + return result.getVolume(); } catch (InterruptedException | ExecutionException e) { logger.error("Volume [{}] migration failed due to [{}].", volToString, e.getMessage()); @@ -1508,6 +1706,8 @@ public void migrateVolumes(VirtualMachine vm, VirtualMachineTO vmTo, Host srcHos throw new CloudRuntimeException(String.format("Failed to find the destination storage pool [%s] to migrate the volume [%s] to.", storagePoolToString, volumeToString)); } + internalBackupService.prepareVolumeForMigration(volume); + volumeMap.put(volFactory.getVolume(volume.getId()), (DataStore)destPool); } @@ -1519,6 +1719,22 @@ public void migrateVolumes(VirtualMachine vm, VirtualMachineTO vmTo, Host srcHos logger.error(msg); throw new CloudRuntimeException(msg); } + for (Map.Entry entry : volumeToPool.entrySet()) { + Volume volume = entry.getKey(); + StoragePool destPool = entry.getValue(); + StoragePoolVO srcPool = _storagePoolDao.findById(volume.getPoolId()); + if (srcPool != null && srcPool.getId() == destPool.getId() && + ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), srcPool, srcHost.getId(), destHost.getId())) { + throw new CloudRuntimeException(String.format( + "Failed to transfer CLVM lock for volume [%s] to destination host [%s].", + volume.getUuid(), destHost.getId())); + } + } else { + updateClvmLockHostAfterMigration(volume, destPool, "vm-migrated"); + } + } } catch (InterruptedException | ExecutionException e) { logger.error("Failed to migrate VM [{}] along with its volumes due to [{}].", vm, e.getMessage()); logger.debug("Exception: ", e); @@ -1775,7 +1991,9 @@ private Pair recreateVolume(VolumeVO vol, VirtualMachinePro if (vol.getState() == Volume.State.Allocated || vol.getState() == Volume.State.Creating) { DiskOffering diskOffering = _entityMgr.findById(DiskOffering.class, vol.getDiskOfferingId()); if (diskOffering.getEncrypt()) { - vol = setPassphraseForVolumeEncryption(vol); + // Retrieve KMS key from volume's kmsKeyId if provided + KMSKeyVO kmsKey = getKmsKeyFromVolume(vol); + vol = setPassphraseForVolumeEncryption(vol, kmsKey, vol.getAccountId()); } newVol = vol; } else { @@ -1807,6 +2025,17 @@ private Pair recreateVolume(VolumeVO vol, VirtualMachinePro volume = volFactory.getVolume(newVol.getId(), destPool); + // CLVM: pin templateless volume creation to the deploy host + StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId()); + if (poolVO != null && ClvmPoolManager.isClvmPoolType(poolVO.getPoolType())) { + Long hostId = vm.getVirtualMachine().getHostId(); + if (hostId != null) { + volume.setDestinationHostId(hostId); + clvmPoolManager.setClvmLockHostId(volume.getId(), hostId); + logger.info("CLVM pool detected during volume creation without a template. Setting lock host {} for volume {} " + + "to route creation to correct host", hostId, volume.getUuid()); + } + } future = volService.createVolumeAsync(volume, destPool); } else { final VirtualMachineTemplate template = _entityMgr.findById(VirtualMachineTemplate.class, templateId); @@ -1851,6 +2080,19 @@ private Pair recreateVolume(VolumeVO vol, VirtualMachinePro future = volService.createManagedStorageVolumeFromTemplateAsync(volume, destPool.getId(), templ, hostId); } else { + // For CLVM pools, set the destination host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId()); + if (poolVO != null && ClvmPoolManager.isClvmPoolType(poolVO.getPoolType())) { + Long hostId = vm.getVirtualMachine().getHostId(); + if (hostId != null) { + volume.setDestinationHostId(hostId); + clvmPoolManager.setClvmLockHostId(volume.getId(), hostId); + logger.info("CLVM pool detected during volume creation from template. Setting lock host {} for volume {} (persisted to DB) to route creation to correct host", + hostId, volume.getUuid()); + } + } + future = volService.createVolumeFromTemplateAsync(volume, destPool.getId(), templ); } } @@ -1898,16 +2140,72 @@ private Pair recreateVolume(VolumeVO vol, VirtualMachinePro return new Pair<>(newVol, destPool); } - private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume) { - if (volume.getPassphraseId() != null) { + /** + * Helper method to retrieve KMS key from volume's kmsKeyId + */ + private KMSKeyVO getKmsKeyFromVolume(VolumeVO volume) { + if (volume.getKmsKeyId() == null) { + return null; + } + return kmsKeyDao.findById(volume.getKmsKeyId()); + } + + private VolumeVO setKmsKeyForVolumeEncryption(VolumeVO volume, KMSKeyVO kmsKey, Long callerAccountId) { + // Determine caller account ID if not provided + if (callerAccountId == null) { + callerAccountId = volume.getAccountId(); + } + + // Validate permission + if (!kmsManager.hasPermission(callerAccountId, kmsKey)) { + throw new CloudRuntimeException("No permission to use KMS key: " + kmsKey); + } + + try { + logger.debug("Generating and wrapping DEK for volume {} using KMS key {}", volume.getName(), kmsKey.getUuid()); + long startTime = System.currentTimeMillis(); + + // Generate and wrap DEK using active KEK version + WrappedKey wrappedKey = kmsManager.generateVolumeKeyWithKek(kmsKey, callerAccountId); + + // The wrapped key is already persisted by generateVolumeKeyWithKek, get its ID + KMSWrappedKeyVO wrappedKeyVO = kmsWrappedKeyDao.findByUuid(wrappedKey.getUuid()); + if (wrappedKeyVO == null) { + throw new CloudRuntimeException("Failed to find persisted wrapped key: " + wrappedKey.getUuid()); + } + + // Set the wrapped key ID on the volume + volume.setKmsWrappedKeyId(wrappedKeyVO.getId()); + + long finishTime = System.currentTimeMillis(); + logger.debug("Generating and persisting wrapped key took {} ms for volume: {}", + (finishTime - startTime), volume.getName()); + + return _volsDao.persist(volume); + + } catch (KMSException e) { + throw new CloudRuntimeException("KMS failure while setting up volume encryption: " + e.getMessage(), e); + } + } + private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume, KMSKeyVO kmsKey, Long callerAccountId) { + // If volume already has encryption set up, return it + if (volume.getKmsWrappedKeyId() != null || volume.getPassphraseId() != null) { return volume; } - logger.debug("Creating passphrase for the volume: " + volume.getName()); + if (kmsKey != null) { + return setKmsKeyForVolumeEncryption(volume, kmsKey, callerAccountId); + } + // Legacy: passphrase-based encryption (fallback when KMS not enabled or KMS key not specified) + return setPassphraseForVolumeEncryption(volume); + } + + private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume) { + logger.debug("Creating passphrase for the volume: {}", volume.getName()); long startTime = System.currentTimeMillis(); PassphraseVO passphrase = passphraseDao.persist(new PassphraseVO(true)); volume.setPassphraseId(passphrase.getId()); long finishTime = System.currentTimeMillis(); - logger.debug("Creating and persisting passphrase took: " + (finishTime - startTime) + " ms for the volume: " + volume.toString()); + logger.debug("Creating and persisting passphrase took: {} ms for the volume: {}", finishTime - startTime, volume.toString()); return _volsDao.persist(volume); } @@ -1936,16 +2234,22 @@ protected void updateVolumeSize(DataStore store, VolumeVO vol) throws ResourceAl PrimaryDataStoreDriver driver = (PrimaryDataStoreDriver) store.getDriver(); long newSize = driver.getVolumeSizeRequiredOnPool(vol.getSize(), template == null ? null : template.getSize(), - vol.getPassphraseId() != null); + vol.getPassphraseId() != null || vol.getKmsKeyId() != null); - if (newSize != vol.getSize()) { - DiskOfferingVO diskOffering = diskOfferingDao.findByIdIncludingRemoved(vol.getDiskOfferingId()); + if (newSize == vol.getSize()) { + return; + } + + DiskOfferingVO diskOffering = diskOfferingDao.findByIdIncludingRemoved(vol.getDiskOfferingId()); + + List reservations = new ArrayList<>(); + try { VMInstanceVO vm = vol.getInstanceId() != null ? vmInstanceDao.findById(vol.getInstanceId()) : null; if (vm == null || vm.getType() == VirtualMachine.Type.User) { // Update resource count for user vm volumes when volume is attached if (newSize > vol.getSize()) { _resourceLimitMgr.checkPrimaryStorageResourceLimit(_accountMgr.getActiveAccountById(vol.getAccountId()), - vol.isDisplay(), newSize - vol.getSize(), diskOffering); + vol.isDisplay(), newSize - vol.getSize(), diskOffering, reservations); _resourceLimitMgr.incrementVolumePrimaryStorageResourceCount(vol.getAccountId(), vol.isDisplay(), newSize - vol.getSize(), diskOffering); } else { @@ -1953,9 +2257,11 @@ protected void updateVolumeSize(DataStore store, VolumeVO vol) throws ResourceAl vol.getSize() - newSize, diskOffering); } } - vol.setSize(newSize); - _volsDao.persist(vol); + } finally { + ReservationHelper.closeAll(reservations); } + vol.setSize(newSize); + _volsDao.persist(vol); } @Override @@ -1966,13 +2272,18 @@ public void prepare(VirtualMachineProfile vm, DeployDestination dest) throws Sto throw new CloudRuntimeException(msg); } - // don't allow to start vm that doesn't have a root volume if (_volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT).isEmpty()) { throw new CloudRuntimeException(String.format("ROOT volume is missing, unable to prepare volumes for the VM [%s].", vm.getVirtualMachine())); } List vols = _volsDao.findUsableVolumesForInstance(vm.getId()); + VirtualMachine vmInstance = vm.getVirtualMachine(); + VMInstanceVO vmInstanceVO = vmInstanceDao.findById(vmInstance.getId()); + if (vmInstance.getState() == State.Starting && dest.getHost() != null) { + transferClvmLocksForVmStart(vols, dest.getHost().getId(), vmInstanceVO); + } + List tasks = getTasks(vols, dest.getStorageForDisks(), vm); Volume vol = null; PrimaryDataStore store; @@ -2244,7 +2555,8 @@ public void destroyVolume(Volume volume) { if (volume.getState() == Volume.State.Allocated) { _volsDao.remove(volume.getId()); stateTransitTo(volume, Volume.Event.DestroyRequested); - _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId())); + _resourceLimitMgr.decrementVolumeResourceCount(volume.getAccountId(), volume.isDisplay(), volume.getSize(), diskOfferingDao.findByIdIncludingRemoved(volume.getDiskOfferingId()), + null); } else { destroyVolumeInContext(volume); } diff --git a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml index 17c5002c718b..e17302e68a1f 100644 --- a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml +++ b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml @@ -44,6 +44,8 @@ value="#{storagePoolAllocatorsRegistry.registered}" /> + + + + diff --git a/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java index fb42f2477888..43d83a672c0f 100644 --- a/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java @@ -22,6 +22,7 @@ import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; import com.cloud.exception.ConnectionException; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; @@ -104,4 +105,36 @@ public void testGetTimeoutWithGranularTimeout() { Assert.assertEquals(50, result); } + + @Test + public void testGetHostSshPortWithHostNull() { + int hostSshPort = mgr.getHostSshPort(null); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithNonKVMHost() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithKVMHostDefaultPort() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(host.getClusterId()).thenReturn(1L); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithKVMHostCustomPort() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(host.getDetail(Host.HOST_SSH_PORT)).thenReturn(String.valueOf(3922)); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(3922, hostSshPort); + } } diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index a07870d09af2..c656e1a282af 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -60,6 +61,7 @@ import com.cloud.network.Network; import com.cloud.network.NetworkModel; import com.cloud.resource.ResourceManager; +import com.cloud.storage.clvm.ClvmPoolManager; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; @@ -336,6 +338,19 @@ public void cleanup() { } } + private ClvmPoolManager injectMockedClvmPoolManager() { + ClvmPoolManager clvmPoolManagerMock = mock(ClvmPoolManager.class); + ReflectionTestUtils.setField(virtualMachineManagerImpl, "clvmPoolManager", clvmPoolManagerMock); + return clvmPoolManagerMock; + } + + private Method getUpdateClvmLockHostForVmVolumesMethod() throws NoSuchMethodException { + Method method = VirtualMachineManagerImpl.class.getDeclaredMethod( + "updateClvmLockHostForVmVolumes", long.class, long.class); + method.setAccessible(true); + return method; + } + @Test public void testaddHostIpToCertDetailsIfConfigAllows() { Host vmHost = mock(Host.class); @@ -1954,4 +1969,181 @@ public void testUnmanageSuccessKvm() throws Exception { } } + @Test + public void testUpdateClvmLockHostForVmVolumes_WithClvmVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO clvmVolume1 = mock(VolumeVO.class); + VolumeVO clvmVolume2 = mock(VolumeVO.class); + + when(clvmVolume1.getId()).thenReturn(1L); + when(clvmVolume1.getPoolId()).thenReturn(poolId); + when(clvmVolume2.getId()).thenReturn(2L); + when(clvmVolume2.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = mock(StoragePoolVO.class); + when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume1, clvmVolume2)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(clvmPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNonClvmVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO nfsVolume = mock(VolumeVO.class); + when(nfsVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO nfsPool = mock(StoragePoolVO.class); + when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(nfsVolume)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(nfsPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithMixedVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long clvmPoolId = 10L; + long nfsPoolId = 20L; + + VolumeVO clvmVolume = mock(VolumeVO.class); + VolumeVO nfsVolume = mock(VolumeVO.class); + + when(clvmVolume.getId()).thenReturn(1L); + when(clvmVolume.getPoolId()).thenReturn(clvmPoolId); + when(nfsVolume.getPoolId()).thenReturn(nfsPoolId); + + StoragePoolVO clvmPool = mock(StoragePoolVO.class); + when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO nfsPool = mock(StoragePoolVO.class); + when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume, nfsVolume)); + when(storagePoolDaoMock.findById(clvmPoolId)).thenReturn(clvmPool); + when(storagePoolDaoMock.findById(nfsPoolId)).thenReturn(nfsPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, never()).setClvmLockHostId(2L, destHostId); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNoVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Collections.emptyList()); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNullPoolId() throws Exception { + long vmId = 100L; + long destHostId = 2L; + + VolumeVO volumeWithoutPool = mock(VolumeVO.class); + when(volumeWithoutPool.getPoolId()).thenReturn(null); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volumeWithoutPool)); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(storagePoolDaoMock, never()).findById(anyLong()); + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNullPool() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO volume = mock(VolumeVO.class); + when(volume.getPoolId()).thenReturn(poolId); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(null); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_MultipleClvmPools() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long pool1Id = 10L; + long pool2Id = 20L; + + VolumeVO volume1 = mock(VolumeVO.class); + VolumeVO volume2 = mock(VolumeVO.class); + VolumeVO volume3 = mock(VolumeVO.class); + + when(volume1.getId()).thenReturn(1L); + when(volume1.getPoolId()).thenReturn(pool1Id); + when(volume2.getId()).thenReturn(2L); + when(volume2.getPoolId()).thenReturn(pool2Id); + when(volume3.getId()).thenReturn(3L); + when(volume3.getPoolId()).thenReturn(pool1Id); + + StoragePoolVO clvmPool1 = mock(StoragePoolVO.class); + when(clvmPool1.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO clvmPool2 = mock(StoragePoolVO.class); + when(clvmPool2.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume1, volume2, volume3)); + when(storagePoolDaoMock.findById(pool1Id)).thenReturn(clvmPool1); + when(storagePoolDaoMock.findById(pool2Id)).thenReturn(clvmPool2); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(3L, destHostId); + } + } diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java index e3989737112d..66f5b699cc46 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java @@ -27,6 +27,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,11 +36,15 @@ import com.cloud.exception.InsufficientVirtualNetworkCapacityException; import com.cloud.network.IpAddressManager; import com.cloud.utils.Pair; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionHelper; +import org.apache.cloudstack.framework.extensions.network.NetworkExtensionElement; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.springframework.test.util.ReflectionTestUtils; import org.mockito.ArgumentMatchers; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -68,6 +73,7 @@ import com.cloud.network.dao.PhysicalNetworkVO; import com.cloud.network.dao.RouterNetworkDao; import com.cloud.network.element.DhcpServiceProvider; +import com.cloud.network.element.NetworkElement; import com.cloud.network.guru.GuestNetworkGuru; import com.cloud.network.guru.NetworkGuru; import com.cloud.network.vpc.VpcManager; @@ -105,6 +111,7 @@ public class NetworkOrchestratorTest extends TestCase { private String guruName = "GuestNetworkGuru"; private String dhcpProvider = "VirtualRouter"; private NetworkGuru guru = mock(NetworkGuru.class); + private NetworkExtensionElement networkExtensionElement; NetworkOfferingVO networkOffering = mock(NetworkOfferingVO.class); @@ -135,6 +142,9 @@ public void setUp() { testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class); testOrchestrator._ipAddrMgr = mock(IpAddressManager.class); testOrchestrator._entityMgr = mock(EntityManager.class); + testOrchestrator.extensionHelper = mock(ExtensionHelper.class); + networkExtensionElement = mock(NetworkExtensionElement.class); + ReflectionTestUtils.setField(testOrchestrator, "networkExtensionElement", networkExtensionElement); DhcpServiceProvider provider = mock(DhcpServiceProvider.class); Map capabilities = new HashMap(); @@ -388,7 +398,7 @@ private void configureTestConfigureNicProfileBasedOnRequestedIpTests(NicProfile when(testOrchestrator._ipAddressDao.findByIpAndSourceNetworkId(Mockito.anyLong(), Mockito.anyString())).thenReturn(ipVoSpy); } - VlanVO vlanSpy = Mockito.spy(new VlanVO(Vlan.VlanType.DirectAttached, "vlanTag", vlanGateway, vlanNetmask, 0l, "192.168.100.100 - 192.168.100.200", 0l, new Long(0l), + VlanVO vlanSpy = Mockito.spy(new VlanVO(Vlan.VlanType.DirectAttached, "vlanTag", vlanGateway, vlanNetmask, 0l, "192.168.100.100 - 192.168.100.200", 0l, 0l, "ip6Gateway", "ip6Cidr", "ip6Range")); Mockito.doReturn(0l).when(vlanSpy).getId(); @@ -1010,4 +1020,57 @@ public void testImportNicWithIP4Address() throws Exception { assertEquals("testtag", nicProfile.getName()); } } + + // ----------------------------------------------------------------------- + // Tests for getNetworkElementsIncludingExtensions + // ----------------------------------------------------------------------- + + @Test + public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenNoExtensions() { + when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)) + .thenReturn(Collections.emptyList()); + + DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class); + List elements = new ArrayList<>(List.of(dhcpProvider)); + testOrchestrator.networkElements = elements; + + List result = testOrchestrator.getNetworkElementsIncludingExtensions(); + + assertNotNull(result); + assertEquals(elements.size(), result.size()); + } + + @Test + public void getNetworkElementsIncludingExtensionsAddsExtensionElements() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("my-net-ext"); + when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)) + .thenReturn(List.of(ext)); + + NetworkExtensionElement extElement = mock(NetworkExtensionElement.class); + when(networkExtensionElement.withProviderName("my-net-ext")).thenReturn(extElement); + + DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class); + testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider)); + + List result = testOrchestrator.getNetworkElementsIncludingExtensions(); + + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.contains(extElement)); + } + + @Test + public void getNetworkElementsIncludingExtensionsReturnsBaseListWhenExtensionHelperReturnsNull() { + when(testOrchestrator.extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)) + .thenReturn(null); + + DhcpServiceProvider dhcpProvider = mock(DhcpServiceProvider.class); + testOrchestrator.networkElements = new ArrayList<>(List.of(dhcpProvider)); + + List result = testOrchestrator.getNetworkElementsIncludingExtensions(); + + assertNotNull(result); + assertEquals(1, result.size()); + } } diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java index b4a26c17e2e5..d61caf16ae5d 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.engine.orchestration; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -30,6 +32,7 @@ import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.offering.DiskOffering; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.ScopeType; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; @@ -42,6 +45,7 @@ import com.cloud.uservm.UserVm; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.utils.Pair; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; @@ -67,6 +71,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedConstruction; @@ -275,6 +280,7 @@ public void testAllocateDuplicateVolumeVOBasic() { Mockito.when(oldVol.isRecreatable()).thenReturn(false); Mockito.when(oldVol.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); Mockito.when(oldVol.getPassphraseId()).thenReturn(null); // no encryption + Mockito.when(oldVol.getKmsKeyId()).thenReturn(null); // no encryption VolumeVO persistedVol = Mockito.mock(VolumeVO.class); Mockito.when(volumeDao.persist(Mockito.any(VolumeVO.class))).thenReturn(persistedVol); @@ -303,6 +309,7 @@ public void testAllocateDuplicateVolumeVOWithEncryption() { Mockito.when(oldVol.getInstanceId()).thenReturn(7L); Mockito.when(oldVol.isRecreatable()).thenReturn(true); Mockito.when(oldVol.getFormat()).thenReturn(Storage.ImageFormat.RAW); + Mockito.when(oldVol.getKmsKeyId()).thenReturn(null); Mockito.when(oldVol.getPassphraseId()).thenReturn(42L); PassphraseVO passphrase = Mockito.mock(PassphraseVO.class); @@ -336,9 +343,6 @@ public void testAllocateDuplicateVolumeVOWithTemplateOverride() { VolumeVO persistedVol = Mockito.mock(VolumeVO.class); Mockito.when(volumeDao.persist(Mockito.any())).thenReturn(persistedVol); - PassphraseVO mockPassPhrase = Mockito.mock(PassphraseVO.class); - Mockito.when(passphraseDao.persist(Mockito.any())).thenReturn(mockPassPhrase); - VolumeVO result = volumeOrchestrator.allocateDuplicateVolumeVO(oldVol, null, 222L); assertNotNull(result); } @@ -640,4 +644,305 @@ public void getVolumeCheckpointPathsAndImageStoreUrlsTestReturnCheckpointIfKVMAn Assert.assertEquals(1, result.second().size()); } + @Test + public void testTransferClvmLocksForVmStart_WithClvmVolumes() throws Exception { + Long destHostId = 2L; + Long currentHostId = 1L; + Long poolId = 10L; + + VolumeVO clvmVolume1 = Mockito.mock(VolumeVO.class); + VolumeVO clvmVolume2 = Mockito.mock(VolumeVO.class); + + Mockito.when(clvmVolume1.getId()).thenReturn(101L); + Mockito.when(clvmVolume1.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume1.getUuid()).thenReturn("vol-uuid-1"); + Mockito.when(clvmVolume1.getPath()).thenReturn("vol-path-1"); + + Mockito.when(clvmVolume2.getId()).thenReturn(102L); + Mockito.when(clvmVolume2.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume2.getUuid()).thenReturn("vol-uuid-2"); + Mockito.when(clvmVolume2.getPath()).thenReturn("vol-path-2"); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(102L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume1, clvmVolume2), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("vol-uuid-1"), Mockito.eq(101L), Mockito.eq("vol-path-1"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("vol-uuid-2"), Mockito.eq(102L), Mockito.eq("vol-path-2"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + } + + @Test + public void testTransferClvmLocksForVmStart_WithNonClvmVolumes() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO nfsVolume = Mockito.mock(VolumeVO.class); + Mockito.when(nfsVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(nfsPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(nfsVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_NoLockTransferNeeded() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class), + ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(destHostId); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + java.lang.reflect.Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_EmptyVolumeList() throws Exception { + Long destHostId = 2L; + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, new ArrayList(), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).getClvmLockHostId(Mockito.anyLong(), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.anyBoolean()); + } + + @Test + public void testTransferClvmLocksForVmStart_NullPoolId() throws Exception { + Long destHostId = 2L; + + VolumeVO volumeWithoutPool = Mockito.mock(VolumeVO.class); + Mockito.when(volumeWithoutPool.getPoolId()).thenReturn(null); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(volumeWithoutPool), destHostId, vmInstance); + + Mockito.verify(storagePoolDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_SetInitialLockHost() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class), + ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(null); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).setClvmLockHostId(101L, destHostId); + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_MixedVolumes() throws Exception { + Long destHostId = 2L; + Long currentHostId = 1L; + Long clvmPoolId = 10L; + Long nfsPoolId = 20L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(clvmPoolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("clvm-vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("clvm-vol-path"); + + VolumeVO nfsVolume = Mockito.mock(VolumeVO.class); + Mockito.when(nfsVolume.getPoolId()).thenReturn(nfsPoolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); + + Mockito.when(storagePoolDao.findById(clvmPoolId)).thenReturn(clvmPool); + Mockito.when(storagePoolDao.findById(nfsPoolId)).thenReturn(nfsPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume, nfsVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("clvm-vol-uuid"), Mockito.eq(101L), Mockito.eq("clvm-vol-path"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + } + + @Test(expected = CloudRuntimeException.class) + public void testTransferClvmLocksForVmStart_TransferFails() throws Throwable { + Long destHostId = 2L; + Long currentHostId = 1L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("vol-path"); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(false); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + try { + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = findField(target.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("Field " + fieldName + " not found in " + target.getClass()); + } + field.setAccessible(true); + field.set(target, value); + } + + private Field findField(Class clazz, String fieldName) { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } + } diff --git a/engine/schema/pom.xml b/engine/schema/pom.xml index 654cd14a25d3..664d4909e677 100644 --- a/engine/schema/pom.xml +++ b/engine/schema/pom.xml @@ -48,6 +48,11 @@ cloud-framework-db ${project.version} + + org.apache.cloudstack + cloud-framework-kms + ${project.version} + com.mysql mysql-connector-j diff --git a/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java b/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java index 94d01f472ba5..97b7c54f0844 100644 --- a/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/alert/dao/AlertDaoImpl.java @@ -20,6 +20,7 @@ import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.alert.AlertVO; @@ -28,7 +29,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.UpdateBuilder; @Component public class AlertDaoImpl extends GenericDaoBase implements AlertDao { @@ -107,25 +108,20 @@ public boolean archiveAlert(List ids, String type, Date startDate, Date en } sc.setParameters("archived", false); - boolean result = true; - ; List alerts = listBy(sc); if (ids != null && alerts.size() < ids.size()) { - result = false; - return result; + return false; } - if (alerts != null && !alerts.isEmpty()) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - for (AlertVO alert : alerts) { - alert = lockRow(alert.getId(), true); - alert.setArchived(true); - update(alert.getId(), alert); - txn.commit(); - } - txn.close(); + + if (CollectionUtils.isEmpty(alerts)) { + return true; } - return result; + + AlertVO alertForUpdate = createForUpdate(); + alertForUpdate.setArchived(true); + UpdateBuilder ub = getUpdateBuilder(alertForUpdate); + update(ub, sc, null); + return true; } @Override diff --git a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java index 4c752ff9b4ff..a8888f98ad29 100644 --- a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java @@ -171,7 +171,7 @@ public Scope getScope() { @Override public String getConfigValue(long id, String key) { ClusterDetailsVO vo = findDetail(id, key); - return vo == null ? null : vo.getValue(); + return vo == null ? null : getActualValue(vo); } @Override diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java index 01afd0780f7e..e4047cf7973d 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDao.java @@ -27,6 +27,6 @@ public interface AccountVlanMapDao extends GenericDao { public List listAccountVlanMapsByVlan(long vlanDbId); - public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId); + public AccountVlanMapVO findAccountVlanMap(Long accountId, long vlanDbId); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java index 12114770f112..0844bb77caa2 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/AccountVlanMapDaoImpl.java @@ -48,9 +48,9 @@ public List listAccountVlanMapsByVlan(long vlanDbId) { } @Override - public AccountVlanMapVO findAccountVlanMap(long accountId, long vlanDbId) { + public AccountVlanMapVO findAccountVlanMap(Long accountId, long vlanDbId) { SearchCriteria sc = AccountVlanSearch.create(); - sc.setParameters("accountId", accountId); + sc.setParametersIfNotNull("accountId", accountId); sc.setParameters("vlanDbId", vlanDbId); return findOneIncludingRemovedBy(sc); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java index 6cfd2608f5de..3d04f6679def 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java @@ -23,6 +23,7 @@ import com.cloud.dc.ClusterVO; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface ClusterDao extends GenericDao { @@ -61,4 +62,6 @@ public interface ClusterDao extends GenericDao { List listDistinctStorageAccessGroups(String name, String keyword); List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch); + + List listByZonesAndHypervisorType(List zoneIds, HypervisorType hypervisorType, Filter filter); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java index c63af0a237ba..9b6f0f7ac3a6 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java @@ -20,6 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.cpu.CPU; @@ -38,6 +40,7 @@ import com.cloud.org.Grouping; import com.cloud.org.Managed; import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.JoinBuilder; @@ -413,4 +416,19 @@ public List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, Hypervi } return customSearch(sc, null); } + + @Override + public List listByZonesAndHypervisorType(List zoneIds, HypervisorType hypervisorType, Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + sc.setParameters("hypervisorType", hypervisorType.toString()); + return listBy(sc, filter); + } } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java index aec54e20d989..687071699920 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDetailsDaoImpl.java @@ -47,7 +47,7 @@ public Scope getScope() { @Override public String getConfigValue(long id, String key) { ResourceDetail vo = findDetail(id, key); - return vo == null ? null : vo.getValue(); + return vo == null ? null : getActualValue(vo); } @Override diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java index 6af16bbace99..d14ccbe86ca6 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDao.java @@ -24,5 +24,5 @@ public interface DomainVlanMapDao extends GenericDao { public List listDomainVlanMapsByDomain(long domainId); public List listDomainVlanMapsByVlan(long vlanDbId); - public DomainVlanMapVO findDomainVlanMap(long domainId, long vlanDbId); + public DomainVlanMapVO findDomainVlanMap(Long domainId, long vlanDbId); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java index f789721d5fd6..0b4c781349fd 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/DomainVlanMapDaoImpl.java @@ -46,9 +46,9 @@ public List listDomainVlanMapsByVlan(long vlanDbId) { } @Override - public DomainVlanMapVO findDomainVlanMap(long domainId, long vlanDbId) { + public DomainVlanMapVO findDomainVlanMap(Long domainId, long vlanDbId) { SearchCriteria sc = DomainVlanSearch.create(); - sc.setParameters("domainId", domainId); + sc.setParametersIfNotNull("domainId", domainId); sc.setParameters("vlanDbId", vlanDbId); return findOneIncludingRemovedBy(sc); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java index a6c267bb189b..beab7ce2cd56 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java @@ -51,6 +51,8 @@ public interface VlanDao extends GenericDao { List listVlansByNetworkId(long networkId); + List listVlansByNetworkIds(List networkIds); + List listVlansByNetworkIdIncludingRemoved(long networkId); List listVlansByPhysicalNetworkId(long physicalNetworkId); diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java index d9fad3cad12a..5687e8588772 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java @@ -20,6 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import javax.naming.ConfigurationException; import com.cloud.dc.VlanDetailsVO; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -134,7 +136,7 @@ public VlanDaoImpl() { ZoneTypeSearch.done(); NetworkVlanSearch = createSearchBuilder(); - NetworkVlanSearch.and("networkId", NetworkVlanSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); + NetworkVlanSearch.and("networkId", NetworkVlanSearch.entity().getNetworkId(), SearchCriteria.Op.IN); NetworkVlanSearch.done(); PhysicalNetworkVlanSearch = createSearchBuilder(); @@ -392,6 +394,16 @@ public List listVlansByNetworkId(long networkId) { return listBy(sc); } + @Override + public List listVlansByNetworkIds(List networkIds) { + if (CollectionUtils.isEmpty(networkIds)) { + return Collections.emptyList(); + } + SearchCriteria sc = NetworkVlanSearch.create(); + sc.setParameters("networkId", networkIds.toArray()); + return listBy(sc); + } + @Override public List listVlansByNetworkIdIncludingRemoved(long networkId) { SearchCriteria sc = NetworkVlanSearch.create(); sc.setParameters("networkId", networkId); diff --git a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java index 56d971bbe015..1afa0d22dcc1 100644 --- a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java @@ -262,7 +262,7 @@ public boolean isChildDomain(Long parentId, Long childId) { SearchCriteria sc = DomainPairSearch.create(); sc.setParameters("id", parentId, childId); - List domainPair = listBy(sc); + List domainPair = listIncludingRemovedBy(sc); if ((domainPair != null) && (domainPair.size() == 2)) { DomainVO d1 = domainPair.get(0); diff --git a/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java b/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java index e748e98900eb..9417ddd12595 100644 --- a/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/event/dao/EventDaoImpl.java @@ -18,8 +18,10 @@ import java.util.Date; import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.event.Event.State; @@ -29,12 +31,13 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.UpdateBuilder; @Component public class EventDaoImpl extends GenericDaoBase implements EventDao { protected final SearchBuilder CompletedEventSearch; protected final SearchBuilder ToArchiveOrDeleteEventSearch; + protected final SearchBuilder ArchiveByIdsSearch; public EventDaoImpl() { CompletedEventSearch = createSearchBuilder(); @@ -51,6 +54,10 @@ public EventDaoImpl() { ToArchiveOrDeleteEventSearch.and("createdDateL", ToArchiveOrDeleteEventSearch.entity().getCreateDate(), Op.LTEQ); ToArchiveOrDeleteEventSearch.and("archived", ToArchiveOrDeleteEventSearch.entity().getArchived(), Op.EQ); ToArchiveOrDeleteEventSearch.done(); + + ArchiveByIdsSearch = createSearchBuilder(); + ArchiveByIdsSearch.and("id", ArchiveByIdsSearch.entity().getId(), Op.IN); + ArchiveByIdsSearch.done(); } @Override @@ -100,16 +107,16 @@ public List listToArchiveOrDeleteEvents(List ids, String type, Da @Override public void archiveEvents(List events) { - if (events != null && !events.isEmpty()) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - for (EventVO event : events) { - event = lockRow(event.getId(), true); - event.setArchived(true); - update(event.getId(), event); - txn.commit(); - } - txn.close(); + if (CollectionUtils.isEmpty(events)) { + return; } + + List ids = events.stream().map(EventVO::getId).collect(Collectors.toList()); + SearchCriteria sc = ArchiveByIdsSearch.create(); + sc.setParameters("id", ids.toArray(new Object[ids.size()])); + EventVO eventForUpdate = createForUpdate(); + eventForUpdate.setArchived(true); + UpdateBuilder ub = getUpdateBuilder(eventForUpdate); + update(ub, sc, null); } } diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index d51b4eca0577..468caf1e2271 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java @@ -45,7 +45,7 @@ import com.cloud.cpu.CPU; import org.apache.cloudstack.util.CPUArchConverter; import org.apache.cloudstack.util.HypervisorTypeConverter; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; @@ -856,7 +856,8 @@ public boolean checkHostServiceOfferingTags(ServiceOffering serviceOffering) { } if (BooleanUtils.isTrue(this.getIsTagARule())) { - return TagAsRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); + return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key()); } if (StringUtils.isEmpty(serviceOffering.getHostTag())) { diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 090b019334f4..d8bdabc3dcbb 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -32,12 +32,17 @@ import com.cloud.utils.db.GenericDao; import com.cloud.utils.fsm.StateDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; /** * Data Access Object for server * */ public interface HostDao extends GenericDao, StateDao { + + ConfigKey guestOsRuleExecutionTimeout = new ConfigKey<>("Advanced", Long.class, "guest.os.rule.execution.timeout", "3000", "The maximum runtime, in milliseconds, " + + "to execute a guest OS rule; if it is reached, a timeout will happen.", true); + long countBy(long clusterId, ResourceState... states); Integer countAllByType(final Host.Type type); @@ -116,6 +121,8 @@ public interface HostDao extends GenericDao, StateDao listIdsForUpEnabledByZoneAndHypervisor(Long zoneId, HypervisorType hypervisorType); + List findRoutingByClusterId(Long clusterId); + List findByClusterIdAndEncryptionSupport(Long clusterId); /** @@ -134,6 +141,8 @@ public interface HostDao extends GenericDao, StateDao listAllHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType); + List listAllRoutingHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType); + List listAllHostsThatHaveNoRuleTag(Host.Type type, Long clusterId, Long podId, Long dcId); HostVO findByPublicIp(String publicIp); @@ -218,7 +227,9 @@ int countHostsByMsResourceStateTypeAndHypervisorType(long msId, List listOrderedHostsHypervisorVersionsInDatacenter(long datacenterId, HypervisorType hypervisorType); - List findHostsWithTagRuleThatMatchComputeOferringTags(String computeOfferingTags); + List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags); + + List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName); List findClustersThatMatchHostTagRule(String computeOfferingTags); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 8f218841b074..15727d9d8e66 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -37,7 +37,9 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.commons.collections.CollectionUtils; import com.cloud.agent.api.VgpuTypesInfo; @@ -84,7 +86,7 @@ @DB @TableGenerator(name = "host_req_sq", table = "op_host", pkColumnName = "id", valueColumnName = "sequence", allocationSize = 1) -public class HostDaoImpl extends GenericDaoBase implements HostDao { //FIXME: , ExternalIdDao { +public class HostDaoImpl extends GenericDaoBase implements HostDao, Configurable { private static final String LIST_HOST_IDS_BY_HOST_TAGS = "SELECT filtered.host_id, COUNT(filtered.tag) AS tag_count " + "FROM (SELECT host_id, tag, is_tag_a_rule FROM host_tags GROUP BY host_id,tag,is_tag_a_rule) AS filtered " @@ -644,16 +646,22 @@ private void resetHosts(long managementServerId, long lastPingSecondsAfter) { sc.setParameters("lastPinged", lastPingSecondsAfter); sc.setParameters("status", Status.Disconnected, Status.Down, Status.Alert); - StringBuilder sb = new StringBuilder(); - List hosts = lockRows(sc, null, true); // exclusive lock - for (HostVO host : hosts) { - host.setManagementServerId(null); - update(host.getId(), host); - sb.append(host.getId()); - sb.append(" "); + // SELECT before bulk UPDATE to preserve per-host-ID trace logging — the bulk UPDATE + // cannot return which rows it matched since the WHERE column is being set to NULL + if (logger.isTraceEnabled()) { + List hosts = listBy(sc); + StringBuilder sb = new StringBuilder(); + for (HostVO host : hosts) { + sb.append(host.getId()); + sb.append(" "); + } + logger.trace("Following hosts will be reset: {}", sb); } - logger.trace("Following hosts got reset: {}", sb); + HostVO host = createForUpdate(); + host.setManagementServerId(null); + UpdateBuilder ub = getUpdateBuilder(host); + update(ub, sc, null); } /* @@ -1333,6 +1341,14 @@ public List listIdsForUpEnabledByZoneAndHypervisor(Long zoneId, Hypervisor return listIdsBy(null, Status.Up, ResourceState.Enabled, hypervisorType, zoneId, null, null); } + @Override + public List findRoutingByClusterId(Long clusterId) { + SearchCriteria sc = ClusterSearch.create(); + sc.setParameters("clusterId", clusterId); + sc.setParameters("type", Type.Routing); + return listBy(sc); + } + @Override public List findByClusterIdAndEncryptionSupport(Long clusterId) { SearchBuilder hostCapabilitySearch = _detailsDao.createSearchBuilder(); @@ -1412,7 +1428,7 @@ public HostVO findAnyStateHypervisorHostInCluster(long clusterId) { SearchCriteria sc = TypeStatusStateSearch.create(); sc.setParameters("type", Host.Type.Routing); sc.setParameters("cluster", clusterId); - List list = listBy(sc, new Filter(1)); + List list = listBy(sc, new Filter(1, true)); return list.isEmpty() ? null : list.get(0); } @@ -1448,6 +1464,16 @@ public List listAllHostsByZoneAndHypervisorType(long zoneId, HypervisorT return listBy(sc); } + @Override + public List listAllRoutingHostsByZoneAndHypervisorType(long zoneId, HypervisorType hypervisorType) { + SearchCriteria sc = DcSearch.create(); + sc.setParameters("dc", zoneId); + sc.setParameters("hypervisorType", hypervisorType.toString()); + sc.setParameters("type", Type.Routing); + + return listBy(sc); + } + @Override public List listAllHostsThatHaveNoRuleTag(Type type, Long clusterId, Long podId, Long dcId) { SearchCriteria sc = searchBuilderFindByIdTypeClusterIdPodIdDcIdAndWithoutRuleTag.create(); @@ -1520,11 +1546,13 @@ private List findHostIdsByHostTags(String hostTags){ } } - public List findHostsWithTagRuleThatMatchComputeOferringTags(String computeOfferingTags) { + @Override + public List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) { List hostTagVOList = _hostTagsDao.findHostRuleTags(); List result = new ArrayList<>(); for (HostTagVO rule: hostTagVOList) { - if (TagAsRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value())) { + if (GenericRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key())) { result.add(findById(rule.getHostId())); } } @@ -1532,9 +1560,26 @@ public List findHostsWithTagRuleThatMatchComputeOferringTags(String comp return result; } + @Override + public List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName) { + List hostIdsWithGuestOsRule = _detailsDao.findByName(Host.GUEST_OS_RULE); + List hostsWithIncompatibleRules = new ArrayList<>(); + for (DetailVO guestOsRule : hostIdsWithGuestOsRule) { + if (!GenericRuleHelper.interpretGuestOsRule(guestOsRule.getValue(), templateGuestOSName, HostDao.guestOsRuleExecutionTimeout.value(), + HostDao.guestOsRuleExecutionTimeout.key())) { + logger.trace("The guest OS rule [{}] of the host with ID [{}] is incompatible with the OS of the VM.", + guestOsRule.getHostId(), guestOsRule.getValue()); + hostsWithIncompatibleRules.add(findById(guestOsRule.getHostId())); + } + } + logger.trace("The hosts with the following IDs [{}] are incompatible with the VM considering their guest OS rule.", + hostsWithIncompatibleRules); + return hostsWithIncompatibleRules; + } + public List findClustersThatMatchHostTagRule(String computeOfferingTags) { Set result = new HashSet<>(); - List hosts = findHostsWithTagRuleThatMatchComputeOferringTags(computeOfferingTags); + List hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags); for (HostVO host: hosts) { result.add(host.getClusterId()); } @@ -1984,4 +2029,14 @@ public List listDistinctStorageAccessGroups(String name, String keyword) return customSearch(sc, null); } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {guestOsRuleExecutionTimeout}; + } + + @Override + public String getConfigComponentName() { + return HostDaoImpl.class.getSimpleName(); + } } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java index 7a00829fd44e..0d86ca0e48c7 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDao.java @@ -45,4 +45,9 @@ public interface HostTagsDao extends GenericDao { HostTagResponse newHostTagResponse(HostTagVO hostTag); List searchByIds(Long... hostTagIds); + + /** + * List all host tags defined on hosts within a cluster + */ + List listByClusterId(Long clusterId); } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java index 4aa14a31cfcf..d3fee6a26761 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostTagsDaoImpl.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -43,9 +44,12 @@ public class HostTagsDaoImpl extends GenericDaoBase implements private final SearchBuilder stSearch; private final SearchBuilder tagIdsearch; private final SearchBuilder ImplicitTagsSearch; + private final GenericSearchBuilder tagSearch; @Inject private ConfigurationDao _configDao; + @Inject + private HostDao hostDao; public HostTagsDaoImpl() { HostSearch = createSearchBuilder(); @@ -72,6 +76,11 @@ public HostTagsDaoImpl() { ImplicitTagsSearch.and("hostId", ImplicitTagsSearch.entity().getHostId(), SearchCriteria.Op.EQ); ImplicitTagsSearch.and("isImplicit", ImplicitTagsSearch.entity().getIsImplicit(), SearchCriteria.Op.EQ); ImplicitTagsSearch.done(); + + tagSearch = createSearchBuilder(String.class); + tagSearch.selectFields(tagSearch.entity().getTag()); + tagSearch.and("hostIdIN", tagSearch.entity().getHostId(), SearchCriteria.Op.IN); + tagSearch.done(); } @Override @@ -235,4 +244,15 @@ public List searchByIds(Long... tagIds) { return tagList; } + + @Override + public List listByClusterId(Long clusterId) { + List hostIds = hostDao.listIdsByClusterId(clusterId); + if (CollectionUtils.isEmpty(hostIds)) { + return new ArrayList<>(); + } + SearchCriteria sc = tagSearch.create(); + sc.setParameters("hostIdIN", hostIds.toArray()); + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/LBHealthCheckPolicyVO.java b/engine/schema/src/main/java/com/cloud/network/LBHealthCheckPolicyVO.java index ee5f67b09cd1..23f0b0f15ae3 100644 --- a/engine/schema/src/main/java/com/cloud/network/LBHealthCheckPolicyVO.java +++ b/engine/schema/src/main/java/com/cloud/network/LBHealthCheckPolicyVO.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.network; +import java.util.Date; import java.util.UUID; import javax.persistence.Column; @@ -25,8 +26,11 @@ import javax.persistence.Id; import javax.persistence.PrimaryKeyJoinColumn; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; import com.cloud.network.rules.HealthCheckPolicy; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @Entity @@ -68,6 +72,10 @@ public class LBHealthCheckPolicyVO implements HealthCheckPolicy { @Column(name = "display", updatable = true, nullable = false) protected boolean display = true; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + protected LBHealthCheckPolicyVO() { this.uuid = UUID.randomUUID().toString(); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java index 7f322ae6c037..3527ce84dcf6 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDao.java @@ -69,6 +69,8 @@ public interface FirewallRulesDao extends GenericDao { List listByNetworkPurposeTrafficType(long networkId, FirewallRule.Purpose purpose, FirewallRule.TrafficType trafficType); + List listByVpcPurposeTrafficType(long vpcId, FirewallRule.Purpose purpose, FirewallRule.TrafficType trafficType); + List listByIpAndPurposeWithState(Long addressId, FirewallRule.Purpose purpose, FirewallRule.State state); void loadSourceCidrs(FirewallRuleVO rule); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java index 27bf7ba6aa83..5a1e1aae6b66 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/FirewallRulesDaoImpl.java @@ -74,6 +74,7 @@ protected FirewallRulesDaoImpl() { AllFieldsSearch.and("domain", AllFieldsSearch.entity().getDomainId(), Op.EQ); AllFieldsSearch.and("id", AllFieldsSearch.entity().getId(), Op.EQ); AllFieldsSearch.and("networkId", AllFieldsSearch.entity().getNetworkId(), Op.EQ); + AllFieldsSearch.and("vpcId", AllFieldsSearch.entity().getVpcId(), Op.EQ); AllFieldsSearch.and("related", AllFieldsSearch.entity().getRelated(), Op.EQ); AllFieldsSearch.and("trafficType", AllFieldsSearch.entity().getTrafficType(), Op.EQ); AllFieldsSearch.done(); @@ -94,6 +95,7 @@ protected FirewallRulesDaoImpl() { ReleaseSearch.and("ipId", ReleaseSearch.entity().getSourceIpAddressId(), Op.EQ); ReleaseSearch.and("purpose", ReleaseSearch.entity().getPurpose(), Op.EQ); ReleaseSearch.and("ports", ReleaseSearch.entity().getSourcePortStart(), Op.IN); + ReleaseSearch.and("removed", ReleaseSearch.entity().getRemoved(), Op.NULL); ReleaseSearch.done(); SystemRuleSearch = createSearchBuilder(); @@ -355,6 +357,22 @@ public List listByNetworkPurposeTrafficType(long networkId, Purp return listBy(sc); } + @Override + public List listByVpcPurposeTrafficType(long vpcId, Purpose purpose, TrafficType trafficType) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vpcId", vpcId); + + if (purpose != null) { + sc.setParameters("purpose", purpose); + } + + if (trafficType != null) { + sc.setParameters("trafficType", trafficType); + } + + return listBy(sc); + } + @Override @DB public boolean remove(Long id) { diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LBHealthCheckPolicyDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/LBHealthCheckPolicyDaoImpl.java index 45927f3c3775..a9b4e02ba87f 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LBHealthCheckPolicyDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LBHealthCheckPolicyDaoImpl.java @@ -32,8 +32,9 @@ public class LBHealthCheckPolicyDaoImpl extends GenericDaoBase sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + sc.addAnd("removed", SearchCriteria.Op.NULL); - expunge(sc); + remove(sc); } @Override @@ -41,8 +42,9 @@ public void remove(long loadBalancerId, Boolean revoke) { SearchCriteria sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); sc.addAnd("revoke", SearchCriteria.Op.EQ, revoke); + sc.addAnd("removed", SearchCriteria.Op.NULL); - expunge(sc); + remove(sc); } @Override diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDao.java b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDao.java index 6669d70824a7..03fadbbd6598 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDao.java @@ -28,4 +28,6 @@ public interface LBStickinessPolicyDao extends GenericDao listByLoadBalancerIdAndDisplayFlag(long loadBalancerId, boolean forDisplay); List listByLoadBalancerId(long loadBalancerId, boolean revoke); + + List listByLoadBalancerId(long loadBalancerId); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDaoImpl.java index 717458fdb99f..86fd908b6b76 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyDaoImpl.java @@ -31,8 +31,9 @@ public class LBStickinessPolicyDaoImpl extends GenericDaoBase sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + sc.addAnd("removed", SearchCriteria.Op.NULL); - expunge(sc); + remove(sc); } @Override @@ -40,8 +41,9 @@ public void remove(long loadBalancerId, Boolean revoke) { SearchCriteria sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); sc.addAnd("revoke", SearchCriteria.Op.EQ, revoke); + sc.addAnd("removed", SearchCriteria.Op.NULL); - expunge(sc); + remove(sc); } @Override @@ -62,4 +64,10 @@ public List listByLoadBalancerId(long loadBalancerId, bool return listBy(sc); } + @Override + public List listByLoadBalancerId(long loadBalancerId) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyVO.java b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyVO.java index 72b8fc151b78..ae658f0757f0 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LBStickinessPolicyVO.java @@ -17,6 +17,7 @@ package com.cloud.network.dao; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -30,9 +31,12 @@ import javax.persistence.Id; import javax.persistence.PrimaryKeyJoinColumn; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; import com.cloud.network.rules.StickinessPolicy; import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @Entity @@ -68,6 +72,10 @@ public class LBStickinessPolicyVO implements StickinessPolicy { @Column(name = "display", updatable = true, nullable = false) protected boolean display = true; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + protected LBStickinessPolicyVO() { this.uuid = UUID.randomUUID().toString(); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerCertMapVO.java b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerCertMapVO.java index f95c61733f28..7fbe58df930c 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerCertMapVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerCertMapVO.java @@ -16,13 +16,17 @@ // under the License. package com.cloud.network.dao; +import java.util.Date; import java.util.UUID; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.api.InternalIdentity; @Entity @@ -45,6 +49,10 @@ public class LoadBalancerCertMapVO implements InternalIdentity { @Column(name = "revoke") private boolean revoke = false; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + public LoadBalancerCertMapVO() { this.uuid = UUID.randomUUID().toString(); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java index dc37cdeefe3d..03a1e45d917e 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapDaoImpl.java @@ -34,8 +34,9 @@ public class LoadBalancerVMMapDaoImpl extends GenericDaoBase sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); + sc.addAnd("removed", SearchCriteria.Op.NULL); - expunge(sc); + remove(sc); } @Override @@ -43,11 +44,12 @@ public void remove(long loadBalancerId, List instanceIds, Boolean revoke) SearchCriteria sc = createSearchCriteria(); sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); sc.addAnd("instanceId", SearchCriteria.Op.IN, instanceIds.toArray()); + sc.addAnd("removed", SearchCriteria.Op.NULL); if (revoke != null) { sc.addAnd("revoke", SearchCriteria.Op.EQ, revoke); } - expunge(sc); + remove(sc); } @Override @@ -56,12 +58,13 @@ public void remove(long loadBalancerId, long instanceId, String instanceIp, Bool sc.addAnd("loadBalancerId", SearchCriteria.Op.EQ, loadBalancerId); sc.addAnd("instanceId", SearchCriteria.Op.IN, instanceId); sc.addAnd("instanceIp", SearchCriteria.Op.EQ, instanceIp); + sc.addAnd("removed", SearchCriteria.Op.NULL); if (revoke != null) { sc.addAnd("revoke", SearchCriteria.Op.EQ, revoke); } - expunge(sc); + remove(sc); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapVO.java b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapVO.java index 721349613d88..2c88bf9c5a60 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/LoadBalancerVMMapVO.java @@ -22,9 +22,14 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.api.InternalIdentity; +import java.util.Date; + @Entity @Table(name = "load_balancer_vm_map") public class LoadBalancerVMMapVO implements InternalIdentity { @@ -48,6 +53,10 @@ public class LoadBalancerVMMapVO implements InternalIdentity { @Column(name = "state") private String state; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + public LoadBalancerVMMapVO() { } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java index fdca6e43f00f..b656d896e37f 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java @@ -24,6 +24,7 @@ import com.cloud.network.Network.GuestType; import com.cloud.network.Network.State; import com.cloud.network.Networks.TrafficType; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.fsm.StateDao; @@ -96,8 +97,13 @@ public interface NetworkDao extends GenericDao, StateDao serviceProviderMap); + List listByZoneAndTrafficType(long zoneId, TrafficType trafficType, Filter filter); + List listByZoneAndTrafficType(long zoneId, TrafficType trafficType); + List listByZonesTrafficTypeAndOwners(List zoneIds, final TrafficType trafficType, + List accountIds, List domainIds, Filter filter); + void setCheckForGc(long networkId); int getNetworkCountByNetworkOffId(long networkOfferingId); @@ -135,4 +141,6 @@ public interface NetworkDao extends GenericDao, StateDao listByPhysicalNetworkPvlan(long physicalNetworkId, String broadcastUri); List getAllPersistentNetworksFromZone(long dataCenterId); + + NetworkVO findByZoneIdAndAccountIdAndGuestTypeAndName(long zoneId, long accountId, GuestType guestType, String name); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java index 4e8b6204f720..fbf6a2ac2261 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java @@ -19,6 +19,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -29,15 +30,14 @@ import javax.inject.Inject; import javax.persistence.TableGenerator; -import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.network.Network; import com.cloud.network.Network.Event; import com.cloud.network.Network.GuestType; -import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.Network.State; import com.cloud.network.Networks.BroadcastDomainType; @@ -63,6 +63,7 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.SequenceFetcher; import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; @Component @@ -193,6 +194,7 @@ protected void init() { PersistentNetworkSearch.and("id", PersistentNetworkSearch.entity().getId(), Op.NEQ); PersistentNetworkSearch.and("guestType", PersistentNetworkSearch.entity().getGuestType(), Op.IN); PersistentNetworkSearch.and("broadcastUri", PersistentNetworkSearch.entity().getBroadcastUri(), Op.EQ); + PersistentNetworkSearch.and("dc", PersistentNetworkSearch.entity().getDataCenterId(), Op.EQ); PersistentNetworkSearch.and("removed", PersistentNetworkSearch.entity().getRemoved(), Op.NULL); final SearchBuilder persistentNtwkOffJoin = _ntwkOffDao.createSearchBuilder(); persistentNtwkOffJoin.and("persistent", persistentNtwkOffJoin.entity().isPersistent(), Op.EQ); @@ -389,7 +391,7 @@ public void persistNetworkServiceProviders(final long networkId, final Map listBy(final long accountId, final long dataCenterId, fin } @Override - public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType) { + public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType, Filter filter) { final SearchCriteria sc = AllFieldsSearch.create(); sc.setParameters("datacenter", zoneId); sc.setParameters("trafficType", trafficType); - return listBy(sc, null); + return listBy(sc, filter); + } + + @Override + public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType) { + return listByZoneAndTrafficType(zoneId, trafficType, null); + } + + @Override + public List listByZonesTrafficTypeAndOwners(List zoneIds, final TrafficType trafficType, + List accountIds, List domainIds, Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + sb.and("trafficType", sb.entity().getTrafficType(), Op.EQ); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + sc.setParameters("trafficType", trafficType); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + return listBy(sc, filter); } @Override @@ -907,4 +943,16 @@ public List listByPhysicalNetworkPvlan(long physicalNetworkId, String return overlappingNetworks; } + + @Override + public NetworkVO findByZoneIdAndAccountIdAndGuestTypeAndName(long zoneId, long accountId, GuestType guestType, String name) { + SearchCriteria sc = AllFieldsSearch.create(); + + sc.setParameters("datacenter", zoneId); + sc.setParameters("account", accountId); + sc.setParameters("guestType", guestType); + sc.setParameters("name", name); + + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkServiceMapVO.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkServiceMapVO.java index 4e1e9aff4561..65598eda7399 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkServiceMapVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkServiceMapVO.java @@ -81,6 +81,12 @@ public NetworkServiceMapVO(long networkId, Service service, Provider provider) { this.provider = provider.getName(); } + public NetworkServiceMapVO(long networkId, String serviceName, String providerName) { + this.networkId = networkId; + this.service = serviceName; + this.provider = providerName; + } + @Override public String toString() { StringBuilder buf = new StringBuilder("[Network Service["); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java index 02abaacd854e..f2572ba91c21 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java @@ -203,9 +203,13 @@ public class NetworkVO implements Network { @Column(name = "private_mtu") Integer privateMtu; + @Column(name = "keep_mac_address_on_public_nic") + private boolean keepMacAddressOnPublicNic = true; + @Transient Integer networkCidrSize; + public NetworkVO() { uuid = UUID.randomUUID().toString(); } @@ -773,4 +777,13 @@ public Integer getNetworkCidrSize() { public void setNetworkCidrSize(Integer networkCidrSize) { this.networkCidrSize = networkCidrSize; } + + @Override + public boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + + public void setKeepMacAddressOnPublicNic(boolean keepMacAddressOnPublicNic) { + this.keepMacAddressOnPublicNic = keepMacAddressOnPublicNic; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java index 9557c7465bff..4be6c5bc344d 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkServiceProviderVO.java @@ -97,6 +97,9 @@ public class PhysicalNetworkServiceProviderVO implements PhysicalNetworkServiceP @Column(name = "networkacl_service_provided") boolean networkAclServiceProvided; + @Column(name = "custom_action_service_provided") + boolean customActionServiceProvided; + @Column(name = GenericDao.REMOVED_COLUMN) Date removed; @@ -278,6 +281,7 @@ public void setEnabledServices(List services) { this.setUserdataServiceProvided(services.contains(Service.UserData)); this.setSecuritygroupServiceProvided(services.contains(Service.SecurityGroup)); this.setNetworkAclServiceProvided(services.contains(Service.NetworkACL)); + this.setCustomActionServiceProvided(services.contains(Service.CustomAction)); } @Override @@ -301,21 +305,27 @@ public List getEnabledServices() { if (this.isLbServiceProvided()) { services.add(Service.Lb); } - if (this.sourcenatServiceProvided) { + if (this.isSourcenatServiceProvided()) { services.add(Service.SourceNat); } - if (this.staticnatServiceProvided) { + if (this.isStaticnatServiceProvided()) { services.add(Service.StaticNat); } - if (this.portForwardingServiceProvided) { + if (this.isPortForwardingServiceProvided()) { services.add(Service.PortForwarding); } + if (this.isNetworkAclServiceProvided()) { + services.add(Service.NetworkACL); + } if (this.isUserdataServiceProvided()) { services.add(Service.UserData); } if (this.isSecuritygroupServiceProvided()) { services.add(Service.SecurityGroup); } + if (this.isCustomActionServiceProvided()) { + services.add(Service.CustomAction); + } return services; } @@ -327,4 +337,12 @@ public boolean isNetworkAclServiceProvided() { public void setNetworkAclServiceProvided(boolean networkAclServiceProvided) { this.networkAclServiceProvided = networkAclServiceProvided; } + + public boolean isCustomActionServiceProvided() { + return customActionServiceProvided; + } + + public void setCustomActionServiceProvided(boolean customActionServiceProvided) { + this.customActionServiceProvided = customActionServiceProvided; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java index ccba6bb18893..606bdaaaa7a7 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDao.java @@ -19,9 +19,21 @@ import com.cloud.network.vo.PublicIpQuarantineVO; import com.cloud.utils.db.GenericDao; +import java.util.Date; +import java.util.List; + public interface PublicIpQuarantineDao extends GenericDao { PublicIpQuarantineVO findByPublicIpAddressId(long publicIpAddressId); PublicIpQuarantineVO findByIpAddress(String publicIpAddress); + + /** + * Returns a list of public IP addresses that are actively quarantined at the specified date and the previous owner differs from the specified user. + * + * @param userId used to check against the IP's previous owner; + * @param date used to check if the quarantine is active; + * @return a list of PublicIpQuarantineVOs. + */ + List listQuarantinedIpAddressesToUser(Long userId, Date date); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java index a1b789b8a46b..0c47a0d36e30 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PublicIpQuarantineDaoImpl.java @@ -26,6 +26,8 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import java.util.Date; +import java.util.List; @Component public class PublicIpQuarantineDaoImpl extends GenericDaoBase implements PublicIpQuarantineDao { @@ -33,6 +35,8 @@ public class PublicIpQuarantineDaoImpl extends GenericDaoBase ipAddressSearchBuilder; + private SearchBuilder quarantinedIpAddressesSearch; + @Inject IPAddressDao ipAddressDao; @@ -47,8 +51,16 @@ public void init() { publicIpAddressByIdSearch.join("quarantineJoin", ipAddressSearchBuilder, ipAddressSearchBuilder.entity().getId(), publicIpAddressByIdSearch.entity().getPublicIpAddressId(), JoinBuilder.JoinType.INNER); + quarantinedIpAddressesSearch = createSearchBuilder(); + quarantinedIpAddressesSearch.and("previousOwnerId", quarantinedIpAddressesSearch.entity().getPreviousOwnerId(), SearchCriteria.Op.NEQ); + quarantinedIpAddressesSearch.and(); + quarantinedIpAddressesSearch.op("removedIsNull", quarantinedIpAddressesSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + quarantinedIpAddressesSearch.and("endDate", quarantinedIpAddressesSearch.entity().getEndDate(), SearchCriteria.Op.GT); + quarantinedIpAddressesSearch.cp(); + ipAddressSearchBuilder.done(); publicIpAddressByIdSearch.done(); + quarantinedIpAddressesSearch.done(); } @Override @@ -68,4 +80,14 @@ public PublicIpQuarantineVO findByIpAddress(String publicIpAddress) { return findOneBy(sc, filter); } + + @Override + public List listQuarantinedIpAddressesToUser(Long userId, Date date) { + SearchCriteria sc = quarantinedIpAddressesSearch.create(); + + sc.setParameters("previousOwnerId", userId); + sc.setParameters("endDate", date); + + return searchIncludingRemoved(sc, null, false, false); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java b/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java index 1dfdc5093a59..2b34e23fa4d4 100644 --- a/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java +++ b/engine/schema/src/main/java/com/cloud/network/rules/FirewallRuleVO.java @@ -32,6 +32,8 @@ import javax.persistence.Inheritance; import javax.persistence.InheritanceType; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; import javax.persistence.Transient; import com.cloud.utils.db.GenericDao; @@ -82,9 +84,16 @@ public class FirewallRuleVO implements FirewallRule { @Column(name = GenericDao.CREATED_COLUMN) Date created; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + @Column(name = "network_id") Long networkId; + @Column(name = "vpc_id") + Long vpcId; + @Column(name = "icmp_code") Integer icmpCode; @@ -190,10 +199,18 @@ public State getState() { } @Override - public long getNetworkId() { + public Long getNetworkId() { return networkId; } + public Long getVpcId() { + return vpcId; + } + + public void setVpcId(Long vpcId) { + this.vpcId = vpcId; + } + @Override public FirewallRuleType getType() { return type; @@ -203,11 +220,15 @@ public Date getCreated() { return created; } + public Date getRemoved() { + return removed; + } + protected FirewallRuleVO() { uuid = UUID.randomUUID().toString(); } - public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, long networkId, long accountId, long domainId, + public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, Long networkId, long accountId, long domainId, Purpose purpose, List sourceCidrs, Integer icmpCode, Integer icmpType, Long related, TrafficType trafficType) { this.xId = xId; if (xId == null) { @@ -251,7 +272,7 @@ public FirewallRuleVO(String xId, long ipAddressId, int port, String protocol, l } - public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, long networkId, long accountId, long domainId, + public FirewallRuleVO(String xId, Long ipAddressId, Integer portStart, Integer portEnd, String protocol, Long networkId, long accountId, long domainId, Purpose purpose, List sourceCidrs, List destCidrs, Integer icmpCode, Integer icmpType, Long related, TrafficType trafficType) { this(xId,ipAddressId, portStart, portEnd, protocol, networkId, accountId, domainId, purpose, sourceCidrs, icmpCode, icmpType, related, trafficType); this.destinationCidrs = destCidrs; diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java index 793334731095..2b18bcbd87bd 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java @@ -31,4 +31,6 @@ public interface SecurityGroupDao extends GenericDao { List findByAccountAndNames(Long accountId, String... names); int removeByAccountId(long accountId); + + List listByIds(List ids); } diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java index 019cf5fec462..ffb45629b625 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java @@ -129,4 +129,14 @@ public boolean expunge(Long id) { txn.commit(); return result; } + + @Override + public List listByIds(List ids) { + SearchBuilder idsSearch = createSearchBuilder(); + idsSearch.and("ids", idsSearch.entity().getId(), SearchCriteria.Op.IN); + idsSearch.done(); + SearchCriteria sc = idsSearch.create(); + sc.setParameters("ids", ids.toArray()); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java index 327d12c759a7..3180ef30a3ce 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupWorkDaoImpl.java @@ -116,7 +116,7 @@ public SecurityGroupWorkVO take(long serverId) { //ensure that there is no job in Processing state for the same VM processing = true; if (logger.isTraceEnabled()) { - logger.trace("Security Group work take: found a job in Scheduled and Processing vmid=" + work.getInstanceId()); + logger.trace("Security Group work take: found a job in Scheduled and Processing vmid={}", work.getInstanceId()); } } work.setServerId(serverId); @@ -141,26 +141,16 @@ public SecurityGroupWorkVO take(long serverId) { } @Override - @DB public void updateStep(Long vmId, Long logSequenceNumber, Step step) { - final TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); SearchCriteria sc = VmIdSeqNumSearch.create(); sc.setParameters("vmId", vmId); sc.setParameters("seqno", logSequenceNumber); - final Filter filter = new Filter(SecurityGroupWorkVO.class, null, true, 0l, 1l); - - final List vos = lockRows(sc, filter, true); - if (vos.size() == 0) { - txn.commit(); - return; - } - SecurityGroupWorkVO work = vos.get(0); - work.setStep(step); - update(work.getId(), work); - - txn.commit(); + SecurityGroupWorkVO workForUpdate = createForUpdate(); + workForUpdate.setStep(step); + // LIMIT 1 preserves the original single-row semantics: op_nwgrp_work has no + // uniqueness on (instance_id, seq_no), so without it duplicate rows would all be updated. + update(workForUpdate, sc, 1); } @Override @@ -172,21 +162,10 @@ public SecurityGroupWorkVO findByVmIdStep(long vmId, Step step) { } @Override - @DB public void updateStep(Long workId, Step step) { - final TransactionLegacy txn = TransactionLegacy.currentTxn(); - txn.start(); - - SecurityGroupWorkVO work = lockRow(workId, true); - if (work == null) { - txn.commit(); - return; - } - work.setStep(step); - update(work.getId(), work); - - txn.commit(); - + SecurityGroupWorkVO workForUpdate = createForUpdate(); + workForUpdate.setStep(step); + update(workId, workForUpdate); } @Override diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java index 9320a37bc96e..b913468384e4 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java @@ -91,6 +91,9 @@ public class VpcOfferingVO implements VpcOffering { @Column(name = "specify_as_number") private Boolean specifyAsNumber = false; + @Column(name = "conserve_mode") + private boolean conserveMode; + public VpcOfferingVO() { this.uuid = UUID.randomUUID().toString(); } @@ -242,4 +245,13 @@ public Boolean isSpecifyAsNumber() { public void setSpecifyAsNumber(Boolean specifyAsNumber) { this.specifyAsNumber = specifyAsNumber; } + + @Override + public boolean isConserveMode() { + return conserveMode; + } + + public void setConserveMode(boolean conserveMode) { + this.conserveMode = conserveMode; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java index 9fa4b505c650..ae156d7d4304 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcServiceMapVO.java @@ -25,8 +25,6 @@ import javax.persistence.Id; import javax.persistence.Table; -import com.cloud.network.Network.Provider; -import com.cloud.network.Network.Service; import com.cloud.utils.db.GenericDao; @Entity @@ -72,10 +70,10 @@ public Date getCreated() { public VpcServiceMapVO() { } - public VpcServiceMapVO(long vpcId, Service service, Provider provider) { + public VpcServiceMapVO(long vpcId, String serviceName, String providerName) { this.vpcId = vpcId; - this.service = service.getName(); - this.provider = provider.getName(); + this.service = serviceName; + this.provider = providerName; } @Override diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcVO.java index e942eadb8ffb..742d3f2f82ee 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcVO.java @@ -108,6 +108,9 @@ public class VpcVO implements Vpc { @Column(name = "use_router_ip_resolver") boolean useRouterIpResolver = false; + @Column(name = "keep_mac_address_on_public_nic") + private boolean keepMacAddressOnPublicNic = true; + @Transient boolean rollingRestart = false; @@ -321,4 +324,13 @@ public boolean useRouterIpAsResolver() { public void setUseRouterIpResolver(boolean useRouterIpResolver) { this.useRouterIpResolver = useRouterIpResolver; } + + @Override + public boolean getKeepMacAddressOnPublicNic() { + return keepMacAddressOnPublicNic; + } + + public void setKeepMacAddressOnPublicNic(boolean keepMacAddressOnPublicNic) { + this.keepMacAddressOnPublicNic = keepMacAddressOnPublicNic; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java index 4e13fe4f5d0d..cecdce1aba42 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcDaoImpl.java @@ -142,7 +142,7 @@ public void persistVpcServiceProviders(long vpcId, Map> ser txn.start(); for (String service : serviceProviderMap.keySet()) { for (String provider : serviceProviderMap.get(service)) { - VpcServiceMapVO serviceMap = new VpcServiceMapVO(vpcId, Network.Service.getService(service), Network.Provider.getProvider(provider)); + VpcServiceMapVO serviceMap = new VpcServiceMapVO(vpcId, Network.Service.getService(service).getName(), provider); _vpcSvcMap.persist(serviceMap); } } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java index 020536e97ec9..4bc9ad646d38 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java @@ -42,4 +42,6 @@ public interface VpcOfferingServiceMapDao extends GenericDao listProvidersForServiceForVpcOffering(long vpcOfferingId, Service service); + List listOfferingIdsByServiceAndProvider(Service service, String provider); + } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java index dcb1becf9e88..de36b6c3219b 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java @@ -37,6 +37,7 @@ public class VpcOfferingServiceMapDaoImpl extends GenericDaoBase AllFieldsSearch; final SearchBuilder MultipleServicesSearch; final GenericSearchBuilder ServicesSearch; + final GenericSearchBuilder OfferingIdsByServiceAndProviderSearch; protected VpcOfferingServiceMapDaoImpl() { super(); @@ -56,6 +57,12 @@ protected VpcOfferingServiceMapDaoImpl() { ServicesSearch.and("offeringId", ServicesSearch.entity().getVpcOfferingId(), SearchCriteria.Op.EQ); ServicesSearch.select(null, Func.DISTINCT, ServicesSearch.entity().getService()); ServicesSearch.done(); + + OfferingIdsByServiceAndProviderSearch = createSearchBuilder(Long.class); + OfferingIdsByServiceAndProviderSearch.selectFields(OfferingIdsByServiceAndProviderSearch.entity().getVpcOfferingId()); + OfferingIdsByServiceAndProviderSearch.and("service", OfferingIdsByServiceAndProviderSearch.entity().getService(), SearchCriteria.Op.EQ); + OfferingIdsByServiceAndProviderSearch.and("provider", OfferingIdsByServiceAndProviderSearch.entity().getProvider(), SearchCriteria.Op.EQ); + OfferingIdsByServiceAndProviderSearch.done(); } @Override @@ -129,4 +136,12 @@ public List listProvidersForServiceForVpcOffering(long return customSearch(sc, null); } + + @Override + public List listOfferingIdsByServiceAndProvider(Service service, String provider) { + SearchCriteria sc = OfferingIdsByServiceAndProviderSearch.create(); + sc.setParameters("service", service.getName()); + sc.setParameters("provider", provider); + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDao.java b/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDao.java index 8fb5d6dff84b..5954f95b3c57 100644 --- a/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDao.java +++ b/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDao.java @@ -44,4 +44,6 @@ public interface NetworkOfferingServiceMapDao extends GenericDao listServicesForNetworkOffering(long networkOfferingId); List getDistinctProviders(long offId); + + List listOfferingIdsByServiceAndProvider(Service service, String provider); } diff --git a/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDaoImpl.java index 67b341a93618..5fe86760d50a 100644 --- a/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/offerings/dao/NetworkOfferingServiceMapDaoImpl.java @@ -40,6 +40,7 @@ public class NetworkOfferingServiceMapDaoImpl extends GenericDaoBase ProvidersSearch; final GenericSearchBuilder ServicesSearch; final GenericSearchBuilder DistinctProvidersSearch; + final GenericSearchBuilder OfferingIdsByServiceAndProviderSearch; protected NetworkOfferingServiceMapDaoImpl() { super(); @@ -71,6 +72,12 @@ protected NetworkOfferingServiceMapDaoImpl() { DistinctProvidersSearch.and("provider", DistinctProvidersSearch.entity().getProvider(), SearchCriteria.Op.EQ); DistinctProvidersSearch.selectFields(DistinctProvidersSearch.entity().getProvider()); DistinctProvidersSearch.done(); + + OfferingIdsByServiceAndProviderSearch = createSearchBuilder(Long.class); + OfferingIdsByServiceAndProviderSearch.selectFields(OfferingIdsByServiceAndProviderSearch.entity().getNetworkOfferingId()); + OfferingIdsByServiceAndProviderSearch.and("service", OfferingIdsByServiceAndProviderSearch.entity().getService(), SearchCriteria.Op.EQ); + OfferingIdsByServiceAndProviderSearch.and("provider", OfferingIdsByServiceAndProviderSearch.entity().getProvider(), SearchCriteria.Op.EQ); + OfferingIdsByServiceAndProviderSearch.done(); } @Override @@ -170,4 +177,12 @@ public List getDistinctProviders(long offId) { List results = customSearch(sc, null); return results; } + + @Override + public List listOfferingIdsByServiceAndProvider(Service service, String provider) { + SearchCriteria sc = OfferingIdsByServiceAndProviderSearch.create(); + sc.setParameters("service", service.getName()); + sc.setParameters("provider", provider); + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java b/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java index 4a504333344f..be7ba2843321 100644 --- a/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/SnapshotVO.java @@ -248,6 +248,10 @@ public Date getRemoved() { return removed; } + public void setRemoved(Date removed) { + this.removed = removed; + } + @Override public State getState() { return state; diff --git a/engine/schema/src/main/java/com/cloud/storage/VolumeVO.java b/engine/schema/src/main/java/com/cloud/storage/VolumeVO.java index 653be54a9109..3815125b3487 100644 --- a/engine/schema/src/main/java/com/cloud/storage/VolumeVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/VolumeVO.java @@ -182,6 +182,12 @@ public class VolumeVO implements Volume { @Column(name = "passphrase_id") private Long passphraseId; + @Column(name = "kms_key_id") + private Long kmsKeyId; + + @Column(name = "kms_wrapped_key_id") + private Long kmsWrappedKeyId; + @Column(name = "encrypt_format") private String encryptFormat; @@ -683,6 +689,14 @@ public void setExternalUuid(String externalUuid) { public void setPassphraseId(Long id) { this.passphraseId = id; } + public Long getKmsKeyId() { return kmsKeyId; } + + public void setKmsKeyId(Long id) { this.kmsKeyId = id; } + + public Long getKmsWrappedKeyId() { return kmsWrappedKeyId; } + + public void setKmsWrappedKeyId(Long id) { this.kmsWrappedKeyId = id; } + public String getEncryptFormat() { return encryptFormat; } public void setEncryptFormat(String encryptFormat) { this.encryptFormat = encryptFormat; } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java index 9beea0037444..ea97f663768f 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java @@ -30,7 +30,7 @@ public interface DiskOfferingDao extends GenericDao { List listAllBySizeAndProvisioningType(long size, Storage.ProvisioningType provisioningType); - List findCustomDiskOfferings(); + List listCustomDiskOfferings(); List listByStorageTag(String tag); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java index 4ca3fe9f12ac..44e6551da17d 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.storage.dao; +import static org.apache.cloudstack.query.QueryService.SortKeyAscending; + import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -33,6 +35,7 @@ import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Storage; import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -138,13 +141,14 @@ public List listAllBySizeAndProvisioningType(long size, Storage. } @Override - public List findCustomDiskOfferings() { + public List listCustomDiskOfferings() { SearchBuilder sb = createSearchBuilder(); sb.and("customized", sb.entity().isCustomized(), SearchCriteria.Op.EQ); sb.done(); SearchCriteria sc = sb.create(); sc.setParameters("customized", true); - return listBy(sc); + Filter searchFilter = new Filter(DiskOfferingVO.class, "sortKey", SortKeyAscending.value()); + return listBy(sc, searchFilter); } @Override diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDao.java index 1a2b098c40a7..f24f3f3b67c4 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDao.java @@ -35,7 +35,7 @@ public interface GuestOSDao extends GenericDao { List listByDisplayName(String displayName); - Pair, Integer> listGuestOSByCriteria(Long startIndex, Long pageSize, Long id, Long osCategoryId, String description, String keyword, Boolean forDisplay); + Pair, Integer> listGuestOSByCriteria(Long startIndex, Long pageSize, List ids, Long osCategoryId, String description, String keyword, Boolean forDisplay); List listIdsByCategoryId(final long categoryId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDaoImpl.java index 881be207c1aa..09f8772fb59c 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/GuestOSDaoImpl.java @@ -125,12 +125,12 @@ public List listByDisplayName(String displayName) { return listBy(sc); } - public Pair, Integer> listGuestOSByCriteria(Long startIndex, Long pageSize, Long id, Long osCategoryId, String description, String keyword, Boolean forDisplay) { + public Pair, Integer> listGuestOSByCriteria(Long startIndex, Long pageSize, List ids, Long osCategoryId, String description, String keyword, Boolean forDisplay) { final Filter searchFilter = new Filter(GuestOSVO.class, "displayName", true, startIndex, pageSize); final SearchCriteria sc = createSearchCriteria(); - if (id != null) { - sc.addAnd("id", SearchCriteria.Op.EQ, id); + if (CollectionUtils.isNotEmpty(ids)) { + sc.addAnd("id", SearchCriteria.Op.IN, ids.toArray()); } if (osCategoryId != null) { diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java index 80e1b7d4d4be..a5596ed840cc 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -56,10 +56,18 @@ public class SnapshotDaoImpl extends GenericDaoBase implements // TODO: we should remove these direct sqls private static final String GET_LAST_SNAPSHOT = "SELECT snapshots.id FROM snapshot_store_ref, snapshots where snapshots.id = snapshot_store_ref.snapshot_id AND snapshosts.volume_id = ? AND snapshot_store_ref.role = ? ORDER BY created DESC"; - private static final String VOLUME_ID = "volumeId"; - private static final String NOT_TYPE = "notType"; + private static final String TYPE = "type"; private static final String STATUS = "status"; + private static final String VERSION = "version"; + private static final String ACCOUNT_ID = "accountId"; + private static final String REMOVED = "removed"; + private static final String NOT_TYPE = "notType"; + private static final String ID = "id"; + private static final String INSTANCE_ID = "instanceId"; + private static final String STATE = "state"; + private static final String INSTANCE_VOLUMES = "instanceVolumes"; + private static final String INSTANCE_SNAPSHOTS = "instanceSnapshots"; private SearchBuilder snapshotIdsSearch; private SearchBuilder VolumeIdSearch; @@ -83,9 +91,9 @@ public class SnapshotDaoImpl extends GenericDaoBase implements @Override public List listByVolumeIdTypeNotDestroyed(long volumeId, Type type) { SearchCriteria sc = VolumeIdTypeNotDestroyedSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("type", type.ordinal()); - sc.setParameters("status", State.Destroyed); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(TYPE, type.ordinal()); + sc.setParameters(STATUS, State.Destroyed); return listBy(sc, null); } @@ -102,28 +110,28 @@ public List listByVolumeId(long volumeId) { @Override public List listByVolumeId(Filter filter, long volumeId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", volumeId); + sc.setParameters(VOLUME_ID, volumeId); return listBy(sc, filter); } @Override public List listByVolumeIdIncludingRemoved(long volumeId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", volumeId); + sc.setParameters(VOLUME_ID, volumeId); return listIncludingRemovedBy(sc, null); } public List listByVolumeIdType(Filter filter, long volumeId, Type type) { SearchCriteria sc = VolumeIdTypeSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("type", type.ordinal()); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(TYPE, type.ordinal()); return listBy(sc, filter); } public List listByVolumeIdVersion(Filter filter, long volumeId, String version) { SearchCriteria sc = VolumeIdVersionSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("version", version); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(VERSION, version); return listBy(sc, filter); } @@ -133,60 +141,61 @@ public SnapshotDaoImpl() { @PostConstruct protected void init() { VolumeIdSearch = createSearchBuilder(); - VolumeIdSearch.and("volumeId", VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdSearch.and(VOLUME_ID, VolumeIdSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); VolumeIdSearch.done(); VolumeIdTypeSearch = createSearchBuilder(); - VolumeIdTypeSearch.and("volumeId", VolumeIdTypeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdTypeSearch.and("type", VolumeIdTypeSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); + VolumeIdTypeSearch.and(VOLUME_ID, VolumeIdTypeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdTypeSearch.and(TYPE, VolumeIdTypeSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); VolumeIdTypeSearch.done(); VolumeIdTypeNotDestroyedSearch = createSearchBuilder(); - VolumeIdTypeNotDestroyedSearch.and("volumeId", VolumeIdTypeNotDestroyedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdTypeNotDestroyedSearch.and("type", VolumeIdTypeNotDestroyedSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); - VolumeIdTypeNotDestroyedSearch.and("status", VolumeIdTypeNotDestroyedSearch.entity().getState(), SearchCriteria.Op.NEQ); + VolumeIdTypeNotDestroyedSearch.and(VOLUME_ID, VolumeIdTypeNotDestroyedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdTypeNotDestroyedSearch.and(TYPE, VolumeIdTypeNotDestroyedSearch.entity().getSnapshotType(), SearchCriteria.Op.EQ); + VolumeIdTypeNotDestroyedSearch.and(STATUS, VolumeIdTypeNotDestroyedSearch.entity().getState(), SearchCriteria.Op.NEQ); VolumeIdTypeNotDestroyedSearch.done(); VolumeIdVersionSearch = createSearchBuilder(); - VolumeIdVersionSearch.and("volumeId", VolumeIdVersionSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - VolumeIdVersionSearch.and("version", VolumeIdVersionSearch.entity().getVersion(), SearchCriteria.Op.EQ); + VolumeIdVersionSearch.and(VOLUME_ID, VolumeIdVersionSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + VolumeIdVersionSearch.and(VERSION, VolumeIdVersionSearch.entity().getVersion(), SearchCriteria.Op.EQ); VolumeIdVersionSearch.done(); AccountIdSearch = createSearchBuilder(); - AccountIdSearch.and("accountId", AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountIdSearch.and(ACCOUNT_ID, AccountIdSearch.entity().getAccountId(), SearchCriteria.Op.EQ); AccountIdSearch.done(); StatusSearch = createSearchBuilder(); - StatusSearch.and("volumeId", StatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - StatusSearch.and("status", StatusSearch.entity().getState(), SearchCriteria.Op.IN); + StatusSearch.and(VOLUME_ID, StatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + StatusSearch.and(STATUS, StatusSearch.entity().getState(), SearchCriteria.Op.IN); StatusSearch.done(); notInStatusSearch = createSearchBuilder(); - notInStatusSearch.and("volumeId", notInStatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - notInStatusSearch.and("status", notInStatusSearch.entity().getState(), SearchCriteria.Op.NOTIN); + notInStatusSearch.and(VOLUME_ID, notInStatusSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + notInStatusSearch.and(STATUS, notInStatusSearch.entity().getState(), SearchCriteria.Op.NOTIN); notInStatusSearch.done(); CountSnapshotsByAccount = createSearchBuilder(Long.class); CountSnapshotsByAccount.select(null, Func.COUNT, null); - CountSnapshotsByAccount.and("account", CountSnapshotsByAccount.entity().getAccountId(), SearchCriteria.Op.EQ); - CountSnapshotsByAccount.and("status", CountSnapshotsByAccount.entity().getState(), SearchCriteria.Op.NIN); - CountSnapshotsByAccount.and("removed", CountSnapshotsByAccount.entity().getRemoved(), SearchCriteria.Op.NULL); + CountSnapshotsByAccount.and(ACCOUNT_ID, CountSnapshotsByAccount.entity().getAccountId(), SearchCriteria.Op.EQ); + CountSnapshotsByAccount.and(STATUS, CountSnapshotsByAccount.entity().getState(), SearchCriteria.Op.NIN); + CountSnapshotsByAccount.and("snapshotTypeNEQ", CountSnapshotsByAccount.entity().getSnapshotType(), SearchCriteria.Op.NIN); + CountSnapshotsByAccount.and(REMOVED, CountSnapshotsByAccount.entity().getRemoved(), SearchCriteria.Op.NULL); CountSnapshotsByAccount.done(); InstanceIdSearch = createSearchBuilder(); - InstanceIdSearch.and("status", InstanceIdSearch.entity().getState(), SearchCriteria.Op.IN); + InstanceIdSearch.and(STATUS, InstanceIdSearch.entity().getState(), SearchCriteria.Op.IN); snapshotIdsSearch = createSearchBuilder(); - snapshotIdsSearch.and("id", snapshotIdsSearch.entity().getId(), SearchCriteria.Op.IN); + snapshotIdsSearch.and(ID, snapshotIdsSearch.entity().getId(), SearchCriteria.Op.IN); SearchBuilder instanceSearch = _instanceDao.createSearchBuilder(); - instanceSearch.and("instanceId", instanceSearch.entity().getId(), SearchCriteria.Op.EQ); + instanceSearch.and(INSTANCE_ID, instanceSearch.entity().getId(), SearchCriteria.Op.EQ); SearchBuilder volumeSearch = _volumeDao.createSearchBuilder(); - volumeSearch.and("state", volumeSearch.entity().getState(), SearchCriteria.Op.EQ); - volumeSearch.join("instanceVolumes", instanceSearch, instanceSearch.entity().getId(), volumeSearch.entity().getInstanceId(), JoinType.INNER); + volumeSearch.and(STATE, volumeSearch.entity().getState(), SearchCriteria.Op.EQ); + volumeSearch.join(INSTANCE_VOLUMES, instanceSearch, instanceSearch.entity().getId(), volumeSearch.entity().getInstanceId(), JoinType.INNER); - InstanceIdSearch.join("instanceSnapshots", volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER); + InstanceIdSearch.join(INSTANCE_SNAPSHOTS, volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER); InstanceIdSearch.done(); volumeIdAndTypeNotInSearch = createSearchBuilder(); @@ -218,8 +227,9 @@ public long getLastSnapshot(long volumeId, DataStoreRole role) { @Override public Long countSnapshotsForAccount(long accountId) { SearchCriteria sc = CountSnapshotsByAccount.create(); - sc.setParameters("account", accountId); - sc.setParameters("status", State.Error, State.Destroyed); + sc.setParameters(ACCOUNT_ID, accountId); + sc.setParameters(STATUS, State.Error, State.Destroyed); + sc.setParameters("snapshotTypeNEQ", Snapshot.Type.GROUP.ordinal()); return customSearch(sc, null).get(0); } @@ -228,19 +238,19 @@ public List listByInstanceId(long instanceId, Snapshot.State... stat SearchCriteria sc = InstanceIdSearch.create(); if (status != null && status.length != 0) { - sc.setParameters("status", (Object[])status); + sc.setParameters(STATUS, (Object[])status); } - sc.setJoinParameters("instanceSnapshots", "state", Volume.State.Ready); - sc.setJoinParameters("instanceVolumes", "instanceId", instanceId); + sc.setJoinParameters(INSTANCE_SNAPSHOTS, STATE, Volume.State.Ready); + sc.setJoinParameters(INSTANCE_VOLUMES, INSTANCE_ID, instanceId); return listBy(sc, null); } @Override public List listByStatus(long volumeId, Snapshot.State... status) { SearchCriteria sc = StatusSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("status", (Object[])status); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(STATUS, (Object[])status); return listBy(sc, null); } @@ -261,7 +271,7 @@ public boolean remove(Long id) { @Override public List listAllByStatus(Snapshot.State... status) { SearchCriteria sc = StatusSearch.create(); - sc.setParameters("status", (Object[])status); + sc.setParameters(STATUS, (Object[])status); return listBy(sc, null); } @@ -275,7 +285,7 @@ public List listAllByStatusIncludingRemoved(Snapshot.State... status @Override public List listByIds(Object... ids) { SearchCriteria sc = snapshotIdsSearch.create(); - sc.setParameters("id", ids); + sc.setParameters(ID, ids); return listBy(sc, null); } @@ -293,7 +303,7 @@ public boolean updateState(State currentState, Event event, State nextState, Sna @Override public void updateVolumeIds(long oldVolId, long newVolId) { SearchCriteria sc = VolumeIdSearch.create(); - sc.setParameters("volumeId", oldVolId); + sc.setParameters(VOLUME_ID, oldVolId); SnapshotVO snapshot = createForUpdate(); snapshot.setVolumeId(newVolId); UpdateBuilder ub = getUpdateBuilder(snapshot); @@ -303,8 +313,8 @@ public void updateVolumeIds(long oldVolId, long newVolId) { @Override public List listByStatusNotIn(long volumeId, Snapshot.State... status) { SearchCriteria sc = this.notInStatusSearch.create(); - sc.setParameters("volumeId", volumeId); - sc.setParameters("status", (Object[]) status); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(STATUS, (Object[]) status); return listBy(sc, null); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java index 4c9f906b68a9..aec06d6d0003 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java @@ -106,4 +106,6 @@ public interface VMTemplateDao extends GenericDao, StateDao< VMTemplateVO findActiveSystemTemplateByHypervisorArchAndUrlPath(HypervisorType hypervisorType, CPU.CPUArch arch, String urlPathSuffix); + + VMTemplateVO findByAccountAndName(Long accountId, String templateName); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index bcf8b39a291f..8c6e3fe0983f 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -457,7 +457,7 @@ public void saveDetails(VMTemplateVO tmpl) { if (detailsStr == null) { return; } - List details = new ArrayList(); + List details = new ArrayList<>(); for (String key : detailsStr.keySet()) { VMTemplateDetailVO detail = new VMTemplateDetailVO(tmpl.getId(), key, detailsStr.get(key), true); details.add(detail); @@ -479,7 +479,7 @@ public long addTemplateToZone(VMTemplateVO tmplt, long zoneId) { } if (tmplt.getDetails() != null) { - List details = new ArrayList(); + List details = new ArrayList<>(); for (String key : tmplt.getDetails().keySet()) { details.add(new VMTemplateDetailVO(tmplt.getId(), key, tmplt.getDetails().get(key), true)); } @@ -945,4 +945,12 @@ public boolean updateState( } return rows > 0; } + + @Override + public VMTemplateVO findByAccountAndName(Long accountId, String templateName) { + SearchCriteria sc = NameAccountIdSearch.create(); + sc.setParameters("name", templateName); + sc.setParameters("accountId", accountId); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index a03b94faa797..4cd9a8e23bfc 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -46,6 +46,8 @@ public interface VolumeDao extends GenericDao, StateDao findByInstanceAndType(long id, Volume.Type vType); + List findByInstanceAndNotStates(long id, Volume.State...states); + List findIncludingRemovedByInstanceAndType(long id, Volume.Type vType); List findNonDestroyedVolumesByInstanceIdAndPoolId(long instanceId, long poolId); @@ -109,6 +111,17 @@ public interface VolumeDao extends GenericDao, StateDao listVolumesByPassphraseId(long passphraseId); + /** + * List volumes with passphrase_id for migration to KMS + * + * @param zoneId Zone ID (required) + * @param accountId Account ID filter (optional, null for all accounts) + * @param domainId Domain ID filter (optional, null for all domains) + * @param limit Maximum number of volumes to return + * @return list of volumes that need migration + */ + Pair, Integer> listVolumesForKMSMigration(Long zoneId, Long accountId, Long domainId, Integer limit); + /** * Gets the Total Primary Storage space allocated for an account * @@ -166,4 +179,16 @@ public interface VolumeDao extends GenericDao, StateDao findByKmsWrappedKeyId(Long kmsWrappedKeyId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 286d4ff75423..1d5ff5e93402 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.reservation.ReservationVO; import org.apache.cloudstack.reservation.dao.ReservationDao; +import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; @@ -74,16 +75,20 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol private final SearchBuilder storeAndInstallPathSearch; private final SearchBuilder volumeIdSearch; protected GenericSearchBuilder CountByAccount; + protected final SearchBuilder ExternalUuidSearch; protected GenericSearchBuilder primaryStorageSearch; protected GenericSearchBuilder primaryStorageSearch2; protected GenericSearchBuilder secondaryStorageSearch; private final SearchBuilder poolAndPathSearch; final GenericSearchBuilder CountByOfferingId; + private final SearchBuilder kmsMigrationSearch; @Inject ReservationDao reservationDao; @Inject ResourceTagDao tagsDao; + @Inject + KMSWrappedKeyDao kmsWrappedKeyDao; // need to account for zone-wide primary storage where storage_pool has // null-value pod and cluster, where hypervisor information is stored in @@ -207,6 +212,17 @@ public List findByInstanceAndType(long id, Type vType) { return listBy(sc); } + @Override + public List findByInstanceAndNotStates(long id, Volume.State...states) { + SearchBuilder sb = createSearchBuilder(); + sb.and("instanceId", sb.entity().getInstanceId(), Op.EQ); + sb.and("state", sb.entity().getState(), Op.NIN); + SearchCriteria sc = sb.create(); + sc.setParameters("instanceId", id); + sc.setParameters("state", (Object[]) states); + return listBy(sc); + } + @Override public List findIncludingRemovedByInstanceAndType(long id, Type vType) { SearchCriteria sc = AllFieldsSearch.create(); @@ -383,7 +399,7 @@ public ImageFormat getImageFormat(Long volumeId) { public VolumeDaoImpl() { AllFieldsSearch = createSearchBuilder(); - AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.EQ); + AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.IN); AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), Op.EQ); AllFieldsSearch.and("dcId", AllFieldsSearch.entity().getDataCenterId(), Op.EQ); AllFieldsSearch.and("pod", AllFieldsSearch.entity().getPodId(), Op.EQ); @@ -400,6 +416,8 @@ public VolumeDaoImpl() { AllFieldsSearch.and("passphraseId", AllFieldsSearch.entity().getPassphraseId(), Op.EQ); AllFieldsSearch.and("iScsiName", AllFieldsSearch.entity().get_iScsiName(), Op.EQ); AllFieldsSearch.and("path", AllFieldsSearch.entity().getPath(), Op.EQ); + AllFieldsSearch.and("kmsKeyId", AllFieldsSearch.entity().getKmsKeyId(), Op.EQ); + AllFieldsSearch.and("kmsWrappedKeyId", AllFieldsSearch.entity().getKmsWrappedKeyId(), Op.EQ); AllFieldsSearch.done(); RootDiskStateSearch = createSearchBuilder(); @@ -459,6 +477,10 @@ public VolumeDaoImpl() { CountByAccount.and("idNIN", CountByAccount.entity().getId(), Op.NIN); CountByAccount.done(); + ExternalUuidSearch = createSearchBuilder(); + ExternalUuidSearch.and("externalUuid", ExternalUuidSearch.entity().getExternalUuid(), Op.EQ); + ExternalUuidSearch.done(); + primaryStorageSearch = createSearchBuilder(SumCount.class); primaryStorageSearch.select("sum", Func.SUM, primaryStorageSearch.entity().getSize()); primaryStorageSearch.and("accountId", primaryStorageSearch.entity().getAccountId(), Op.EQ); @@ -512,6 +534,13 @@ public VolumeDaoImpl() { CountByOfferingId.select(null, Func.COUNT, CountByOfferingId.entity().getId()); CountByOfferingId.and("diskOfferingId", CountByOfferingId.entity().getDiskOfferingId(), Op.EQ); CountByOfferingId.done(); + + kmsMigrationSearch = createSearchBuilder(); + kmsMigrationSearch.and("passphraseId", kmsMigrationSearch.entity().getPassphraseId(), Op.NNULL); + kmsMigrationSearch.and("zoneId", kmsMigrationSearch.entity().getDataCenterId(), Op.EQ); + kmsMigrationSearch.and("accountId", kmsMigrationSearch.entity().getAccountId(), Op.EQ); + kmsMigrationSearch.and("domainId", kmsMigrationSearch.entity().getDomainId(), Op.EQ); + kmsMigrationSearch.done(); } @Override @@ -581,17 +610,16 @@ public long secondaryStorageUsedForAccount(long accountId) { @Override public List listVolumesToBeDestroyed() { - SearchCriteria sc = AllFieldsSearch.create(); - sc.setParameters("state", Volume.State.Destroy); - - return listBy(sc); + return listVolumesToBeDestroyed(null); } @Override public List listVolumesToBeDestroyed(Date date) { SearchCriteria sc = AllFieldsSearch.create(); - sc.setParameters("state", Volume.State.Destroy); - sc.setParameters("updateTime", date); + sc.setParameters("state", Volume.State.Destroy, Volume.State.Expunging); + if (date != null) { + sc.setParameters("updateTime", date); + } return listBy(sc); } @@ -733,6 +761,21 @@ public List listVolumesByPassphraseId(long passphraseId) { return listBy(sc); } + @Override + public Pair, Integer> listVolumesForKMSMigration(Long zoneId, Long accountId, Long domainId, Integer limit) { + SearchCriteria sc = kmsMigrationSearch.create(); + + Filter filter = new Filter(limit); + sc.setParameters("zoneId", zoneId); + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (domainId != null) { + sc.setParameters("domainId", domainId); + } + return searchAndCount(sc, filter); + } + @Override @DB public boolean remove(Long id) { @@ -741,6 +784,17 @@ public boolean remove(Long id) { logger.debug(String.format("Removing volume %s from DB", id)); VolumeVO entry = findById(id); if (entry != null) { + // Clean up KMS wrapped key if volume was encrypted with KMS + if (entry.getKmsWrappedKeyId() != null) { + try { + kmsWrappedKeyDao.remove(entry.getKmsWrappedKeyId()); + logger.debug("Removed KMS wrapped key [id={}] for volume [id={}, uuid={}]", + entry.getKmsWrappedKeyId(), id, entry.getUuid()); + } catch (Exception e) { + logger.warn("Failed to remove KMS wrapped key [id={}] for volume [id={}, uuid={}]: {}", + entry.getKmsWrappedKeyId(), id, entry.getUuid(), e.getMessage(), e); + } + } tagsDao.removeByIdAndType(id, ResourceObjectType.Volume); } boolean result = super.remove(id); @@ -935,4 +989,26 @@ public VolumeVO findByLastIdAndState(long lastVolumeId, State ...states) { sc.and(sc.entity().getState(), SearchCriteria.Op.IN, (Object[]) states); return sc.find(); } + + @Override + public boolean existsWithKmsKey(long kmsKeyId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + sc.setParameters("notDestroyed", Volume.State.Expunged, Volume.State.Destroy); + return findOneBy(sc) != null; + } + + public VolumeVO findByExternalUuid(String externalUuid) { + SearchCriteria sc = ExternalUuidSearch.create(); + sc.setParameters("externalUuid", externalUuid); + return findOneBy(sc); + } + + @Override + public List findByKmsWrappedKeyId(Long kmsWrappedKeyId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("kmsWrappedKeyId", kmsWrappedKeyId); + sc.setParameters("notDestroyed", Volume.State.Expunged); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java index bacb09b98793..5efaea40a943 100644 --- a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java +++ b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java @@ -20,11 +20,13 @@ import java.util.Map; import java.util.Set; +import org.apache.cloudstack.api.response.ResourceTagResponse; + import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.tags.ResourceTagVO; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; -import org.apache.cloudstack.api.response.ResourceTagResponse; public interface ResourceTagDao extends GenericDao { @@ -60,4 +62,13 @@ public interface ResourceTagDao extends GenericDao { void removeByResourceIdAndKey(long resourceId, ResourceObjectType resourceType, String key); List listByResourceUuid(String resourceUuid); + + List listByResourceTypeKeyPrefixAndOwners(ResourceObjectType resourceType, String key, + List accountIds, List domainIds, + Filter filter); + + ResourceTagVO findByResourceTypeKeyPrefixAndValue(ResourceObjectType resourceType, String key, String value); + + List listByResourceTypeIdAndKeyPrefix(ResourceObjectType resourceType, long resourceId, String key); + } diff --git a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java index cc9d99e6ab16..22c7b7b2ee5b 100644 --- a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java @@ -16,19 +16,22 @@ // under the License. package com.cloud.tags.dao; -import java.util.List; -import java.util.Set; -import java.util.Map; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.cloudstack.api.response.ResourceTagResponse; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.tags.ResourceTagVO; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; @@ -120,4 +123,62 @@ public List listByResourceUuid(String resourceUuid) { sc.setParameters("resourceUuid", resourceUuid); return listBy(sc); } + + @Override + public List listByResourceTypeKeyPrefixAndOwners(ResourceObjectType resourceType, String key, + List accountIds, List domainIds, + Filter filter) { + GenericSearchBuilder sb = createSearchBuilder(String.class); + sb.select(null, SearchCriteria.Func.DISTINCT, sb.entity().getValue()); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("key", key + "%"); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + return customSearch(sc, filter); + } + + @Override + public ResourceTagVO findByResourceTypeKeyPrefixAndValue(ResourceObjectType resourceType, String key, + String value) { + SearchBuilder sb = createSearchBuilder(); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + sb.and("value", sb.entity().getValue(), Op.EQ); + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("key", key + "%"); + sc.setParameters("value", value); + return findOneBy(sc); + } + + @Override + public List listByResourceTypeIdAndKeyPrefix(ResourceObjectType resourceType, long resourceId, + String key) { + SearchBuilder sb = createSearchBuilder(); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("resourceId", sb.entity().getResourceId(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("resourceId", resourceId); + sc.setParameters("key", key + "%"); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java index 0e784d961b3d..c3a982aa70e5 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -88,7 +88,9 @@ import com.cloud.upgrade.dao.Upgrade41900to41910; import com.cloud.upgrade.dao.Upgrade41910to42000; import com.cloud.upgrade.dao.Upgrade42000to42010; -import com.cloud.upgrade.dao.Upgrade42010to42100; +import com.cloud.upgrade.dao.Upgrade42020to42030; +import com.cloud.upgrade.dao.Upgrade42030to42040; +import com.cloud.upgrade.dao.Upgrade42040to42100; import com.cloud.upgrade.dao.Upgrade42100to42200; import com.cloud.upgrade.dao.Upgrade42200to42210; import com.cloud.upgrade.dao.Upgrade420to421; @@ -240,7 +242,9 @@ public DatabaseUpgradeChecker() { .next("4.19.0.0", new Upgrade41900to41910()) .next("4.19.1.0", new Upgrade41910to42000()) .next("4.20.0.0", new Upgrade42000to42010()) - .next("4.20.1.0", new Upgrade42010to42100()) + .next("4.20.2.0", new Upgrade42020to42030()) + .next("4.20.3.0", new Upgrade42030to42040()) + .next("4.20.4.0", new Upgrade42040to42100()) .next("4.21.0.0", new Upgrade42100to42200()) .next("4.22.0.0", new Upgrade42200to42210()) .next("4.22.1.0", new Upgrade42210to42300()) diff --git a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseVersionHierarchy.java b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseVersionHierarchy.java index 445a59310fb4..377b2f913755 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/DatabaseVersionHierarchy.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/DatabaseVersionHierarchy.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.upgrade; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -96,7 +97,9 @@ public DbUpgrade[] getPath(final CloudStackVersion fromVersion, final CloudStack // we cannot find the version specified, so get the // most recent one immediately before this version if (!contains(fromVersion)) { - return getPath(getRecentVersion(fromVersion), toVersion); + DbUpgrade[] dbUpgrades = getPath(getRecentVersion(fromVersion), toVersion); + return Arrays.stream(dbUpgrades).filter(up -> CloudStackVersion.compare(up.getUpgradedVersion(), fromVersion.toString()) > 0) + .toArray(DbUpgrade[]::new); } final Predicate predicate; diff --git a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java index 292bafefbb65..2acb4138d234 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/SystemVmTemplateRegistration.java @@ -1073,7 +1073,7 @@ protected void updateRegisteredTemplateDetails(Long templateId, MetadataTemplate } Hypervisor.HypervisorType hypervisorType = templateDetails.getHypervisorType(); updateSystemVMEntries(templateId, hypervisorType); - updateConfigurationParams(hypervisorType, templateDetails.getName(), zoneId); + updateConfigurationParams(hypervisorType, templateVO.getName(), zoneId); } protected void updateTemplateUrlChecksumAndGuestOsId(VMTemplateVO templateVO, diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java index a78f93fbdd4f..5c47087b9689 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade410to420.java @@ -1947,7 +1947,7 @@ private void migrateS3ToImageStore(Connection conn) { Map detailMap = new HashMap(); detailMap.put(ApiConstants.S3_ACCESS_KEY, s3_accesskey); - detailMap.put(ApiConstants.S3_SECRET_KEY, s3_secretkey); + detailMap.put(ApiConstants.SECRET_KEY, s3_secretkey); detailMap.put(ApiConstants.S3_BUCKET_NAME, s3_bucket); detailMap.put(ApiConstants.S3_END_POINT, s3_endpoint); detailMap.put(ApiConstants.S3_HTTPS_FLAG, String.valueOf(s3_https)); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42020to42030.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42020to42030.java index 68100e164018..e0aa38a717b2 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42020to42030.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42020to42030.java @@ -57,8 +57,4 @@ public void performDataMigration(Connection conn) { public InputStream[] getCleanupScripts() { return null; } - - @Override - public void updateSystemVmTemplates(Connection conn) { - } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42030to42040.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42030to42040.java new file mode 100644 index 000000000000..90f69a87cb03 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42030to42040.java @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.upgrade.dao; + +import java.io.InputStream; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +public class Upgrade42030to42040 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { + + @Override + public String[] getUpgradableVersionRange() { + return new String[]{"4.20.3.0", "4.20.4.0"}; + } + + @Override + public String getUpgradedVersion() { + return "4.20.4.0"; + } + + @Override + public boolean supportsRollingUpgrade() { + return false; + } + + @Override + public InputStream[] getPrepareScripts() { + return null; + } + + @Override + public void performDataMigration(Connection conn) { + final List indexList = new ArrayList(); + logger.debug("Dropping index vm_instance_id from usage_vm_instance table if it exists"); + indexList.add("vm_instance_id"); + DbUpgradeUtils.dropKeysIfExist(conn, "cloud_usage.usage_vm_instance", indexList, false); + } + + @Override + public InputStream[] getCleanupScripts() { + return null; + } +} diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java similarity index 97% rename from engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java rename to engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java index 786ee5afbc8e..64ac2c90bc07 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42010to42100.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42040to42100.java @@ -32,12 +32,12 @@ import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; -public class Upgrade42010to42100 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { +public class Upgrade42040to42100 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { private SystemVmTemplateRegistration systemVmTemplateRegistration; @Override public String[] getUpgradableVersionRange() { - return new String[] {"4.20.1.0", "4.21.0.0"}; + return new String[] {"4.20.4.0", "4.21.0.0"}; } @Override @@ -52,7 +52,7 @@ public boolean supportsRollingUpgrade() { @Override public InputStream[] getPrepareScripts() { - final String scriptFile = "META-INF/db/schema-42010to42100.sql"; + final String scriptFile = "META-INF/db/schema-42040to42100.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); @@ -69,7 +69,7 @@ public void performDataMigration(Connection conn) { @Override public InputStream[] getCleanupScripts() { - final String scriptFile = "META-INF/db/schema-42010to42100-cleanup.sql"; + final String scriptFile = "META-INF/db/schema-42040to42100-cleanup.sql"; final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); if (script == null) { throw new CloudRuntimeException("Unable to find " + scriptFile); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42200to42210.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42200to42210.java index c9610f7b9ff5..813351d75341 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42200to42210.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42200to42210.java @@ -16,6 +16,14 @@ // under the License. package com.cloud.upgrade.dao; +import org.apache.cloudstack.vm.UnmanagedVMsManager; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + public class Upgrade42200to42210 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { @Override @@ -27,4 +35,47 @@ public String[] getUpgradableVersionRange() { public String getUpgradedVersion() { return "4.22.1.0"; } + + @Override + public void performDataMigration(Connection conn) { + removeDuplicateKVMImportTemplates(conn); + } + + private void removeDuplicateKVMImportTemplates(Connection conn) { + List templateIds = new ArrayList<>(); + try (PreparedStatement selectStmt = conn.prepareStatement(String.format("SELECT id FROM cloud.vm_template WHERE name='%s' ORDER BY id ASC", UnmanagedVMsManager.KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME))) { + ResultSet rs = selectStmt.executeQuery(); + while (rs.next()) { + templateIds.add(rs.getLong(1)); + } + + if (templateIds.size() <= 1) { + return; + } + + logger.info("Removing duplicate template " + UnmanagedVMsManager.KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME + " entries"); + Long firstTemplateId = templateIds.get(0); + + String updateTemplateSql = "UPDATE cloud.vm_instance SET vm_template_id = ? WHERE vm_template_id = ?"; + String deleteTemplateSql = "DELETE FROM cloud.vm_template WHERE id = ?"; + + try (PreparedStatement updateTemplateStmt = conn.prepareStatement(updateTemplateSql); + PreparedStatement deleteTemplateStmt = conn.prepareStatement(deleteTemplateSql)) { + for (int i = 1; i < templateIds.size(); i++) { + Long duplicateTemplateId = templateIds.get(i); + + // Update VM references + updateTemplateStmt.setLong(1, firstTemplateId); + updateTemplateStmt.setLong(2, duplicateTemplateId); + updateTemplateStmt.executeUpdate(); + + // Delete duplicate dummy template + deleteTemplateStmt.setLong(1, duplicateTemplateId); + deleteTemplateStmt.executeUpdate(); + } + } + } catch (Exception e) { + logger.warn("Failed to remove duplicate template " + UnmanagedVMsManager.KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME + " entries", e); + } + } } diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42210to42300.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42210to42300.java index df4743894c9d..b60a52bba88b 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42210to42300.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42210to42300.java @@ -17,7 +17,14 @@ package com.cloud.upgrade.dao; import java.io.InputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.exception.CloudRuntimeException; public class Upgrade42210to42300 extends DbUpgradeAbstractImpl implements DbUpgrade, DbUpgradeSystemVmTemplate { @@ -42,4 +49,54 @@ public InputStream[] getPrepareScripts() { return new InputStream[] {script}; } + + @Override + public void performDataMigration(Connection conn) { + unhideJsInterpretationEnabled(conn); + dropUsageVmInstanceIndex(conn); + } + + protected void unhideJsInterpretationEnabled(Connection conn) { + String value = getJsInterpretationEnabled(conn); + if (value != null) { + updateJsInterpretationEnabledFields(conn, value); + } + } + + protected String getJsInterpretationEnabled(Connection conn) { + String query = "SELECT value FROM cloud.configuration WHERE name = 'js.interpretation.enabled' AND category = 'Hidden';"; + + try (PreparedStatement pstmt = conn.prepareStatement(query)) { + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + return rs.getString("value"); + } + logger.debug("Unable to retrieve value of hidden configuration 'js.interpretation.enabled'. The configuration may already be unhidden."); + return null; + } catch (SQLException e) { + throw new CloudRuntimeException("Error while retrieving value of hidden configuration 'js.interpretation.enabled'.", e); + } + } + + protected void updateJsInterpretationEnabledFields(Connection conn, String encryptedValue) { + String query = "UPDATE cloud.configuration SET value = ?, category = 'System', component = 'JsInterpreter', is_dynamic = 1 WHERE name = 'js.interpretation.enabled';"; + + try (PreparedStatement pstmt = conn.prepareStatement(query)) { + String decryptedValue = DBEncryptionUtil.decrypt(encryptedValue); + logger.info("Updating setting 'js.interpretation.enabled' to decrypted value [{}], category 'System', component 'JsInterpreter', and is_dynamic '1'.", decryptedValue); + pstmt.setString(1, decryptedValue); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new CloudRuntimeException("Error while unhiding configuration 'js.interpretation.enabled'.", e); + } catch (CloudRuntimeException e) { + logger.warn("Error while decrypting configuration 'js.interpretation.enabled'. The configuration may already be decrypted."); + } + } + + private void dropUsageVmInstanceIndex(Connection conn) { + final List indexList = new ArrayList<>(); + logger.debug("Dropping index vm_instance_id from usage_vm_instance table if it exists"); + indexList.add("vm_instance_id"); + DbUpgradeUtils.dropKeysIfExist(conn, "cloud_usage.usage_vm_instance", indexList, false); + } } diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java index 6f340501cf18..68ad8fa12edd 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java @@ -61,9 +61,6 @@ public long getLastJobSuccessDateMillis() { public void updateJobSuccess(Long jobId, long startMillis, long endMillis, long execTime, boolean success) { TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB); try { - txn.start(); - - UsageJobVO job = lockRow(jobId, Boolean.TRUE); UsageJobVO jobForUpdate = createForUpdate(); jobForUpdate.setStartMillis(startMillis); jobForUpdate.setEndMillis(endMillis); @@ -71,11 +68,8 @@ public void updateJobSuccess(Long jobId, long startMillis, long endMillis, long jobForUpdate.setStartDate(new Date(startMillis)); jobForUpdate.setEndDate(new Date(endMillis)); jobForUpdate.setSuccess(success); - update(job.getId(), jobForUpdate); - - txn.commit(); + update(jobId, jobForUpdate); } catch (Exception ex) { - txn.rollback(); logger.error("error updating job success date", ex); throw new CloudRuntimeException(ex.getMessage()); } finally { diff --git a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java index c5ca410fc530..7345eeb48539 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -36,7 +36,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; @Entity @@ -69,13 +68,6 @@ public class UserAccountVO implements UserAccount, InternalIdentity { @Column(name = "state") private String state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -203,24 +195,6 @@ public void setState(String state) { this.state = state; } - @Override - public String getApiKey() { - return apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - @Override - public String getSecretKey() { - return secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - @Override public Date getCreated() { return created; diff --git a/engine/schema/src/main/java/com/cloud/user/UserVO.java b/engine/schema/src/main/java/com/cloud/user/UserVO.java index 6e355e102e6c..1b89bc215cf7 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -33,7 +33,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import com.cloud.user.Account.State; -import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; import org.apache.commons.lang3.StringUtils; @@ -71,13 +70,6 @@ public class UserVO implements User, Identity, InternalIdentity { @Enumerated(value = EnumType.STRING) private State state; - @Column(name = "api_key") - private String apiKey = null; - - @Encrypt - @Column(name = "secret_key") - private String secretKey = null; - @Column(name = GenericDao.CREATED_COLUMN) private Date created; @@ -123,8 +115,8 @@ public UserVO() { } public UserVO(long id) { + this(); this.id = id; - this.uuid = UUID.randomUUID().toString(); } public UserVO(long accountId, String username, String password, String firstName, String lastName, String email, String timezone, String uuid, Source source) { @@ -150,8 +142,6 @@ public UserVO(UserVO user) { this.setTimezone(user.getTimezone()); this.setUuid(user.getUuid()); this.setSource(user.getSource()); - this.setApiKey(user.getApiKey()); - this.setSecretKey(user.getSecretKey()); this.setExternalEntity(user.getExternalEntity()); this.setRegistered(user.isRegistered()); this.setRegistrationToken(user.getRegistrationToken()); @@ -243,26 +233,6 @@ public void setState(State state) { this.state = state; } - @Override - public String getApiKey() { - return apiKey; - } - - @Override - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - @Override - public String getSecretKey() { - return secretKey; - } - - @Override - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - @Override public String getTimezone() { if (StringUtils.isEmpty(timezone)) { diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java index dae5f3a34677..67b70571cb4c 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java @@ -21,13 +21,11 @@ import com.cloud.user.Account; import com.cloud.user.AccountVO; -import com.cloud.user.User; import com.cloud.utils.Pair; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface AccountDao extends GenericDao { - Pair findUserAccountByApiKey(String apiKey); List findAccountsLike(String accountName); diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java index f5f95d5da1ff..48b29fac45eb 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java @@ -16,8 +16,6 @@ // under the License. package com.cloud.user.dao; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.util.Date; import java.util.List; @@ -27,10 +25,7 @@ import com.cloud.user.Account; import com.cloud.user.Account.State; import com.cloud.user.AccountVO; -import com.cloud.user.User; -import com.cloud.user.UserVO; import com.cloud.utils.Pair; -import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; @@ -38,13 +33,9 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Func; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; @Component public class AccountDaoImpl extends GenericDaoBase implements AccountDao { - private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, u.api_key_access, " - + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state, a.api_key_access " + "FROM `cloud`.`user` u, `cloud`.`account` a " - + "WHERE u.account_id = a.id AND u.api_key = ? and u.removed IS NULL"; protected final SearchBuilder AllFieldsSearch; protected final SearchBuilder AccountTypeSearch; @@ -132,51 +123,6 @@ public List findCleanupsForDisabledAccounts() { return listBy(sc); } - @Override - public Pair findUserAccountByApiKey(String apiKey) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - PreparedStatement pstmt = null; - Pair userAcctPair = null; - try { - String sql = FIND_USER_ACCOUNT_BY_API_KEY; - pstmt = txn.prepareAutoCloseStatement(sql); - pstmt.setString(1, apiKey); - ResultSet rs = pstmt.executeQuery(); - // TODO: make sure we don't have more than 1 result? ApiKey had better be unique - if (rs.next()) { - User u = new UserVO(rs.getLong(1)); - u.setUsername(rs.getString(2)); - u.setAccountId(rs.getLong(3)); - u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); - u.setState(State.getValueOf(rs.getString(5))); - boolean apiKeyAccess = rs.getBoolean(6); - if (rs.wasNull()) { - u.setApiKeyAccess(null); - } else { - u.setApiKeyAccess(apiKeyAccess); - } - - AccountVO a = new AccountVO(rs.getLong(7)); - a.setAccountName(rs.getString(8)); - a.setType(Account.Type.getFromValue(rs.getInt(9))); - a.setRoleId(rs.getLong(10)); - a.setDomainId(rs.getLong(11)); - a.setState(State.getValueOf(rs.getString(12))); - apiKeyAccess = rs.getBoolean(13); - if (rs.wasNull()) { - a.setApiKeyAccess(null); - } else { - a.setApiKeyAccess(apiKeyAccess); - } - - userAcctPair = new Pair(u, a); - } - } catch (Exception e) { - logger.warn("Exception finding user/acct by api key: " + apiKey, e); - } - return userAcctPair; - } - @Override public List findAccountsLike(String accountName) { return findAccountsLike(accountName, null).first(); @@ -341,11 +287,9 @@ public long getDomainIdForGivenAccountId(long id) { domain_id = account_vo.getDomainId(); } catch (Exception e) { - logger.warn("getDomainIdForGivenAccountId: Exception :" + e.getMessage()); - } - finally { - return domain_id; + logger.warn("Can not get DomainId for the given AccountId; exception message : {}", e.getMessage()); } + return domain_id; } @Override diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java index de3b769571e9..e377bbab94ed 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDao.java @@ -30,6 +30,4 @@ public interface UserAccountDao extends GenericDao { List getUserAccountByEmail(String email, Long domainId); boolean validateUsernameInDomain(String username, Long domainId); - - UserAccount getUserByApiKey(String apiKey); } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java index c9de9a367eed..28392abbff5c 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java @@ -19,7 +19,6 @@ import com.cloud.user.UserAccount; import com.cloud.user.UserAccountVO; import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import org.springframework.stereotype.Component; @@ -28,14 +27,6 @@ @Component public class UserAccountDaoImpl extends GenericDaoBase implements UserAccountDao { - protected final SearchBuilder userAccountSearch; - - public UserAccountDaoImpl() { - userAccountSearch = createSearchBuilder(); - userAccountSearch.and("apiKey", userAccountSearch.entity().getApiKey(), SearchCriteria.Op.EQ); - userAccountSearch.done(); - } - @Override public List getAllUsersByNameAndEntity(String username, String entity) { if (username == null) { @@ -79,12 +70,4 @@ public boolean validateUsernameInDomain(String username, Long domainId) { } return false; } - - @Override - public UserAccount getUserByApiKey(String apiKey) { - SearchCriteria sc = userAccountSearch.create(); - sc.setParameters("apiKey", apiKey); - return findOneBy(sc); - } - } diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java index 14b074251508..2e160efb9506 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDao.java @@ -37,13 +37,6 @@ public interface UserDao extends GenericDao { List listByAccount(long accountId); - /** - * Finds a user based on the secret key provided. - * @param secretKey - * @return - */ - UserVO findUserBySecretKey(String secretKey); - /** * Finds a user based on the registration token provided. * @param registrationToken diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java index 8baf732c2406..de60e48dff8f 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserDaoImpl.java @@ -65,10 +65,6 @@ protected UserDaoImpl() { UserIdSearch.and("id", UserIdSearch.entity().getId(), SearchCriteria.Op.EQ); UserIdSearch.done(); - SecretKeySearch = createSearchBuilder(); - SecretKeySearch.and("secretKey", SecretKeySearch.entity().getSecretKey(), SearchCriteria.Op.EQ); - SecretKeySearch.done(); - RegistrationTokenSearch = createSearchBuilder(); RegistrationTokenSearch.and("registrationToken", RegistrationTokenSearch.entity().getRegistrationToken(), SearchCriteria.Op.EQ); RegistrationTokenSearch.done(); @@ -121,13 +117,6 @@ public List findUsersLike(String username) { return listBy(sc); } - @Override - public UserVO findUserBySecretKey(String secretKey) { - SearchCriteria sc = SecretKeySearch.create(); - sc.setParameters("secretKey", secretKey); - return findOneBy(sc); - } - @Override public UserVO findUserByRegistrationToken(String registrationToken) { SearchCriteria sc = RegistrationTokenSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index 6c569e22dd95..65946b8d8210 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -131,6 +131,9 @@ protected NicVO() { @Column(name = "mtu") Integer mtu; + @Column(name = "enabled") + boolean enabled; + @Transient transient String nsxLogicalSwitchUuid; @@ -143,6 +146,7 @@ public NicVO(String reserver, Long instanceId, long configurationId, VirtualMach this.networkId = configurationId; this.state = State.Allocated; this.vmType = vmType; + this.enabled = true; } @Override @@ -397,6 +401,14 @@ public void setNsxLogicalSwitchPortUuid(String nsxLogicalSwitchPortUuid) { this.nsxLogicalSwitchPortUuid = nsxLogicalSwitchPortUuid; } + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + @Override public int hashCode() { return new HashCodeBuilder(17, 31).append(id).toHashCode(); diff --git a/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java b/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java new file mode 100644 index 000000000000..f4a3f1168188 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/VmIsoMapVO.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "vm_iso_map") +public class VmIsoMapVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "iso_id") + private long isoId; + + @Column(name = "device_seq") + private int deviceSeq; + + @Column(name = "created") + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + public VmIsoMapVO() { + } + + public VmIsoMapVO(long vmId, long isoId, int deviceSeq) { + this.vmId = vmId; + this.isoId = isoId; + this.deviceSeq = deviceSeq; + this.created = new Date(); + } + + @Override + public long getId() { + return id; + } + + public long getVmId() { + return vmId; + } + + public long getIsoId() { + return isoId; + } + + public int getDeviceSeq() { + return deviceSeq; + } + + public Date getCreated() { + return created; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index 96a1b6e3bd16..ad599053ff0c 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -26,6 +26,8 @@ public interface NicDao extends GenericDao { List listByVmId(long instanceId); + int countByVmId(long instanceId); + List listByVmIdOrderByDeviceId(long instanceId); List listIpAddressInNetwork(long networkConfigId); @@ -97,6 +99,8 @@ public interface NicDao extends GenericDao { NicVO findByMacAddress(String macAddress, long networkId); + List listByMacAddresses(List macAddresses); + NicVO findByNetworkIdAndMacAddressIncludingRemoved(long networkId, String mac); List findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index 78966a09e97c..e468b1b9a3bf 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -18,12 +18,13 @@ import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; -import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.db.Filter; @@ -71,7 +72,7 @@ protected void init() { AllFieldsSearch.and("strategy", AllFieldsSearch.entity().getReservationStrategy(), Op.EQ); AllFieldsSearch.and("strategyNEQ", AllFieldsSearch.entity().getReservationStrategy(), Op.NEQ); AllFieldsSearch.and("reserverName",AllFieldsSearch.entity().getReserver(),Op.EQ); - AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.EQ); + AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.IN); AllFieldsSearch.and("deviceid", AllFieldsSearch.entity().getDeviceId(), Op.EQ); AllFieldsSearch.and("ipv6Gateway", AllFieldsSearch.entity().getIPv6Gateway(), Op.EQ); AllFieldsSearch.and("ipv6Cidr", AllFieldsSearch.entity().getIPv6Cidr(), Op.EQ); @@ -126,6 +127,13 @@ public List listByVmId(long instanceId) { return listBy(sc); } + @Override + public int countByVmId(long instanceId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("instance", instanceId); + return getCount(sc); + } + @Override public List listByVmIdOrderByDeviceId(long instanceId) { SearchCriteria sc = AllFieldsSearch.create(); @@ -427,6 +435,16 @@ public NicVO findByMacAddress(String macAddress, long networkId) { return findOneBy(sc); } + @Override + public List listByMacAddresses(List macAddresses) { + if (CollectionUtils.isEmpty(macAddresses)) { + return Collections.emptyList(); + } + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("macAddress", macAddresses.toArray()); + return listBy(sc); + } + @Override public List findNicsByIpv6GatewayIpv6CidrAndReserver(String ipv6Gateway, String ipv6Cidr, String reserverName) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java index 2ca901fa3e55..403727ed9c61 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java @@ -16,10 +16,14 @@ // under the License. package com.cloud.vm.dao; +import java.util.Set; + import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; import com.cloud.utils.db.GenericDao; import com.cloud.vm.NicDetailVO; public interface NicDetailsDao extends GenericDao, ResourceDetailsDao { + + void removeDetailsForNicIds(String resourceName, Set nicIds); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java index 2a0b02aae1d9..1011f0588bbc 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java @@ -17,17 +17,43 @@ package com.cloud.vm.dao; -import org.springframework.stereotype.Component; +import java.util.Set; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.NicDetailVO; @Component public class NicDetailsDaoImpl extends ResourceDetailsDaoBase implements NicDetailsDao { + private final SearchBuilder ResourceIdNameSearch; + + public NicDetailsDaoImpl() { + super(); + ResourceIdNameSearch = createSearchBuilder(); + ResourceIdNameSearch.and(ApiConstants.NAME, ResourceIdNameSearch.entity().getName(), SearchCriteria.Op.EQ); + ResourceIdNameSearch.and(ApiConstants.RESOURCE_ID, ResourceIdNameSearch.entity().getResourceId(), SearchCriteria.Op.IN); + ResourceIdNameSearch.done(); + } + @Override public void addDetail(long resourceId, String key, String value, boolean display) { super.addDetail(new NicDetailVO(resourceId, key, value, display)); } + + @Override + public void removeDetailsForNicIds(String resourceName, Set nicIds) { + if (CollectionUtils.isEmpty(nicIds)) { + return; + } + SearchCriteria sc = ResourceIdNameSearch.create(); + sc.setParameters(ApiConstants.NAME, resourceName); + sc.setParameters(ApiConstants.RESOURCE_ID, nicIds.toArray()); + remove(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java index 4c8208b4be84..baa331d81f67 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java @@ -43,7 +43,7 @@ public NicSecondaryIpVO(long nicId, String ipaddr, long vmId, long accountId, lo this.networkId = networkId; } - public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long vmId, long accountId, long domainId, long networkId) { + public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long vmId, long accountId, long domainId, long networkId, String description) { this.nicId = nicId; this.vmId = vmId; this.ip4Address = ip4Address; @@ -51,6 +51,7 @@ public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long v this.accountId = accountId; this.domainId = domainId; this.networkId = networkId; + this.description = description; } protected NicSecondaryIpVO() { @@ -88,6 +89,18 @@ protected NicSecondaryIpVO() { @Column(name = "vmId") long vmId; + @Column(name = "description") + String description; + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + @Override public String toString() { return String.format("NicSecondaryIp %s", diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index 23541c2431e7..1a5b8cedd9ea 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import com.cloud.hypervisor.Hypervisor; import com.cloud.utils.Pair; @@ -192,4 +193,10 @@ List searchRemovedByRemoveDate(final Date startDate, final Date en int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List domainIds); List listByIdsIncludingRemoved(List ids); + + List listDeleteProtectedVmsByAccountId(long accountId); + + List listDeleteProtectedVmsByDomainIds(Set domainIds); + + List listIdsByHostIdForVolumeStats(long hostIds); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index b4ad7d2f42d1..ae1e838649ba 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -25,11 +25,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.api.ApiConstants; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; @@ -106,6 +108,8 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem protected SearchBuilder IdsPowerStateSelectSearch; GenericSearchBuilder CountByOfferingId; GenericSearchBuilder CountUserVmNotInDomain; + SearchBuilder DeleteProtectedVmSearchByAccount; + SearchBuilder DeleteProtectedVmSearchByDomainIds; @Inject ResourceTagDao tagsDao; @@ -354,7 +358,8 @@ protected void init() { IdsPowerStateSelectSearch.entity().getPowerHostId(), IdsPowerStateSelectSearch.entity().getPowerState(), IdsPowerStateSelectSearch.entity().getPowerStateUpdateCount(), - IdsPowerStateSelectSearch.entity().getPowerStateUpdateTime()); + IdsPowerStateSelectSearch.entity().getPowerStateUpdateTime(), + IdsPowerStateSelectSearch.entity().getState()); IdsPowerStateSelectSearch.done(); CountByOfferingId = createSearchBuilder(Integer.class); @@ -368,6 +373,19 @@ protected void init() { CountUserVmNotInDomain.and("domainIdsNotIn", CountUserVmNotInDomain.entity().getDomainId(), Op.NIN); CountUserVmNotInDomain.done(); + DeleteProtectedVmSearchByAccount = createSearchBuilder(); + DeleteProtectedVmSearchByAccount.selectFields(DeleteProtectedVmSearchByAccount.entity().getUuid()); + DeleteProtectedVmSearchByAccount.and(ApiConstants.ACCOUNT_ID, DeleteProtectedVmSearchByAccount.entity().getAccountId(), Op.EQ); + DeleteProtectedVmSearchByAccount.and(ApiConstants.DELETE_PROTECTION, DeleteProtectedVmSearchByAccount.entity().isDeleteProtection(), Op.EQ); + DeleteProtectedVmSearchByAccount.and(ApiConstants.REMOVED, DeleteProtectedVmSearchByAccount.entity().getRemoved(), Op.NULL); + DeleteProtectedVmSearchByAccount.done(); + + DeleteProtectedVmSearchByDomainIds = createSearchBuilder(); + DeleteProtectedVmSearchByDomainIds.selectFields(DeleteProtectedVmSearchByDomainIds.entity().getUuid()); + DeleteProtectedVmSearchByDomainIds.and(ApiConstants.DOMAIN_IDS, DeleteProtectedVmSearchByDomainIds.entity().getDomainId(), Op.IN); + DeleteProtectedVmSearchByDomainIds.and(ApiConstants.DELETE_PROTECTION, DeleteProtectedVmSearchByDomainIds.entity().isDeleteProtection(), Op.EQ); + DeleteProtectedVmSearchByDomainIds.and(ApiConstants.REMOVED, DeleteProtectedVmSearchByDomainIds.entity().getRemoved(), Op.NULL); + DeleteProtectedVmSearchByDomainIds.done(); } @Override @@ -846,15 +864,18 @@ public HashMap countVgpuVMs(Long dcId, Long podId, Long clusterId) try { pstmtLegacy = txn.prepareAutoCloseStatement(finalQueryLegacy.toString()); - pstmt = txn.prepareAutoCloseStatement(finalQuery.toString()); for (int i = 0; i < resourceIdList.size(); i++) { pstmtLegacy.setLong(1 + i, resourceIdList.get(i)); - pstmt.setLong(1 + i, resourceIdList.get(i)); } ResultSet rs = pstmtLegacy.executeQuery(); while (rs.next()) { result.put(rs.getString(1).concat(rs.getString(2)), rs.getLong(3)); } + + pstmt = txn.prepareAutoCloseStatement(finalQuery.toString()); + for (int i = 0; i < resourceIdList.size(); i++) { + pstmt.setLong(1 + i, resourceIdList.get(i)); + } rs = pstmt.executeQuery(); while (rs.next()) { result.put(rs.getString(1).concat(rs.getString(2)), rs.getLong(3)); @@ -1085,10 +1106,14 @@ public Map updatePowerState( private boolean isPowerStateInSyncWithInstanceState(final VirtualMachine.PowerState powerState, final long powerHostId, final VMInstanceVO instance) { State instanceState = instance.getState(); + if (instanceState == null) { + logger.warn("VM {} has null instance state during power state sync check, treating as out of sync", instance); + return false; + } if ((powerState == VirtualMachine.PowerState.PowerOff && instanceState == State.Running) || (powerState == VirtualMachine.PowerState.PowerOn && instanceState == State.Stopped)) { HostVO instanceHost = hostDao.findById(instance.getHostId()); - HostVO powerHost = powerHostId == instance.getHostId() ? instanceHost : hostDao.findById(powerHostId); + HostVO powerHost = instance.getHostId() != null && powerHostId == instance.getHostId() ? instanceHost : hostDao.findById(powerHostId); logger.debug("VM: {} on host: {} and power host : {} is in {} state, but power state is {}", instance, instanceHost, powerHost, instanceState, powerState); return false; @@ -1293,4 +1318,38 @@ public List listByIdsIncludingRemoved(List ids) { sc.setParameters("ids", ids.toArray()); return listIncludingRemovedBy(sc); } + + @Override + public List listDeleteProtectedVmsByAccountId(long accountId) { + SearchCriteria sc = DeleteProtectedVmSearchByAccount.create(); + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + sc.setParameters(ApiConstants.DELETE_PROTECTION, true); + Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L); + return listBy(sc, filter); + } + + @Override + public List listDeleteProtectedVmsByDomainIds(Set domainIds) { + SearchCriteria sc = DeleteProtectedVmSearchByDomainIds.create(); + sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds.toArray()); + sc.setParameters(ApiConstants.DELETE_PROTECTION, true); + Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L); + return listBy(sc, filter); + } + + @Override + public List listIdsByHostIdForVolumeStats(long hostId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getId()); + sb.and().op("host", sb.entity().getHostId(), SearchCriteria.Op.EQ); + sb.or().op("hostNull", sb.entity().getHostId(), Op.NULL); + sb.and("lastHost", sb.entity().getLastHostId(), SearchCriteria.Op.EQ); + sb.cp(); + sb.cp(); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("host", hostId); + sc.setParameters("lastHost", hostId); + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java new file mode 100644 index 000000000000..a472a3b4dece --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDao.java @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm.dao; + +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VmIsoMapVO; + +public interface VmIsoMapDao extends GenericDao { + List listByVmId(long vmId); + + List listByIsoId(long isoId); + + VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq); + + VmIsoMapVO findByVmIdIsoId(long vmId, long isoId); + + int removeByVmId(long vmId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java new file mode 100644 index 000000000000..44749eea75f1 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VmIsoMapDaoImpl.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.vm.dao; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VmIsoMapVO; + +@Component +public class VmIsoMapDaoImpl extends GenericDaoBase implements VmIsoMapDao { + + private SearchBuilder ListByVmId; + private SearchBuilder ListByIsoId; + private SearchBuilder ByVmIdDeviceSeq; + private SearchBuilder ByVmIdIsoId; + + protected VmIsoMapDaoImpl() { + ListByVmId = createSearchBuilder(); + ListByVmId.and("vmId", ListByVmId.entity().getVmId(), SearchCriteria.Op.EQ); + ListByVmId.done(); + + ListByIsoId = createSearchBuilder(); + ListByIsoId.and("isoId", ListByIsoId.entity().getIsoId(), SearchCriteria.Op.EQ); + ListByIsoId.done(); + + ByVmIdDeviceSeq = createSearchBuilder(); + ByVmIdDeviceSeq.and("vmId", ByVmIdDeviceSeq.entity().getVmId(), SearchCriteria.Op.EQ); + ByVmIdDeviceSeq.and("deviceSeq", ByVmIdDeviceSeq.entity().getDeviceSeq(), SearchCriteria.Op.EQ); + ByVmIdDeviceSeq.done(); + + ByVmIdIsoId = createSearchBuilder(); + ByVmIdIsoId.and("vmId", ByVmIdIsoId.entity().getVmId(), SearchCriteria.Op.EQ); + ByVmIdIsoId.and("isoId", ByVmIdIsoId.entity().getIsoId(), SearchCriteria.Op.EQ); + ByVmIdIsoId.done(); + } + + @Override + public List listByVmId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("vmId", vmId); + return listBy(sc); + } + + @Override + public List listByIsoId(long isoId) { + SearchCriteria sc = ListByIsoId.create(); + sc.setParameters("isoId", isoId); + return listBy(sc); + } + + @Override + public VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq) { + SearchCriteria sc = ByVmIdDeviceSeq.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("deviceSeq", deviceSeq); + return findOneBy(sc); + } + + @Override + public VmIsoMapVO findByVmIdIsoId(long vmId, long isoId) { + SearchCriteria sc = ByVmIdIsoId.create(); + sc.setParameters("vmId", vmId); + sc.setParameters("isoId", isoId); + return findOneBy(sc); + } + + @Override + public int removeByVmId(long vmId) { + SearchCriteria sc = ListByVmId.create(); + sc.setParameters("vmId", vmId); + return remove(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java new file mode 100644 index 000000000000..7972fe6bc624 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairPermissionVO.java @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl; + +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "api_keypair_permissions") +public class ApiKeyPairPermissionVO extends RolePermissionBaseVO implements ApiKeyPairPermission { + @Column(name = "api_keypair_id") + private long apiKeyPairId; + + @Column(name = "sort_order") + private long sortOrder = 0; + + public ApiKeyPairPermissionVO(long apiKeyPairId, String rule, Permission permission, String description) { + super(rule, permission, description); + this.apiKeyPairId = apiKeyPairId; + } + + public ApiKeyPairPermissionVO(String rule, Permission permission, String description) { + super(rule, permission, description); + } + + public ApiKeyPairPermissionVO() { + } + + public long getApiKeyPairId() { + return this.apiKeyPairId; + } + + public void setApiKeyPairId(long keyPairId) { + this.apiKeyPairId = keyPairId; + } + + public void setSortOrder(long sortOrder) { + this.sortOrder = sortOrder; + } + + public long getSortOrder() { + return sortOrder; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java new file mode 100644 index 000000000000..7a6bd7a3b140 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java @@ -0,0 +1,244 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.utils.db.Encrypt; +import java.time.Instant; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; +import org.joda.time.DateTime; + +@Entity +@Table(name = "api_keypair") +public class ApiKeyPairVO implements ApiKeyPair { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "domain_id", nullable = false) + private Long domainId; + + @Column(name = "account_id", nullable = false) + private Long accountId; + + @Column(name = "start_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startDate; + + @Column(name = "end_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate; + + @Column(name = "created", nullable = false) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = Date.from(Instant.now()); + + @Column(name = "description", length = 1024) + private String description = ""; + + @Column(name = "api_key", nullable = false) + private String apiKey; + + @Encrypt + @Column(name = "secret_key", nullable = false) + private String secretKey; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public ApiKeyPairVO() { + } + + public ApiKeyPairVO(Long id) { + this.id = id; + } + + public ApiKeyPairVO(Long userId, String description, Date startDate, Date endDate, + String apiKey, String secretKey) { + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.apiKey = apiKey; + this.secretKey = secretKey; + } + + public ApiKeyPairVO(String name, Long userId, String description, Date startDate, Date endDate, Account account) { + this.name = Objects.requireNonNullElseGet(name, () -> userId + " - API key pair"); + this.userId = userId; + this.description = description; + this.startDate = startDate; + this.endDate = endDate; + this.domainId = account.getDomainId(); + this.accountId = account.getAccountId(); + } + + public ApiKeyPairVO(Long id, Long userId) { + this.id = id; + this.userId = userId; + } + + public void validateDate() { + Date now = DateTime.now().toDate(); + Date keypairStart = this.getStartDate(); + Date keypairExpiration = this.getEndDate(); + if (keypairStart != null && now.compareTo(keypairStart) <= 0) { + throw new InvalidParameterValueException(String.format("API key pair is not valid yet, start date: %s", keypairStart)); + } + if (keypairExpiration != null && now.compareTo(keypairExpiration) >= 0) { + throw new InvalidParameterValueException(String.format("API key pair is expired, expiration date: %s", keypairExpiration)); + } + } + + public boolean hasEndDatePassed() { + Date now = DateTime.now().toDate(); + Date keypairExpiration = this.getEndDate(); + return keypairExpiration != null && now.compareTo(keypairExpiration) >= 0; + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public Long getUserId() { + return userId; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Date getCreated() { + return created; + } + + public String getDescription() { + return description; + } + + public String getApiKey() { + return apiKey; + } + + public String getSecretKey() { + return secretKey; + } + + public Class getEntityType() { + return ApiKeyPair.class; + } + + public String getName() { + return name; + } + + public Date getRemoved() { + return removed; + } + + @Override + public long getDomainId() { + return this.domainId; + } + + @Override + public long getAccountId() { + return this.accountId; + } + + public void setId(Long id) { this.id = id; } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public void setName(String name) { + this.name = name; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java index f3347ab66305..588fa0299649 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/RolePermissionBaseVO.java @@ -50,6 +50,12 @@ public class RolePermissionBaseVO implements RolePermissionEntity { public RolePermissionBaseVO() { this.uuid = UUID.randomUUID().toString(); } + public RolePermissionBaseVO(final String rule, final Permission permission) { + this(); + this.rule = rule; + this.permission = permission; + } + public RolePermissionBaseVO(final String rule, final Permission permission, final String description) { this(); this.rule = rule; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java new file mode 100644 index 000000000000..006c2afbc96e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDao.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; + +public interface ApiKeyPairDao extends GenericDao { + ApiKeyPairVO findBySecretKey(String secretKey); + + ApiKeyPairVO findByApiKey(String apiKey); + + ApiKeyPairVO findByUuid(String uuid); + + Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId); + + ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId); + + Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd); + +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java new file mode 100644 index 000000000000..a4895efed9e0 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairDaoImpl.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.List; +import org.apache.cloudstack.acl.ApiKeyPairVO; +import org.apache.cloudstack.api.command.admin.user.ListUserKeysCmd; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +@Component +public class ApiKeyPairDaoImpl extends GenericDaoBase implements ApiKeyPairDao { + private static final String ID = "id"; + private static final String USER_ID = "userId"; + private static final String API_KEY = "apiKey"; + private static final String SECRET_KEY = "secretKey"; + + private final SearchBuilder keyPairSearch; + + ApiKeyPairDaoImpl() { + super(); + + keyPairSearch = createSearchBuilder(); + keyPairSearch.and(API_KEY, keyPairSearch.entity().getApiKey(), SearchCriteria.Op.EQ); + keyPairSearch.and(SECRET_KEY, keyPairSearch.entity().getSecretKey(), SearchCriteria.Op.EQ); + keyPairSearch.and(ID, keyPairSearch.entity().getId(), SearchCriteria.Op.EQ); + keyPairSearch.and(USER_ID, keyPairSearch.entity().getUserId(), SearchCriteria.Op.IN); + keyPairSearch.done(); + } + + @Override + public ApiKeyPairVO findByApiKey(String apiKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters(API_KEY, apiKey); + return findOneBy(sc); + } + + public ApiKeyPairVO findBySecretKey(String secretKey) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParameters(SECRET_KEY, secretKey); + return findOneBy(sc); + } + + public Pair, Integer> listApiKeysByUserOrApiKeyId(Long userId, Long apiKeyId) { + SearchCriteria sc = keyPairSearch.create(); + sc.setParametersIfNotNull(USER_ID, userId); + sc.setParametersIfNotNull(ID, apiKeyId); + final Filter searchFilter = new Filter(100); + return searchAndCount(sc, searchFilter); + } + + public ApiKeyPairVO getLastApiKeyCreatedByUser(Long userId) { + final SearchCriteria sc = keyPairSearch.create(); + sc.setParametersIfNotNull(USER_ID, userId); + final Filter searchBySorted = new Filter(ApiKeyPairVO.class, ID, false, null, null); + return findOneBy(sc, searchBySorted); + } + + public Pair, Integer> listByUserIdsPaginated(List userIds, ListUserKeysCmd cmd) { + Long pageSizeVal = cmd.getPageSizeVal(); + Long startIndex = cmd.getStartIndex(); + Filter searchFilter = new Filter(ApiKeyPairVO.class, ID, true, startIndex, pageSizeVal); + + final SearchCriteria sc = keyPairSearch.create(); + sc.setParameters(USER_ID, (Object[]) userIds.toArray(new Long[0])); + + Pair, Integer> apiKeyPairVOList = searchAndCount(sc, searchFilter); + if (CollectionUtils.isEmpty(apiKeyPairVOList.first())) { + return new Pair<>(List.of(), 0); + } + return apiKeyPairVOList; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java new file mode 100644 index 000000000000..cbca2fd72747 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDao.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; + +import java.util.List; + +public interface ApiKeyPairPermissionsDao extends GenericDao { + List findAllByApiKeyPairId(Long apiKeyPairId); + + List findAllByKeyPairIdSorted(Long apiKeyPairId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java new file mode 100644 index 000000000000..4510e0ae2896 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/dao/ApiKeyPairPermissionsDaoImpl.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.acl.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import java.util.Collections; +import java.util.Objects; +import org.apache.commons.collections.CollectionUtils; +import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ApiKeyPairPermissionsDaoImpl extends GenericDaoBase implements ApiKeyPairPermissionsDao { + private static final String API_KEY_PAIR_ID = "apiKeyPairId"; + private static final String SORT_ORDER = "sortOrder"; + + private final SearchBuilder permissionByApiKeyPairIdSearch; + + public ApiKeyPairPermissionsDaoImpl() { + super(); + + permissionByApiKeyPairIdSearch = createSearchBuilder(); + permissionByApiKeyPairIdSearch.and(API_KEY_PAIR_ID, permissionByApiKeyPairIdSearch.entity().getApiKeyPairId(), SearchCriteria.Op.EQ); + permissionByApiKeyPairIdSearch.done(); + } + + public List findAllByApiKeyPairId(Long apiKeyPairId) { + SearchCriteria sc = permissionByApiKeyPairIdSearch.create(); + sc.setParameters(API_KEY_PAIR_ID, String.valueOf(apiKeyPairId)); + return listBy(sc); + } + + @Override + public ApiKeyPairPermissionVO persist(final ApiKeyPairPermissionVO item) { + item.setSortOrder(0); + final List permissionsList = findAllByKeyPairIdSorted(item.getApiKeyPairId()); + if (!CollectionUtils.isEmpty(permissionsList)) { + ApiKeyPairPermissionVO lastPermission = permissionsList.get(permissionsList.size() - 1); + item.setSortOrder(lastPermission.getSortOrder() + 1); + } + return super.persist(item); + } + + @Override + public List findAllByKeyPairIdSorted(Long apiKeyPairId) { + final SearchCriteria sc = permissionByApiKeyPairIdSearch.create(); + sc.setParameters(API_KEY_PAIR_ID, apiKeyPairId); + final Filter searchBySorted = new Filter(ApiKeyPairPermissionVO.class, SORT_ORDER, true, null, null); + final List apiKeyPairPermissionList = listBy(sc, searchBySorted); + return Objects.requireNonNullElse(apiKeyPairPermissionList, Collections.emptyList()); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java index ebeb7d4a2d59..9156f8f5ff84 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java @@ -83,6 +83,11 @@ public BackupOfferingVO(final long zoneId, final String externalId, final String this.created = new Date(); } + public BackupOfferingVO(final long zoneId, final String provider, final String name, final String description, final boolean userDrivenBackupAllowed) { + this(zoneId, null, provider, name, description, userDrivenBackupAllowed); + this.externalId = this.uuid; + } + public String getUuid() { return uuid; } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java index 1ee2cff78b65..aa02bb077163 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java @@ -74,10 +74,14 @@ public class BackupScheduleVO implements BackupSchedule { @Column(name = "domain_id") Long domainId; + @Column(name = "isolated") + private boolean isolated; + public BackupScheduleVO() { } - public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM, Long accountId, Long domainId) { + public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp, int maxBackups, Boolean quiesceVM, + Long accountId, Long domainId, boolean isolated) { this.vmId = vmId; this.scheduleType = (short) scheduleType.ordinal(); this.schedule = schedule; @@ -87,6 +91,7 @@ public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String sc this.quiesceVM = quiesceVM; this.accountId = accountId; this.domainId = domainId; + this.isolated = isolated; } @Override @@ -197,4 +202,13 @@ public void setAccountId(Long accountId) { public void setDomainId(Long domainId) { this.domainId = domainId; } + + @Override + public boolean isIsolated() { + return isolated; + } + + public void setIsolated(boolean isolated) { + this.isolated = isolated; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java index 0f8a10fb7be6..b4cad92d8770 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@ -19,7 +19,6 @@ import com.cloud.utils.db.GenericDao; import com.google.gson.Gson; - import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.lang3.StringUtils; @@ -66,7 +65,7 @@ public class BackupVO implements Backup { private String externalId; @Column(name = "type") - private String backupType; + private String type; @Column(name = "date") @Temporal(value = TemporalType.DATE) @@ -81,6 +80,9 @@ public class BackupVO implements Backup { @Column(name = "protected_size") private Long protectedSize; + @Column(name = "uncompressed_size") + private Long uncompressedSize; + @Enumerated(value = EnumType.STRING) @Column(name = "status") private Backup.Status status; @@ -103,6 +105,24 @@ public class BackupVO implements Backup { @Column(name = "backup_schedule_id") private Long backupScheduleId; + @Column(name = "compression_status") + private CompressionStatus compressionStatus; + + @Column(name = "validation_status") + private ValidationStatus validationStatus; + + @Column(name = "from_checkpoint_id") + private String fromCheckpointId; + + @Column(name = "to_checkpoint_id") + private String toCheckpointId; + + @Column(name = "checkpoint_create_time") + private Long checkpointCreateTime; + + @Column(name = "host_id") + private Long hostId; + @Transient Map details; @@ -110,10 +130,27 @@ public BackupVO() { this.uuid = UUID.randomUUID().toString(); } + public BackupVO(String name, long vmId, long backupOfferingId, long accountId, long domainId, long zoneId, long virtualSize, + Status status, Long backupScheduleId, CompressionStatus compressionStatus, ValidationStatus validationStatus) { + this.name = name; + this.vmId = vmId; + this.backupOfferingId = backupOfferingId; + this.accountId = accountId; + this.domainId = domainId; + this.zoneId = zoneId; + this.protectedSize = virtualSize; + this.status = status; + this.setType("FULL"); + this.uuid = UUID.randomUUID().toString(); + this.backupScheduleId = backupScheduleId; + this.compressionStatus = compressionStatus; + this.validationStatus = validationStatus; + } + @Override public String toString() { return String.format("Backup %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( - this, "id", "uuid", "vmId", "backupType", "externalId")); + this, "id", "uuid", "vmId", "type", "externalId")); } @Override @@ -144,12 +181,13 @@ public void setExternalId(String externalId) { this.externalId = externalId; } + @Override public String getType() { - return backupType; + return type; } public void setType(String type) { - this.backupType = type; + this.type = type; } @Override @@ -288,4 +326,66 @@ public Long getBackupScheduleId() { public void setBackupScheduleId(Long backupScheduleId) { this.backupScheduleId = backupScheduleId; } + + @Override + public CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public void setCompressionStatus(CompressionStatus compressionStatus) { + this.compressionStatus = compressionStatus; + } + + @Override + public ValidationStatus getValidationStatus() { + return validationStatus; + } + + public void setValidationStatus(ValidationStatus validationStatus) { + this.validationStatus = validationStatus; + } + + public Long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(Long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + + @Override + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public void setFromCheckpointId(String fromCheckpointId) { + this.fromCheckpointId = fromCheckpointId; + } + + @Override + public String getToCheckpointId() { + return toCheckpointId; + } + + public void setToCheckpointId(String toCheckpointId) { + this.toCheckpointId = toCheckpointId; + } + + @Override + public Long getCheckpointCreateTime() { + return checkpointCreateTime; + } + + public void setCheckpointCreateTime(Long checkpointCreateTime) { + this.checkpointCreateTime = checkpointCreateTime; + } + + @Override + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java new file mode 100644 index 000000000000..ec9b927b63e4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java @@ -0,0 +1,242 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "image_transfer") +public class ImageTransferVO implements ImageTransfer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "backup_id") + private Long backupId; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "host_id") + private long hostId; + + @Column(name = "socket") + private String socket; + + @Column(name = "file") + private String file; + + @Column(name = "transfer_url") + private String transferUrl; + + @Enumerated(value = EnumType.STRING) + @Column(name = "phase") + private Phase phase; + + @Enumerated(value = EnumType.STRING) + @Column(name = "direction") + private Direction direction; + + @Enumerated(value = EnumType.STRING) + @Column(name = "backend") + private Backend backend; + + @Column(name = "signed_ticket_id") + private String signedTicketId; + + @Column(name = "account_id") + Long accountId; + + @Column(name = "domain_id") + Long domainId; + + @Column(name = "data_center_id") + Long dataCenterId; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public ImageTransferVO() { + } + + private ImageTransferVO(String uuid, long volumeId, long hostId, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this.uuid = uuid; + this.volumeId = volumeId; + this.hostId = hostId; + this.phase = phase; + this.direction = direction; + this.accountId = accountId; + this.domainId = domainId; + this.dataCenterId = dataCenterId; + this.created = new Date(); + } + + public ImageTransferVO(String uuid, Long backupId, long volumeId, long hostId, String socket, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this(uuid, volumeId, hostId, phase, direction, accountId, domainId, dataCenterId); + this.backupId = backupId; + this.socket = socket; + this.backend = Backend.nbd; + } + + public ImageTransferVO(String uuid, long volumeId, long hostId, String file, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this(uuid, volumeId, hostId, phase, direction, accountId, domainId, dataCenterId); + this.file = file; + this.backend = Backend.file; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public Long getBackupId() { + return backupId; + } + + public void setBackupId(long backupId) { + this.backupId = backupId; + } + + @Override + public long getVolumeId() { + return volumeId; + } + + public void setVolumeId(long volumeId) { + this.volumeId = volumeId; + } + + @Override + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + public void setSocket(String socket) { + this.socket = socket; + } + + @Override + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + + @Override + public Phase getPhase() { + return phase; + } + + public void setPhase(Phase phase) { + this.phase = phase; + this.updated = new Date(); + } + + @Override + public Direction getDirection() { + return direction; + } + + public void setDirection(Direction direction) { + this.direction = direction; + } + + @Override + public Backend getBackend() { + return backend; + } + + @Override + public String getSignedTicketId() { + return signedTicketId; + } + + public void setSignedTicketId(String signedTicketId) { + this.signedTicketId = signedTicketId; + } + + @Override + public Class getEntityType() { + return ImageTransfer.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public long getDataCenterId() { + return dataCenterId; + } + + public Date getCreated() { + return created; + } + + public Date getUpdated() { + return updated; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java new file mode 100644 index 000000000000..5266a4129905 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupDataStoreVO.java @@ -0,0 +1,95 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.Column; +import javax.persistence.Entity; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + + +@Entity +@Table(name = "internal_backup_store_ref") +public class InternalBackupDataStoreVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "volume_id") + private long volumeId; + + @Column (name = "device_id") + private long deviceId; + + @Column(name = "path") + private String backupPath; + + public InternalBackupDataStoreVO() { + } + + public InternalBackupDataStoreVO(long backupId, long volumeId, long deviceId, String backupPath) { + this.backupId = backupId; + this.volumeId = volumeId; + this.deviceId = deviceId; + this.backupPath = backupPath; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getVolumeId() { + return volumeId; + } + + public long getDeviceId() { + return deviceId; + } + + public String getBackupPath() { + return backupPath; + } + + public void setVolumeId(long volumeId) { + this.volumeId = volumeId; + } + + public void setBackupPath(String backupPath) { + this.backupPath = backupPath; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java new file mode 100644 index 000000000000..acc54ccd41fa --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -0,0 +1,190 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.backup; + +import com.google.gson.Gson; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.commons.lang3.StringUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@Entity +@Table(name = "internal_backup_view") +public class InternalBackupJoinVO { + + @Id + @Column(name="id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "backed_volumes", length = 65535) + private String backedUpVolumes; + + @Column(name = "backup_offering_id") + private long backupOfferingId; + + @Column(name = "image_store_id") + private long imageStoreId; + + @Column(name = "parent_id") + private long parentId; + + @Column(name = "type") + private String type; + + @Column(name = "date") + @Temporal(value = TemporalType.DATE) + private Date date; + + @Enumerated(value = EnumType.STRING) + @Column(name = "status") + private Backup.Status status; + + @Enumerated(value = EnumType.STRING) + @Column(name = "compression_status") + private Backup.CompressionStatus compressionStatus; + + @Column(name = "end_of_chain") + private Boolean endOfChain; + + @Column(name = "current") + private Boolean current; + + @Column(name = "image_store_path") + private String imageStorePath; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "size") + private long size; + + @Column(name = "protected_size") + private long protectedSize; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "isolated") + private Boolean isolated; + + public InternalBackupJoinVO() { + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public long getVmId() { + return vmId; + } + + public List getBackedUpVolumes() { + if (StringUtils.isEmpty(this.backedUpVolumes)) { + return Collections.emptyList(); + } + return Arrays.asList(new Gson().fromJson(this.backedUpVolumes, Backup.VolumeInfo[].class)); + } + + public long getBackupOfferingId() { + return backupOfferingId; + } + + public long getImageStoreId() { + return imageStoreId; + } + + public long getParentId() { + return parentId; + } + + public String getType() { + return type; + } + + public Date getDate() { + return date; + } + + public Backup.Status getStatus() { + return status; + } + + public Boolean getEndOfChain() { + return BooleanUtils.isTrue(endOfChain); + } + + public Boolean getCurrent() { + return BooleanUtils.isTrue(current); + } + + public String getImageStorePath() { + return imageStorePath; + } + + public long getZoneId() { + return zoneId; + } + + public long getSize() { + return size; + } + + public long getProtectedSize() { + return protectedSize; + } + + public long getVolumeId() { + return volumeId; + } + + public Boolean getIsolated() { + return BooleanUtils.isTrue(isolated); + } + + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java new file mode 100644 index 000000000000..287b64c2e6fc --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobType.java @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +public enum InternalBackupServiceJobType { + StartCompression, FinalizeCompression, BackupValidation +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java new file mode 100644 index 000000000000..87b4a53cb615 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceJobVO.java @@ -0,0 +1,179 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; + +@Entity +@Table(name = "internal_backup_service_job") +public class InternalBackupServiceJobVO implements InternalIdentity, Comparable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "instance_id") + private long instanceId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "host_id") + private Long hostId; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "attempts") + private int attempts; + + @Column (name = "type") + private InternalBackupServiceJobType type; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = "scheduled_start_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date scheduledStartTime; + + @Column(name = "start_time") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startTime; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public InternalBackupServiceJobVO() { + } + + public InternalBackupServiceJobVO(long backupId, long zoneId, long instanceId, long accountId, InternalBackupServiceJobType type) { + this.created = new Date(); + this.backupId = backupId; + this.zoneId = zoneId; + this.instanceId = instanceId; + this.accountId = accountId; + this.type = type; + this.scheduledStartTime = this.created; + } + + public InternalBackupServiceJobVO(long backupId, long zoneId, long instanceId, long accountId, InternalBackupServiceJobType type, Date scheduledStartTime) { + this(backupId, zoneId, instanceId, accountId, type); + this.scheduledStartTime = scheduledStartTime; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getInstanceId() { + return instanceId; + } + + public long getAccountId() { + return accountId; + } + + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public int getAttempts() { + return attempts; + } + + public void setAttempts(int attempts) { + this.attempts = attempts; + } + + public InternalBackupServiceJobType getType() { + return type; + } + + public Date getCreated() { + return created; + } + + public Date getScheduledStartTime() { + return scheduledStartTime; + } + + public void setScheduledStartTime(Date scheduledStartTime) { + this.scheduledStartTime = scheduledStartTime; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public int compareTo(InternalBackupServiceJobVO that) { + return this.created.compareTo(that.created); + } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "backupId", "zoneId", "hostId", "created", "scheduledStartTime", "startTime", "attempts", + "type"); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java new file mode 100644 index 000000000000..f6d4cd10ffb7 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupStoragePoolVO.java @@ -0,0 +1,101 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "internal_backup_pool_ref") +public class InternalBackupStoragePoolVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_id") + private long backupId; + + @Column(name = "storage_pool_id") + private long storagePoolId; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "backup_delta_path") + private String backupDeltaPath; + + @Column(name = "backup_parent_path") + private String backupDeltaParentPath; + + public InternalBackupStoragePoolVO() { + } + + public InternalBackupStoragePoolVO(long backupId, long storagePoolId, long volumeId, String backupDeltaPath, String backupDeltaParentPath) { + this.backupId = backupId; + this.storagePoolId = storagePoolId; + this.volumeId = volumeId; + this.backupDeltaPath = backupDeltaPath; + this.backupDeltaParentPath = backupDeltaParentPath; + } + + @Override + public long getId() { + return id; + } + + public long getBackupId() { + return backupId; + } + + public long getStoragePoolId() { + return storagePoolId; + } + + public long getVolumeId() { + return volumeId; + } + + public String getBackupDeltaPath() { + return backupDeltaPath; + } + + public String getBackupDeltaParentPath() { + return backupDeltaParentPath; + } + + public void setBackupDeltaPath(String backupDeltaPath) { + this.backupDeltaPath = backupDeltaPath; + } + + public void setBackupDeltaParentPath(String backupDeltaParentPath) { + this.backupDeltaParentPath = backupDeltaParentPath; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java index e60e49e1a0c2..3829777d9d8f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java @@ -42,4 +42,5 @@ public interface BackupDao extends GenericDao { void loadDetails(BackupVO backup); void saveDetails(BackupVO backup); List listBySchedule(Long backupScheduleId); + BackupVO findLatestByStatusAndVmId(Backup.Status status, long vmId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java index fd29da72c718..9859f29701b7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java @@ -90,6 +90,7 @@ protected void init() { backupSearch.and("external_id", backupSearch.entity().getExternalId(), SearchCriteria.Op.EQ); backupSearch.and("backup_offering_id", backupSearch.entity().getBackupOfferingId(), SearchCriteria.Op.EQ); backupSearch.and("zone_id", backupSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + backupSearch.and("status", backupSearch.entity().getStatus(), SearchCriteria.Op.IN); backupSearch.done(); backupVmSearchInZone = createSearchBuilder(Long.class); @@ -280,4 +281,13 @@ public List listVmIdsWithBackupsInZone(Long zoneId) { sc.setParameters("zone_id", zoneId); return customSearchIncludingRemoved(sc, null); } + + @Override + public BackupVO findLatestByStatusAndVmId(Backup.Status status, long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + sc.setParameters("status", status); + Filter filter = new Filter(BackupVO.class, "date", false, 0L, 1L); + return findOneBy(sc, filter); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java index 664650074bce..c6c8fc3a322e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDao.java @@ -23,4 +23,19 @@ public interface BackupDetailsDao extends GenericDao, ResourceDetailsDao { + String END_OF_CHAIN = "end_of_chain"; + + String CURRENT = "current"; + + String IMAGE_STORE_ID = "image_store_id"; + + String PARENT_ID = "parent_id"; + + String ISOLATED = "isolated"; + + String SCREENSHOT_PATH = "screenshot_path"; + + String BACKUP_HASH = "backup_hash"; + + void removeDetailsExcept(long backupId, String exception); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java index 08c7192af909..5f257c23892c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDetailsDaoImpl.java @@ -17,13 +17,39 @@ package org.apache.cloudstack.backup.dao; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import org.apache.cloudstack.backup.BackupDetailVO; import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; + @Component public class BackupDetailsDaoImpl extends ResourceDetailsDaoBase implements BackupDetailsDao { + private SearchBuilder backupDetailSearch; + + private static final String BACKUP_ID = "backup_id"; + + private static final String KEY = "key"; + + @PostConstruct + protected void init() { + backupDetailSearch = createSearchBuilder(); + backupDetailSearch.and(BACKUP_ID, backupDetailSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + backupDetailSearch.and(KEY, backupDetailSearch.entity().getName(), SearchCriteria.Op.NEQ); + backupDetailSearch.done(); + } + + @Override + public void removeDetailsExcept(long backupId, String exception) { + SearchCriteria sc = backupDetailSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + sc.setParameters(KEY, exception); + super.expunge(sc); + } + @Override public void addDetail(long resourceId, String key, String value, boolean display) { super.addDetail(new BackupDetailVO(resourceId, key, value, display)); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java index 708faeef4643..1d7a55d7caf7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java @@ -22,8 +22,10 @@ import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupOfferingDetailsVO; import org.apache.cloudstack.backup.BackupOfferingVO; import com.cloud.dc.DataCenterVO; @@ -32,7 +34,9 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class BackupOfferingDaoImpl extends GenericDaoBase implements BackupOfferingDao { @@ -62,6 +66,7 @@ public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, DataCenterVO zone = dataCenterDao.findById(offering.getZoneId()); List domainIds = backupOfferingDetailsDao.findDomainIds(offering.getId()); + List details = backupOfferingDetailsDao.listDetails(offering.getId()); BackupOfferingResponse response = new BackupOfferingResponse(); response.setId(offering.getUuid()); response.setName(offering.getName()); @@ -88,6 +93,14 @@ public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, if (crossZoneInstanceCreation) { response.setCrossZoneInstanceCreation(true); } + details.removeIf(backupOfferingDetailsVO -> ApiConstants.DOMAIN_ID.equals(backupOfferingDetailsVO.getName())); + Map detailString = new HashMap<>(); + for (BackupOfferingDetailsVO detail : details) { + detailString.put(detail.getName(), detail.getValue()); + } + if (!detailString.isEmpty()) { + response.setDetails(detailString); + } response.setCreated(offering.getCreated()); response.setObjectName("backupoffering"); return response; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java index ee1783a9c896..87b7dab1ff7b 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java @@ -21,20 +21,14 @@ import java.util.List; import com.cloud.utils.DateUtil; -import org.apache.cloudstack.api.response.BackupScheduleResponse; -import org.apache.cloudstack.backup.BackupSchedule; import org.apache.cloudstack.backup.BackupScheduleVO; import com.cloud.utils.db.GenericDao; public interface BackupScheduleDao extends GenericDao { - BackupScheduleVO findByVM(Long vmId); - List listByVM(Long vmId); BackupScheduleVO findByVMAndIntervalType(Long vmId, DateUtil.IntervalType intervalType); List getSchedulesToExecute(Date currentTimestamp); - - BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java index d9cf7b63680b..972af73391af 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java @@ -17,28 +17,23 @@ package org.apache.cloudstack.backup.dao; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.Date; import java.util.List; import javax.annotation.PostConstruct; -import javax.inject.Inject; import com.cloud.utils.DateUtil; -import org.apache.cloudstack.api.response.BackupScheduleResponse; -import org.apache.cloudstack.backup.BackupSchedule; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.TransactionLegacy; import org.apache.cloudstack.backup.BackupScheduleVO; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.dao.VMInstanceDao; public class BackupScheduleDaoImpl extends GenericDaoBase implements BackupScheduleDao { - - @Inject - VMInstanceDao vmInstanceDao; - private SearchBuilder backupScheduleSearch; private SearchBuilder executableSchedulesSearch; @@ -59,13 +54,6 @@ protected void init() { executableSchedulesSearch.done(); } - @Override - public BackupScheduleVO findByVM(Long vmId) { - SearchCriteria sc = backupScheduleSearch.create(); - sc.setParameters("vm_id", vmId); - return findOneBy(sc); - } - @Override public List listByVM(Long vmId) { SearchCriteria sc = backupScheduleSearch.create(); @@ -88,21 +76,19 @@ public List getSchedulesToExecute(Date currentTimestamp) { return listBy(sc); } + @DB @Override - public BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule) { - VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(schedule.getVmId()); - BackupScheduleResponse response = new BackupScheduleResponse(); - response.setId(schedule.getUuid()); - response.setVmId(vm.getUuid()); - response.setVmName(vm.getHostName()); - response.setIntervalType(schedule.getScheduleType()); - response.setSchedule(schedule.getSchedule()); - response.setTimezone(schedule.getTimezone()); - response.setMaxBackups(schedule.getMaxBackups()); - if (schedule.getQuiesceVM() != null) { - response.setQuiesceVM(schedule.getQuiesceVM()); + public boolean remove(Long id) { + String sql = "UPDATE backups SET backup_schedule_id = NULL WHERE backup_schedule_id = ?"; + TransactionLegacy transaction = TransactionLegacy.currentTxn(); + try { + PreparedStatement preparedStatement = transaction.prepareAutoCloseStatement(sql); + preparedStatement.setLong(1, id); + preparedStatement.executeUpdate(); + return super.remove(id); + } catch (SQLException e) { + logger.warn("Unable to clean up backup schedules references from the backups table.", e); + return false; } - response.setObjectName("backupschedule"); - return response; } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java new file mode 100644 index 000000000000..9a9fbeb8814e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java @@ -0,0 +1,36 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup.dao; + +import java.util.List; + +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.ImageTransferVO; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +public interface ImageTransferDao extends GenericDao { + List listByBackupId(Long backupId); + ImageTransferVO findByUuid(String uuid); + ImageTransferVO findByVolume(Long volumeId); + ImageTransferVO findUnfinishedByVolume(Long volumeId); + List listByPhaseAndDirection(ImageTransfer.Phase phase, ImageTransfer.Direction direction); + List listByZonesAndOwners(List zoneIds, List accountIds, List domainIds, + Filter filter); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java new file mode 100644 index 000000000000..2e36f924e4ef --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java @@ -0,0 +1,136 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package org.apache.cloudstack.backup.dao; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.ImageTransferVO; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class ImageTransferDaoImpl extends GenericDaoBase implements ImageTransferDao { + + private SearchBuilder backupIdSearch; + private SearchBuilder uuidSearch; + private SearchBuilder volumeSearch; + private SearchBuilder volumeUnfinishedSearch; + private SearchBuilder phaseDirectionSearch; + + public ImageTransferDaoImpl() { + } + + @PostConstruct + protected void init() { + backupIdSearch = createSearchBuilder(); + backupIdSearch.and("backupId", backupIdSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupIdSearch.done(); + + uuidSearch = createSearchBuilder(); + uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ); + uuidSearch.done(); + + volumeSearch = createSearchBuilder(); + volumeSearch.and("volumeId", volumeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + volumeSearch.done(); + + volumeUnfinishedSearch = createSearchBuilder(); + volumeUnfinishedSearch.and("volumeId", volumeUnfinishedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + volumeUnfinishedSearch.and("phase", volumeUnfinishedSearch.entity().getPhase(), SearchCriteria.Op.NEQ); + volumeUnfinishedSearch.done(); + + phaseDirectionSearch = createSearchBuilder(); + phaseDirectionSearch.and("phase", phaseDirectionSearch.entity().getPhase(), SearchCriteria.Op.EQ); + phaseDirectionSearch.and("direction", phaseDirectionSearch.entity().getDirection(), SearchCriteria.Op.EQ); + phaseDirectionSearch.done(); + } + + @Override + public List listByBackupId(Long backupId) { + SearchCriteria sc = backupIdSearch.create(); + sc.setParameters("backupId", backupId); + return listBy(sc); + } + + @Override + public ImageTransferVO findByUuid(String uuid) { + SearchCriteria sc = uuidSearch.create(); + sc.setParameters("uuid", uuid); + return findOneBy(sc); + } + + @Override + public ImageTransferVO findByVolume(Long volumeId) { + SearchCriteria sc = volumeSearch.create(); + sc.setParameters("volumeId", volumeId); + return findOneBy(sc); + } + + @Override + public ImageTransferVO findUnfinishedByVolume(Long volumeId) { + SearchCriteria sc = volumeUnfinishedSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("phase", ImageTransferVO.Phase.finished.toString()); + return findOneBy(sc); + } + + @Override + public List listByPhaseAndDirection(ImageTransfer.Phase phase, ImageTransfer.Direction direction) { + SearchCriteria sc = phaseDirectionSearch.create(); + sc.setParameters("phase", phase); + sc.setParameters("direction", direction); + return listBy(sc); + } + + @Override + public List listByZonesAndOwners(List zoneIds, List accountIds, List domainIds, + Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + + return listBy(sc, filter); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java new file mode 100644 index 000000000000..e71ffe5ebde9 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDao.java @@ -0,0 +1,33 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupDataStoreVO; + +import java.util.List; + +public interface InternalBackupDataStoreDao extends GenericDao { + + List listByBackupId(long backupId); + + InternalBackupDataStoreVO findByBackupIdAndVolumeId(long backupId, long volumeId); + + void expungeByBackupId(long backupId); + + void updateVolumeId(long oldVolumeId, long newVolumeId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java new file mode 100644 index 000000000000..34a064ff62a2 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupDataStoreDaoImpl.java @@ -0,0 +1,74 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.UpdateBuilder; +import org.apache.cloudstack.backup.InternalBackupDataStoreVO; + +import javax.annotation.PostConstruct; +import java.util.List; + +public class InternalBackupDataStoreDaoImpl extends GenericDaoBase implements InternalBackupDataStoreDao { + + private SearchBuilder backupSearch; + + private static final String BACKUP_ID = "backup_id"; + private static final String VOLUME_ID = "volume_id"; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(BACKUP_ID, backupSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupSearch.and(VOLUME_ID, backupSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + backupSearch.done(); + } + + @Override + public List listByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + return listBy(sc); + } + + @Override + public InternalBackupDataStoreVO findByBackupIdAndVolumeId(long backupId, long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + sc.setParameters(VOLUME_ID, volumeId); + return findOneBy(sc); + } + + @Override + public void expungeByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + expunge(sc); + } + + @Override + public void updateVolumeId(long oldVolumeId, long newVolumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, oldVolumeId); + InternalBackupDataStoreVO delta = createForUpdate(); + delta.setVolumeId(newVolumeId); + UpdateBuilder ub = getUpdateBuilder(delta); + update(ub, sc, null); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java new file mode 100644 index 000000000000..2f9c3dd1ff8f --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDao.java @@ -0,0 +1,40 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupJoinVO; + +import java.util.Date; +import java.util.List; + +public interface InternalBackupJoinDao extends GenericDao { + + List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Date date, boolean before, boolean ascending); + + List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Date beforeDate); + + InternalBackupJoinVO findCurrent(long vmId); + + InternalBackupJoinVO findByParentId(long parentId); + + List listByImageStoreId(long imageStoreId); + + List listById(long id); + + List listByParentId(long parentId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java new file mode 100644 index 000000000000..e8d823bb69c5 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupJoinDaoImpl.java @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupJoinVO; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class InternalBackupJoinDaoImpl extends GenericDaoBase implements InternalBackupJoinDao { + + private static final String ID = "id"; + private static final String VM_ID = "vm_id"; + private static final String STATUS = "status"; + private static final String CREATED_BEFORE = "created_before"; + private static final String CREATED_AFTER = "created_after"; + private static final String CURRENT = "current"; + private static final String ISOLATED = "isolated"; + private static final String PARENT_ID = "parent_id"; + private static final String IMAGE_STORE_ID = "image_store_id"; + private SearchBuilder backupSearch; + private SearchBuilder allBackupsSearch; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(VM_ID, backupSearch.entity().getVmId(), SearchCriteria.Op.EQ); + backupSearch.and(STATUS, backupSearch.entity().getStatus(), SearchCriteria.Op.IN); + backupSearch.and(CREATED_BEFORE, backupSearch.entity().getDate(), SearchCriteria.Op.LT); + backupSearch.and(CREATED_AFTER, backupSearch.entity().getDate(), SearchCriteria.Op.GT); + backupSearch.and(CURRENT, backupSearch.entity().getCurrent(), SearchCriteria.Op.EQ); + backupSearch.and(PARENT_ID, backupSearch.entity().getParentId(), SearchCriteria.Op.EQ); + backupSearch.and(ISOLATED, backupSearch.entity().getIsolated(), SearchCriteria.Op.EQ); + backupSearch.groupBy(backupSearch.entity().getId()); + backupSearch.done(); + + allBackupsSearch = createSearchBuilder(); + allBackupsSearch.and(ID, allBackupsSearch.entity().getId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(STATUS, allBackupsSearch.entity().getStatus(), SearchCriteria.Op.IN); + allBackupsSearch.and(PARENT_ID, allBackupsSearch.entity().getParentId(), SearchCriteria.Op.EQ); + allBackupsSearch.and(IMAGE_STORE_ID, allBackupsSearch.entity().getImageStoreId(), SearchCriteria.Op.EQ); + allBackupsSearch.done(); + } + + @Override + public List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Date date, boolean before, boolean ascending) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + if (before) { + sc.setParameters(CREATED_BEFORE, date); + } else { + sc.setParameters(CREATED_AFTER, date); + } + sc.setParameters(ISOLATED, Boolean.FALSE.toString()); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", ascending); + return new ArrayList<>(listBy(sc, filter)); + } + + @Override + public List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Date beforeDate) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(STATUS, Backup.Status.BackedUp, Backup.Status.Removed); + sc.setParameters(CREATED_BEFORE, beforeDate); + sc.setParameters(ISOLATED, Boolean.FALSE.toString()); + Filter filter = new Filter(InternalBackupJoinVO.class, "date", false); + return new ArrayList<>(listIncludingRemovedBy(sc, filter)); + } + + @Override + public InternalBackupJoinVO findCurrent(long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VM_ID, vmId); + sc.setParameters(CURRENT, Boolean.TRUE.toString()); + return findOneBy(sc); + } + + @Override + public InternalBackupJoinVO findByParentId(long parentId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(PARENT_ID, parentId); + return findOneIncludingRemovedBy(sc); + } + + @Override + public List listByImageStoreId(long imageStoreId) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(IMAGE_STORE_ID, imageStoreId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } + + @Override + public List listById(long id) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(ID, id); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } + + @Override + public List listByParentId(long parentId) { + SearchCriteria sc = allBackupsSearch.create(); + sc.setParameters(PARENT_ID, parentId); + sc.setParameters(STATUS, Backup.Status.BackedUp); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java new file mode 100644 index 000000000000..c80734b9cb3c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDao.java @@ -0,0 +1,39 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupServiceJobType; +import org.apache.cloudstack.backup.InternalBackupServiceJobVO; + +import java.util.Date; +import java.util.List; + +public interface InternalBackupServiceJobDao extends GenericDao { + + List listExecutingJobsByZoneIdAndJobType(long zoneId, InternalBackupServiceJobType... jobTypes); + + List listWaitingJobsAndScheduledToBeforeNow(long zoneId, InternalBackupServiceJobType... jobTypes); + + List listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(Object[] hostIds, Date date, InternalBackupServiceJobType... jobTypes); + + Pair, Integer> searchAndCountForListApi(Long id, Long backupId, Long hostId, Long zoneId, InternalBackupServiceJobType type, boolean executing, + boolean scheduled, Long startIndex, Long pageSize); + + void update(InternalBackupServiceJobVO job); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java new file mode 100644 index 000000000000..32e81261838d --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupServiceJobDaoImpl.java @@ -0,0 +1,138 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.InternalBackupServiceJobType; +import org.apache.cloudstack.backup.InternalBackupServiceJobVO; + +import javax.annotation.PostConstruct; +import java.util.Date; +import java.util.List; + +public class InternalBackupServiceJobDaoImpl extends GenericDaoBase implements InternalBackupServiceJobDao { + private static final String ID = "id"; + private static final String BACKUP_ID = "backup_id"; + private SearchBuilder executingBeforeAndHostInAndTypeInSearch; + private SearchBuilder scheduledAndNotStartedSearch; + + private SearchBuilder executingAndZoneIdAndTypeSearch; + + private static final String HOST_ID = "host_id"; + private static final String TYPE = "type"; + private static final String START_TIME = "start_time"; + private static final String SCHEDULED = "scheduled"; + private static final String ZONE_ID = "zone_id"; + + @PostConstruct + protected void init() { + executingBeforeAndHostInAndTypeInSearch = createSearchBuilder(); + executingBeforeAndHostInAndTypeInSearch.and(HOST_ID, executingBeforeAndHostInAndTypeInSearch.entity().getHostId(), SearchCriteria.Op.IN); + executingBeforeAndHostInAndTypeInSearch.and(START_TIME, executingBeforeAndHostInAndTypeInSearch.entity().getStartTime(), SearchCriteria.Op.LTEQ); + executingBeforeAndHostInAndTypeInSearch.and(TYPE, executingBeforeAndHostInAndTypeInSearch.entity().getType(), SearchCriteria.Op.IN); + executingBeforeAndHostInAndTypeInSearch.done(); + + scheduledAndNotStartedSearch = createSearchBuilder(); + scheduledAndNotStartedSearch.and(SCHEDULED, scheduledAndNotStartedSearch.entity().getScheduledStartTime(), SearchCriteria.Op.LTEQ); + scheduledAndNotStartedSearch.and(START_TIME, scheduledAndNotStartedSearch.entity().getStartTime(), SearchCriteria.Op.NULL); + scheduledAndNotStartedSearch.and(ZONE_ID, scheduledAndNotStartedSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + scheduledAndNotStartedSearch.and(TYPE, scheduledAndNotStartedSearch.entity().getType(), SearchCriteria.Op.IN); + scheduledAndNotStartedSearch.done(); + + executingAndZoneIdAndTypeSearch = createSearchBuilder(); + executingAndZoneIdAndTypeSearch.and(START_TIME, executingAndZoneIdAndTypeSearch.entity().getStartTime(), SearchCriteria.Op.NNULL); + executingAndZoneIdAndTypeSearch.and(ZONE_ID, executingAndZoneIdAndTypeSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + executingAndZoneIdAndTypeSearch.and(TYPE, executingAndZoneIdAndTypeSearch.entity().getType(), SearchCriteria.Op.IN); + executingAndZoneIdAndTypeSearch.done(); + } + + @Override + public List listExecutingJobsByZoneIdAndJobType(long zoneId, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = executingAndZoneIdAndTypeSearch.create(); + sc.setParameters(TYPE, (Object[]) jobTypes); + sc.setParameters(ZONE_ID, zoneId); + + return listBy(sc); + } + + @Override + public List listWaitingJobsAndScheduledToBeforeNow(long zoneId, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = scheduledAndNotStartedSearch.create(); + + sc.setParameters(SCHEDULED, DateUtil.now()); + sc.setParameters(ZONE_ID, zoneId); + sc.setParameters(TYPE, (Object[]) jobTypes); + + Filter filter = new Filter(InternalBackupServiceJobVO.class, "scheduledStartTime", true); + return listBy(sc, filter); + } + + @Override + public List listExecutingJobsByHostsAndStartTimeBeforeAndTypeIn(Object[] hostIds, Date date, InternalBackupServiceJobType... jobTypes) { + SearchCriteria sc = executingBeforeAndHostInAndTypeInSearch.create(); + sc.setParameters(HOST_ID, hostIds); + sc.setParameters(START_TIME, date); + sc.setParameters(TYPE, (Object[]) jobTypes); + + return listBy(sc); + } + + @Override + public Pair, Integer> searchAndCountForListApi(Long id, Long backupId, Long hostId, Long zoneId, InternalBackupServiceJobType type, boolean executing, + boolean scheduled, Long startIndex, Long pageSize) { + SearchBuilder sb = createSearchBuilder(); + + sb.and(ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(BACKUP_ID, sb.entity().getBackupId(), SearchCriteria.Op.EQ); + sb.and(HOST_ID, sb.entity().getHostId(), SearchCriteria.Op.EQ); + sb.and(ZONE_ID, sb.entity().getZoneId(), SearchCriteria.Op.EQ); + sb.and(TYPE, sb.entity().getType(), SearchCriteria.Op.EQ); + + boolean removed = !executing && !scheduled; + if (executing && !scheduled) { + sb.and("executing", sb.entity().getStartTime(), SearchCriteria.Op.NNULL); + } else if (scheduled && !executing) { + sb.and("scheduled", sb.entity().getStartTime(), SearchCriteria.Op.NULL); + } + + SearchCriteria sc = sb.create(); + + sc.setParametersIfNotNull(ID, id); + sc.setParametersIfNotNull(BACKUP_ID, backupId); + sc.setParametersIfNotNull(HOST_ID, hostId); + sc.setParametersIfNotNull(ZONE_ID, zoneId); + if (type != null) { + sc.setParameters(TYPE, type); + } + + Filter filter = new Filter(InternalBackupServiceJobVO.class, "created", false, startIndex, pageSize); + + return searchAndCount(sc, filter, removed); + } + + @Override + @DB + public void update(InternalBackupServiceJobVO job) { + super.update(job.getId(), job); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java new file mode 100644 index 000000000000..7e2ac5a249e4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDao.java @@ -0,0 +1,33 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.backup.InternalBackupStoragePoolVO; + +import java.util.List; + +public interface InternalBackupStoragePoolDao extends GenericDao { + + List listByBackupId(long backupId); + + InternalBackupStoragePoolVO findOneByVolumeId(long volumeId); + + void expungeByBackupId(long backupId); + + void expungeByVolumeId(long volumeId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java new file mode 100644 index 000000000000..443ceed02e7d --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/InternalBackupStoragePoolDaoImpl.java @@ -0,0 +1,70 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. +package org.apache.cloudstack.backup.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.backup.InternalBackupStoragePoolVO; + +import javax.annotation.PostConstruct; +import java.util.List; + +public class InternalBackupStoragePoolDaoImpl extends GenericDaoBase implements InternalBackupStoragePoolDao { + + private SearchBuilder backupSearch; + + private static final String BACKUP_ID = "backup_id"; + + private static final String VOLUME_ID = "volume_id"; + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and(BACKUP_ID, backupSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupSearch.and(VOLUME_ID, backupSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + backupSearch.done(); + } + + @Override + public List listByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + return listBy(sc); + } + + @Override + public InternalBackupStoragePoolVO findOneByVolumeId(long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + return findOneBy(sc); + } + + @Override + public void expungeByBackupId(long backupId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(BACKUP_ID, backupId); + expunge(sc); + } + + @Override + public void expungeByVolumeId(long volumeId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + expunge(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java index 2df23b3d1064..45bad5a44d79 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoinVO.java @@ -63,6 +63,9 @@ public class GuiThemeJoinVO implements GuiThemeJoin { @Column(name = "is_public") private boolean isPublic; + @Column(name = "login_base_domain") + private String loginBaseDomain; + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) @Temporal(value = TemporalType.TIMESTAMP) private Date created; @@ -138,4 +141,9 @@ public Date getCreated() { public Date getRemoved() { return removed; } + + @Override + public String getLoginBaseDomain() { + return loginBaseDomain; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java index 887e3886f6c6..729071f60b80 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeVO.java @@ -59,6 +59,9 @@ public class GuiThemeVO implements GuiTheme { @Column(name = "recursive_domains") private boolean recursiveDomains = false; + @Column(name = "login_base_domain", length = 65535) + private String loginBaseDomain; + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) @Temporal(value = TemporalType.TIMESTAMP) private Date created; @@ -71,7 +74,8 @@ public GuiThemeVO() { } - public GuiThemeVO(String name, String description, String css, String jsonConfiguration, boolean recursiveDomains, boolean isPublic, Date created, Date removed) { + public GuiThemeVO(String name, String description, String css, String jsonConfiguration, boolean recursiveDomains, + boolean isPublic, Date created, String loginBaseDomain, Date removed) { this.name = name; this.description = description; this.css = css; @@ -79,6 +83,7 @@ public GuiThemeVO(String name, String description, String css, String jsonConfig this.recursiveDomains = recursiveDomains; this.isPublic = isPublic; this.created = created; + this.loginBaseDomain = loginBaseDomain; this.removed = removed; } @@ -186,4 +191,8 @@ public void setRecursiveDomains(boolean recursiveDomains) { public String toString() { return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "uuid", "name", "description", "isPublic", "recursiveDomains"); } + + public void setLoginBaseDomain(String loginBaseDomain) { + this.loginBaseDomain = loginBaseDomain; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileDetailsVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileDetailsVO.java new file mode 100644 index 000000000000..cd20b8a74fe5 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileDetailsVO.java @@ -0,0 +1,84 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import org.apache.cloudstack.api.ResourceDetail; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "kms_hsm_profile_details") +public class HSMProfileDetailsVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "profile_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + public HSMProfileDetailsVO() { + } + + public HSMProfileDetailsVO(long profileId, String name, String value) { + this.resourceId = profileId; + this.name = name; + this.value = value; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return true; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java new file mode 100644 index 000000000000..86d57400c0d2 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java @@ -0,0 +1,183 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "kms_hsm_profiles") +public class HSMProfileVO implements HSMProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "protocol") + private String protocol; + + @Column(name = "account_id") + private Long accountId; + + @Column(name = "domain_id") + private Long domainId; + + @Column(name = "zone_id") + private Long zoneId; + + @Column(name = "vendor_name") + private String vendorName; + + @Column(name = "enabled") + private boolean enabled; + + @Column(name = "is_public") + private boolean isPublic; + + @Column(name = "created") + private Date created; + + @Column(name = "removed") + private Date removed; + + public HSMProfileVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + this.isPublic = false; + } + + public HSMProfileVO(String name, String protocol, Long accountId, Long domainId, Long zoneId, String vendorName) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.protocol = protocol; + this.accountId = accountId; + this.domainId = domainId; + this.zoneId = zoneId; + this.vendorName = vendorName; + this.enabled = true; + this.isPublic = false; + this.created = new Date(); + } + + @Override + public String toString() { + return String.format("HSMProfileVO %s", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "name", "protocol", "system", "enabled")); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public long getAccountId() { + return accountId == null ? -1 : accountId; + } + + @Override + public long getDomainId() { + return domainId == null ? -1 : domainId; + } + + @Override + public Long getZoneId() { + return zoneId; + } + + @Override + public String getVendorName() { + return vendorName; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Date getRemoved() { + return removed; + } + + @Override + public Class getEntityType() { + return HSMProfile.class; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setVendorName(String vendorName) { + this.vendorName = vendorName; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean getIsPublic() { + return isPublic; + } + + public void setIsPublic(boolean isPublic) { + this.isPublic = isPublic; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKekVersionVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKekVersionVO.java new file mode 100644 index 000000000000..1dacdff68027 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKekVersionVO.java @@ -0,0 +1,193 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +/** + * Database entity for KEK versions. + * Tracks multiple KEK versions per KMS key to support gradual rotation. + * During rotation, a new version is created (status=Active) and old versions + * are marked as Previous (still usable for decryption) or Archived (no longer used). + */ +@Entity +@Table(name = "kms_kek_versions") +public class KMSKekVersionVO { + + public enum Status { + /** + * Used for new encryption operations + */ + Active, + /** + * Still usable for decryption during key rotation + */ + Previous, + /** + * No longer used; all wrapped keys have been re-encrypted + */ + Archived + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid; + + @Column(name = "kms_key_id", nullable = false) + private Long kmsKeyId; + + @Column(name = "version_number", nullable = false) + private Integer versionNumber; + + @Column(name = "kek_label", nullable = false) + private String kekLabel; + + @Column(name = "status", nullable = false, length = 32) + @Enumerated(EnumType.STRING) + private Status status; + + @Column(name = "hsm_profile_id") + private Long hsmProfileId; + + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(TemporalType.TIMESTAMP) + private Date removed; + + public KMSKekVersionVO(Long kmsKeyId, Integer versionNumber, String kekLabel) { + this(); + this.kmsKeyId = kmsKeyId; + this.versionNumber = versionNumber; + this.kekLabel = kekLabel; + this.status = Status.Active; + } + + public KMSKekVersionVO(Long kmsKeyId, String kekLabel) { + this(); + this.kmsKeyId = kmsKeyId; + this.kekLabel = kekLabel; + this.status = Status.Active; + } + + public KMSKekVersionVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + this.status = Status.Active; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Long getKmsKeyId() { + return kmsKeyId; + } + + public void setKmsKeyId(Long kmsKeyId) { + this.kmsKeyId = kmsKeyId; + } + + public Integer getVersionNumber() { + return versionNumber; + } + + public void setVersionNumber(Integer versionNumber) { + this.versionNumber = versionNumber; + } + + public String getKekLabel() { + return kekLabel; + } + + public void setKekLabel(String kekLabel) { + this.kekLabel = kekLabel; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Long getHsmProfileId() { + return hsmProfileId; + } + + public void setHsmProfileId(Long hsmProfileId) { + this.hsmProfileId = hsmProfileId; + } + + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public String toString() { + return String.format("KMSKekVersion %s", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "kmsKeyId", "versionNumber", "status", "kekLabel")); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKeyVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKeyVO.java new file mode 100644 index 000000000000..36c68de3744b --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSKeyVO.java @@ -0,0 +1,264 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.kms.KeyPurpose; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +/** + * Database entity for KMS Key (Key Encryption Key) metadata. + * Tracks ownership, purpose, and lifecycle of KEKs used in envelope encryption. + */ +@Entity +@Table(name = "kms_keys") +public class KMSKeyVO implements KMSKey { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description", length = 1024) + private String description; + + @Column(name = "kek_label", nullable = false) + private String kekLabel; + + @Column(name = "purpose", nullable = false, length = 32) + @Enumerated(EnumType.STRING) + private KeyPurpose purpose; + + @Column(name = "account_id", nullable = false) + private Long accountId; + + @Column(name = "domain_id", nullable = false) + private Long domainId; + + @Column(name = "zone_id", nullable = false) + private Long zoneId; + + @Column(name = "algorithm", nullable = false, length = 64) + private String algorithm; + + @Column(name = "key_bits", nullable = false) + private Integer keyBits; + + @Column(name = "enabled", nullable = false) + private boolean enabled; + + @Column(name = "hsm_profile_id") + private Long hsmProfileId; + + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(TemporalType.TIMESTAMP) + private Date removed; + + public KMSKeyVO(String name, String description, String kekLabel, + KeyPurpose purpose, Long accountId, Long domainId, + Long zoneId, String algorithm, Integer keyBits + ) { + this(); + this.name = name; + this.description = description; + this.kekLabel = kekLabel; + this.purpose = purpose; + this.accountId = accountId; + this.domainId = domainId; + this.zoneId = zoneId; + this.algorithm = algorithm; + this.keyBits = keyBits; + } + + public KMSKeyVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + this.enabled = true; + } + + @Override + public long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getKekLabel() { + return kekLabel; + } + + @Override + public KeyPurpose getPurpose() { + return purpose; + } + + @Override + public Long getZoneId() { + return zoneId; + } + + @Override + public String getAlgorithm() { + return algorithm; + } + + @Override + public Integer getKeyBits() { + return keyBits; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Date getRemoved() { + return removed; + } + + @Override + public Long getHsmProfileId() { + return hsmProfileId; + } + + public void setHsmProfileId(Long hsmProfileId) { + this.hsmProfileId = hsmProfileId; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setKeyBits(Integer keyBits) { + this.keyBits = keyBits; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public void setPurpose(KeyPurpose purpose) { + this.purpose = purpose; + } + + public void setKekLabel(String kekLabel) { + this.kekLabel = kekLabel; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + @Override + public long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + @Override + public Class getEntityType() { + return KMSKey.class; + } + + @Override + public String toString() { + return String.format("KMSKey %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "name", "purpose", + "accountId", "zoneId", "enabled" + )); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSWrappedKeyVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSWrappedKeyVO.java new file mode 100644 index 000000000000..8a763623fba7 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/KMSWrappedKeyVO.java @@ -0,0 +1,176 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Arrays; +import java.util.Date; +import java.util.UUID; + +/** + * Database entity for storing wrapped (encrypted) Data Encryption Keys. + * Each entry represents a DEK that has been encrypted by a Key Encryption Key (KEK). + * KEK metadata is stored in kms_keys table via the kms_key_id foreign key. + */ +@Entity +@Table(name = "kms_wrapped_key") +public class KMSWrappedKeyVO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "uuid", nullable = false) + private String uuid; + + @Column(name = "kms_key_id") + private Long kmsKeyId; + + @Column(name = "kek_version_id") + private Long kekVersionId; + + @Column(name = "zone_id", nullable = false) + private Long zoneId; + + @Column(name = "wrapped_blob", nullable = false) + private byte[] wrappedBlob; + + @Column(name = GenericDao.CREATED_COLUMN, nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(TemporalType.TIMESTAMP) + private Date removed; + + public KMSWrappedKeyVO(KMSKeyVO kmsKey, byte[] wrappedBlob) { + this(); + this.kmsKeyId = kmsKey.getId(); + this.zoneId = kmsKey.getZoneId(); + this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public KMSWrappedKeyVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public KMSWrappedKeyVO(KMSKeyVO kmsKey, Long kekVersionId, byte[] wrappedBlob) { + this(); + this.kmsKeyId = kmsKey.getId(); + this.kekVersionId = kekVersionId; + this.zoneId = kmsKey.getZoneId(); + this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public KMSWrappedKeyVO(Long kmsKeyId, Long zoneId, byte[] wrappedBlob) { + this(); + this.kmsKeyId = kmsKeyId; + this.zoneId = zoneId; + this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public KMSWrappedKeyVO(Long kmsKeyId, Long kekVersionId, Long zoneId, byte[] wrappedBlob) { + this(); + this.kmsKeyId = kmsKeyId; + this.kekVersionId = kekVersionId; + this.zoneId = zoneId; + this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Long getKmsKeyId() { + return kmsKeyId; + } + + public void setKmsKeyId(Long kmsKeyId) { + this.kmsKeyId = kmsKeyId; + } + + public Long getKekVersionId() { + return kekVersionId; + } + + public void setKekVersionId(Long kekVersionId) { + this.kekVersionId = kekVersionId; + } + + public Long getZoneId() { + return zoneId; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public byte[] getWrappedBlob() { + return wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public void setWrappedBlob(byte[] wrappedBlob) { + this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public String toString() { + return String.format("KMSWrappedKey %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "kmsKeyId", "kekVersionId", "zoneId", "created", "removed")); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDao.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDao.java new file mode 100644 index 000000000000..a2202a18bfc1 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDao.java @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.kms.HSMProfileVO; + +public interface HSMProfileDao extends GenericDao { +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDaoImpl.java new file mode 100644 index 000000000000..b063d2cfe749 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDaoImpl.java @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDaoBase; +import org.apache.cloudstack.kms.HSMProfileVO; +import org.springframework.stereotype.Component; + +@Component +public class HSMProfileDaoImpl extends GenericDaoBase implements HSMProfileDao { + public HSMProfileDaoImpl() { + super(); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDao.java new file mode 100644 index 000000000000..0d5c71b9e5f0 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDao.java @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.kms.HSMProfileDetailsVO; + +import java.util.List; + +public interface HSMProfileDetailsDao extends GenericDao { + List listByProfileId(long profileId); + + void persist(long profileId, String name, String value); + + HSMProfileDetailsVO findDetail(long profileId, String name); + + void deleteDetails(long profileId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDaoImpl.java new file mode 100644 index 000000000000..59c0ec43259c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/HSMProfileDetailsDaoImpl.java @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.SearchCriteria.Op; +import org.apache.cloudstack.kms.HSMProfileDetailsVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class HSMProfileDetailsDaoImpl extends GenericDaoBase implements HSMProfileDetailsDao { + + protected SearchBuilder ProfileSearch; + protected SearchBuilder DetailSearch; + + public HSMProfileDetailsDaoImpl() { + super(); + + ProfileSearch = createSearchBuilder(); + ProfileSearch.and("profileId", ProfileSearch.entity().getResourceId(), Op.EQ); + ProfileSearch.done(); + + DetailSearch = createSearchBuilder(); + DetailSearch.and("profileId", DetailSearch.entity().getResourceId(), Op.EQ); + DetailSearch.and("name", DetailSearch.entity().getName(), Op.EQ); + DetailSearch.done(); + } + + @Override + public List listByProfileId(long profileId) { + SearchCriteria sc = ProfileSearch.create(); + sc.setParameters("profileId", profileId); + return listBy(sc); + } + + @Override + public void persist(long profileId, String name, String value) { + HSMProfileDetailsVO vo = new HSMProfileDetailsVO(profileId, name, value); + persist(vo); + } + + @Override + public HSMProfileDetailsVO findDetail(long profileId, String name) { + SearchCriteria sc = DetailSearch.create(); + sc.setParameters("profileId", profileId); + sc.setParameters("name", name); + return findOneBy(sc); + } + + @Override + public void deleteDetails(long profileId) { + SearchCriteria sc = ProfileSearch.create(); + sc.setParameters("profileId", profileId); + remove(sc); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDao.java similarity index 51% rename from server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java rename to engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDao.java index cd295e92caf3..dbe4ca5f648a 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDao.java @@ -15,26 +15,29 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.storage.heuristics.presetvariables; +package org.apache.cloudstack.kms.dao; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.kms.KMSKekVersionVO; -@RunWith(MockitoJUnitRunner.class) -public class GenericHeuristicPresetVariableTest { +import java.util.List; - @Test - public void toStringTestReturnsValidJson() { - GenericHeuristicPresetVariable variable = new GenericHeuristicPresetVariable(); - variable.setName("test name"); +public interface KMSKekVersionDao extends GenericDao { - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name"); - String result = variable.toString(); + KMSKekVersionVO getActiveVersion(Long kmsKeyId); - Assert.assertEquals(expected, result); - } + /** + * Returns Active and Previous versions (usable for decryption) + */ + List getVersionsForDecryption(Long kmsKeyId); + List listByKmsKeyId(Long kmsKeyId); + + KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber); + + KMSKekVersionVO findByKekLabel(String kekLabel); + + List findByStatus(KMSKekVersionVO.Status status); + + List listByHsmProfileId(Long hsmProfileId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDaoImpl.java new file mode 100644 index 000000000000..841ab8c03d9b --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKekVersionDaoImpl.java @@ -0,0 +1,98 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.kms.KMSKekVersionVO; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class KMSKekVersionDaoImpl extends GenericDaoBase implements KMSKekVersionDao { + + private final SearchBuilder allFieldSearch; + + public KMSKekVersionDaoImpl() { + allFieldSearch = createSearchBuilder(); + allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ); + allFieldSearch.and("status", allFieldSearch.entity().getStatus(), SearchCriteria.Op.IN); + allFieldSearch.and("versionNumber", allFieldSearch.entity().getVersionNumber(), SearchCriteria.Op.EQ); + allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ); + allFieldSearch.and("hsmProfileId", allFieldSearch.entity().getHsmProfileId(), SearchCriteria.Op.EQ); + allFieldSearch.done(); + } + + @Override + public KMSKekVersionVO getActiveVersion(Long kmsKeyId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + sc.setParameters("status", KMSKekVersionVO.Status.Active); + return findOneBy(sc); + } + + @Override + public List getVersionsForDecryption(Long kmsKeyId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + sc.setParameters("status", KMSKekVersionVO.Status.Active, KMSKekVersionVO.Status.Previous); + return listBy(sc); + } + + @Override + public List listByKmsKeyId(Long kmsKeyId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + return listBy(sc); + } + + @Override + public KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + sc.setParameters("versionNumber", versionNumber); + return findOneBy(sc); + } + + @Override + public KMSKekVersionVO findByKekLabel(String kekLabel) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kekLabel", kekLabel); + return findOneBy(sc); + } + + @Override + public List findByStatus(KMSKekVersionVO.Status status) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("status", status); + return listBy(sc); + } + + @Override + public List listByHsmProfileId(Long hsmProfileId) { + if (hsmProfileId == null) { + return new ArrayList<>(); + } + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("hsmProfileId", hsmProfileId); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDao.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDao.java new file mode 100644 index 000000000000..4a03f2ff96c2 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDao.java @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.kms.KeyPurpose; +import org.apache.cloudstack.kms.KMSKeyVO; + +import java.util.List; + +public interface KMSKeyDao extends GenericDao { + + List listByAccount(Long accountId, KeyPurpose purpose, Boolean enabled); + + List listByZone(Long zoneId, KeyPurpose purpose, Boolean enabled); + + long countByHsmProfileId(Long hsmProfileId); + + KMSKeyVO findByNameAndAccountId(String name, long accountId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDaoImpl.java new file mode 100644 index 000000000000..ac442c19d1ab --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSKeyDaoImpl.java @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.framework.kms.KeyPurpose; +import org.apache.cloudstack.kms.KMSKeyVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class KMSKeyDaoImpl extends GenericDaoBase implements KMSKeyDao { + + private final SearchBuilder allFieldSearch; + + public KMSKeyDaoImpl() { + allFieldSearch = createSearchBuilder(); + allFieldSearch.and("name", allFieldSearch.entity().getName(), SearchCriteria.Op.EQ); + allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ); + allFieldSearch.and("domainId", allFieldSearch.entity().getDomainId(), SearchCriteria.Op.EQ); + allFieldSearch.and("accountId", allFieldSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + allFieldSearch.and("purpose", allFieldSearch.entity().getPurpose(), SearchCriteria.Op.EQ); + allFieldSearch.and("enabled", allFieldSearch.entity().isEnabled(), SearchCriteria.Op.EQ); + allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + allFieldSearch.and("hsmProfileId", allFieldSearch.entity().getHsmProfileId(), SearchCriteria.Op.EQ); + allFieldSearch.done(); + } + + @Override + public List listByAccount(Long accountId, KeyPurpose purpose, Boolean enabled) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParametersIfNotNull("purpose", purpose); + sc.setParametersIfNotNull("enabled", enabled); + return listBy(sc); + } + + @Override + public List listByZone(Long zoneId, KeyPurpose purpose, Boolean enabled) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("zoneId", zoneId); + sc.setParametersIfNotNull("purpose", purpose); + sc.setParametersIfNotNull("enabled", enabled); + return listBy(sc); + } + + @Override + public long countByHsmProfileId(Long hsmProfileId) { + if (hsmProfileId == null) { + return 0; + } + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("hsmProfileId", hsmProfileId); + Integer count = getCount(sc); + return count != null ? count : 0; + } + + @Override + public KMSKeyVO findByNameAndAccountId(String name, long accountId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("name", name); + sc.setParameters("accountId", accountId); + return findOneBy(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDao.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDao.java new file mode 100644 index 000000000000..5e9e418a1667 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDao.java @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.kms.KMSWrappedKeyVO; + +import java.util.List; + +public interface KMSWrappedKeyDao extends GenericDao { + + long countByKmsKeyId(Long kmsKeyId); + + /** + * Limited variant for batch processing during key rotation + */ + List listByKekVersionId(Long kekVersionId, int limit); + + long countByKekVersionId(Long kekVersionId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDaoImpl.java new file mode 100644 index 000000000000..fc1a7190ae8e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/dao/KMSWrappedKeyDaoImpl.java @@ -0,0 +1,70 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.kms.dao; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.kms.KMSWrappedKeyVO; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class KMSWrappedKeyDaoImpl extends GenericDaoBase implements KMSWrappedKeyDao { + + private final SearchBuilder allFieldSearch; + + public KMSWrappedKeyDaoImpl() { + super(); + + allFieldSearch = createSearchBuilder(); + allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ); + allFieldSearch.and("kekVersionId", allFieldSearch.entity().getKekVersionId(), SearchCriteria.Op.EQ); + allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + allFieldSearch.done(); + } + + @Override + public long countByKmsKeyId(Long kmsKeyId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kmsKeyId", kmsKeyId); + Integer count = getCount(sc); + return count != null ? count.longValue() : 0L; + } + + @Override + public List listByKekVersionId(Long kekVersionId, int limit) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kekVersionId", kekVersionId); + Filter filter = new Filter(limit); + return listBy(sc, filter); + } + + @Override + public long countByKekVersionId(Long kekVersionId) { + if (kekVersionId == null) { + return 0; + } + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("kekVersionId", kekVersionId); + Integer count = getCount(sc); + return count != null ? count.longValue() : 0L; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/region/gslb/GlobalLoadBalancerRuleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/region/gslb/GlobalLoadBalancerRuleVO.java index 4ce7033156fa..47e2d130d871 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/region/gslb/GlobalLoadBalancerRuleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/region/gslb/GlobalLoadBalancerRuleVO.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.region.gslb; +import java.util.Date; import java.util.UUID; import javax.persistence.Column; @@ -27,8 +28,11 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; import com.cloud.region.ha.GlobalLoadBalancerRule; +import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @Entity @@ -74,6 +78,10 @@ public class GlobalLoadBalancerRuleVO implements GlobalLoadBalancerRule { @Column(name = "state") GlobalLoadBalancerRule.State state; + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + public GlobalLoadBalancerRuleVO() { uuid = UUID.randomUUID().toString(); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java index 1102de16e4ea..4383278d7c48 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java @@ -113,4 +113,6 @@ public interface ResourceDetailsDao extends GenericDao long batchExpungeForResources(List ids, Long batchSize); String getActualValue(ResourceDetail resourceDetail); + + List listDetailsForResourceIdsAndKey(List resourceIds, String key); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java index eafaed182abd..8f376a71f66f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java @@ -16,11 +16,13 @@ // under the License. package org.apache.cloudstack.resourcedetail; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import com.cloud.utils.StringUtils; import org.apache.commons.collections.CollectionUtils; import com.cloud.utils.Pair; @@ -48,6 +50,7 @@ public abstract class ResourceDetailsDaoBase extends G public ResourceDetailsDaoBase() { AllFieldsSearch = createSearchBuilder(); AllFieldsSearch.and("resourceId", AllFieldsSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("resourceIdIn", AllFieldsSearch.entity().getResourceId(), SearchCriteria.Op.IN); AllFieldsSearch.and("name", AllFieldsSearch.entity().getName(), SearchCriteria.Op.EQ); AllFieldsSearch.and("value", AllFieldsSearch.entity().getValue(), SearchCriteria.Op.EQ); // FIXME SnapshotDetailsVO doesn't have a display field @@ -266,4 +269,15 @@ public String getActualValue(ResourceDetail resourceDetail) { } return resourceDetail.getValue(); } + + @Override + public List listDetailsForResourceIdsAndKey(List resourceIds, String key) { + if (CollectionUtils.isEmpty(resourceIds) || StringUtils.isBlank(key)) { + return Collections.emptyList(); + } + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("name", key); + sc.setParameters("resourceIdIn", resourceIds.toArray()); + return search(sc, null); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java index d0cfcc3d4396..93b49bc20a10 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java @@ -48,6 +48,8 @@ public class UserDetailVO implements ResourceDetail { public static final String Setup2FADetail = "2FASetupStatus"; public static final String PasswordResetToken = "PasswordResetToken"; public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate"; + public static final String PasswordChangeRequired = "PasswordChangeRequired"; + public static final String OauthLogin = "OauthLogin"; public UserDetailVO() { } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleDetailVO.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleDetailVO.java new file mode 100644 index 000000000000..16985e6bac30 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleDetailVO.java @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.schedule; + +import org.apache.cloudstack.api.ResourceDetail; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "resource_schedule_details") +public class ResourceScheduleDetailVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "schedule_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Column(name = "value", length = 1024) + private String value; + + @Column(name = "display") + private boolean display = true; + + public ResourceScheduleDetailVO() { + } + + public ResourceScheduleDetailVO(long scheduleId, String name, String value, boolean display) { + this.resourceId = scheduleId; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleVO.java similarity index 71% rename from engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleVO.java rename to engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleVO.java index e0065db1e77a..33a09d79239d 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduleVO.java @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule; +package org.apache.cloudstack.schedule; import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import javax.persistence.Column; @@ -37,8 +38,8 @@ import java.util.UUID; @Entity -@Table(name = "vm_schedule") -public class VMScheduleVO implements VMSchedule { +@Table(name = "resource_schedule") +public class ResourceScheduleVO implements ResourceSchedule { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) @@ -50,8 +51,12 @@ public class VMScheduleVO implements VMSchedule { @Column(name = "description") String description; - @Column(name = "vm_id", nullable = false) - long vmId; + @Enumerated(value = EnumType.STRING) + @Column(name = "resource_type", nullable = false) + ApiCommandResourceType resourceType; + + @Column(name = "resource_id", nullable = false) + long resourceId; @Column(name = "schedule", nullable = false) String schedule; @@ -60,8 +65,7 @@ public class VMScheduleVO implements VMSchedule { String timeZone; @Column(name = "action", nullable = false) - @Enumerated(value = EnumType.STRING) - Action action; + String actionName; @Column(name = "enabled", nullable = false) boolean enabled; @@ -80,17 +84,19 @@ public class VMScheduleVO implements VMSchedule { @Column(name = GenericDao.REMOVED_COLUMN) Date removed; - public VMScheduleVO() { + public ResourceScheduleVO() { uuid = UUID.randomUUID().toString(); } - public VMScheduleVO(long vmId, String description, String schedule, String timeZone, Action action, Date startDate, Date endDate, boolean enabled) { + public ResourceScheduleVO(ApiCommandResourceType resourceType, long resourceId, String description, String schedule, + String timeZone, String action, Date startDate, Date endDate, boolean enabled) { uuid = UUID.randomUUID().toString(); - this.vmId = vmId; + this.resourceType = resourceType; + this.resourceId = resourceId; this.description = description; this.schedule = schedule; this.timeZone = timeZone; - this.action = action; + this.actionName = action; this.startDate = startDate; this.endDate = endDate; this.enabled = enabled; @@ -98,7 +104,8 @@ public VMScheduleVO(long vmId, String description, String schedule, String timeZ @Override public String toString() { - return String.format("VMSchedule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "action", "description")); + return String.format("ResourceSchedule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "resourceType", "actionName", "description", "enabled")); } @Override @@ -111,14 +118,25 @@ public long getId() { return id; } - public long getVmId() { - return vmId; + @Override + public ApiCommandResourceType getResourceType() { + return resourceType; + } + + public void setResourceType(ApiCommandResourceType resourceType) { + this.resourceType = resourceType; + } + + @Override + public long getResourceId() { + return resourceId; } - public void setVmId(long vmId) { - this.vmId = vmId; + public void setResourceId(long resourceId) { + this.resourceId = resourceId; } + @Override public String getDescription() { return description; } @@ -127,6 +145,7 @@ public void setDescription(String description) { this.description = description; } + @Override public String getSchedule() { return schedule.substring(2); } @@ -144,14 +163,16 @@ public void setTimeZone(String timeZone) { this.timeZone = timeZone; } - public Action getAction() { - return action; + @Override + public String getActionName() { + return actionName; } - public void setAction(Action action) { - this.action = action; + public void setActionName(String action) { + this.actionName = action; } + @Override public boolean getEnabled() { return enabled; } @@ -183,6 +204,7 @@ public ZoneId getTimeZoneId() { return TimeZone.getTimeZone(getTimeZone()).toZoneId(); } + @Override public Date getCreated() { return created; } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJobVO.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJobVO.java similarity index 64% rename from engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJobVO.java rename to engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJobVO.java index 775e9cfe40cf..ca03056b67a8 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/VMScheduledJobVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/ResourceScheduledJobVO.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule; +package org.apache.cloudstack.schedule; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import javax.persistence.Column; @@ -34,8 +35,8 @@ import java.util.UUID; @Entity -@Table(name = "vm_scheduled_job") -public class VMScheduledJobVO implements VMScheduledJob { +@Table(name = "resource_scheduled_job") +public class ResourceScheduledJobVO implements ResourceScheduledJob { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -44,41 +45,44 @@ public class VMScheduledJobVO implements VMScheduledJob { @Column(name = "uuid", nullable = false) String uuid; - @Column(name = "vm_id", nullable = false) - long vmId; + @Enumerated(value = EnumType.STRING) + @Column(name = "resource_type", nullable = false) + ApiCommandResourceType resourceType; + + @Column(name = "resource_id", nullable = false) + long resourceId; - @Column(name = "vm_schedule_id", nullable = false) - long vmScheduleId; + @Column(name = "schedule_id", nullable = false) + long scheduleId; @Column(name = "async_job_id") Long asyncJobId; @Column(name = "action", nullable = false) - @Enumerated(value = EnumType.STRING) - VMSchedule.Action action; + String actionName; @Column(name = "scheduled_timestamp") @Temporal(value = TemporalType.TIMESTAMP) Date scheduledTime; - public VMScheduledJobVO() { + public ResourceScheduledJobVO() { uuid = UUID.randomUUID().toString(); } - public VMScheduledJobVO(long vmId, long vmScheduleId, VMSchedule.Action action, Date scheduledTime) { + public ResourceScheduledJobVO(ApiCommandResourceType resourceType, long resourceId, long scheduleId, String action, Date scheduledTime) { uuid = UUID.randomUUID().toString(); - this.vmId = vmId; - this.vmScheduleId = vmScheduleId; - this.action = action; + this.resourceType = resourceType; + this.resourceId = resourceId; + this.scheduleId = scheduleId; + this.actionName = action; this.scheduledTime = scheduledTime; } - @Override public String toString() { - return String.format("VMScheduledJob %s", + return String.format("ResourceScheduledJob %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( - this, "id", "uuid", "action", "vmScheduleId", "vmId", "asyncJobId")); + this, "id", "uuid", "resourceType", "actionName", "scheduleId", "resourceId", "asyncJobId")); } @Override @@ -92,13 +96,18 @@ public long getId() { } @Override - public long getVmId() { - return vmId; + public ApiCommandResourceType getResourceType() { + return resourceType; } @Override - public long getVmScheduleId() { - return vmScheduleId; + public long getResourceId() { + return resourceId; + } + + @Override + public long getScheduleId() { + return scheduleId; } @Override @@ -112,8 +121,12 @@ public void setAsyncJobId(long asyncJobId) { } @Override - public VMSchedule.Action getAction() { - return action; + public String getActionName() { + return actionName; + } + + public void setActionName(String action) { + this.actionName = action; } @Override diff --git a/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDao.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDao.java new file mode 100644 index 000000000000..ee56991fb9ac --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDao.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.schedule.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.schedule.ResourceScheduleVO; + +import java.util.List; + +public interface ResourceScheduleDao extends GenericDao { + List listAllActiveSchedules(ApiCommandResourceType resourceType); + + long removeSchedulesForResourceAndIds(ApiCommandResourceType resourceType, long resourceId, List ids); + + long removeAllSchedulesForResource(ApiCommandResourceType resourceType, long resourceId); + + Pair, Integer> searchAndCount(List ids, ApiCommandResourceType resourceType, Long resourceId, + String action, Boolean enabled, Long offset, Long limit); + + SearchCriteria getSearchCriteriaForResource(ApiCommandResourceType resourceType, long resourceId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDaoImpl.java new file mode 100644 index 000000000000..7217d1359a81 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDaoImpl.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.cloudstack.schedule.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.schedule.ResourceScheduleVO; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ResourceScheduleDaoImpl extends GenericDaoBase implements ResourceScheduleDao { + + private final SearchBuilder activeScheduleSearch; + private final SearchBuilder allSearch; + + static final String RESOURCE_TYPE = "resourceType"; + static final String RESOURCE_ID = "resourceId"; + + public ResourceScheduleDaoImpl() { + super(); + + activeScheduleSearch = createSearchBuilder(); + activeScheduleSearch.and(RESOURCE_TYPE, activeScheduleSearch.entity().getResourceType(), SearchCriteria.Op.EQ); + activeScheduleSearch.and(ApiConstants.ENABLED, activeScheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ); + activeScheduleSearch.done(); + + allSearch = createSearchBuilder(); + allSearch.and(ApiConstants.ID, allSearch.entity().getId(), SearchCriteria.Op.IN); + allSearch.and(RESOURCE_TYPE, allSearch.entity().getResourceType(), SearchCriteria.Op.EQ); + allSearch.and(RESOURCE_ID, allSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + allSearch.and(ApiConstants.ACTION, allSearch.entity().getActionName(), SearchCriteria.Op.EQ); + allSearch.and(ApiConstants.ENABLED, allSearch.entity().getEnabled(), SearchCriteria.Op.EQ); + allSearch.done(); + } + + @Override + public List listAllActiveSchedules(ApiCommandResourceType resourceType) { + SearchCriteria sc = activeScheduleSearch.create(); + sc.setParameters(RESOURCE_TYPE, resourceType); + sc.setParameters(ApiConstants.ENABLED, true); + return search(sc, null); + } + + @Override + public long removeSchedulesForResourceAndIds(ApiCommandResourceType resourceType, long resourceId, List ids) { + SearchCriteria sc = allSearch.create(); + if (CollectionUtils.isNotEmpty(ids)) { + sc.setParameters(ApiConstants.ID, ids.toArray()); + } + sc.setParameters(RESOURCE_TYPE, resourceType); + sc.setParameters(RESOURCE_ID, resourceId); + return remove(sc); + } + + @Override + public long removeAllSchedulesForResource(ApiCommandResourceType resourceType, long resourceId) { + SearchCriteria sc = allSearch.create(); + sc.setParameters(RESOURCE_TYPE, resourceType); + sc.setParameters(RESOURCE_ID, resourceId); + return remove(sc); + } + + @Override + public Pair, Integer> searchAndCount(List ids, ApiCommandResourceType resourceType, Long resourceId, + String action, Boolean enabled, Long offset, Long limit) { + SearchCriteria sc = allSearch.create(); + if (CollectionUtils.isNotEmpty(ids)) { + sc.setParameters(ApiConstants.ID, ids.toArray()); + } + sc.setParametersIfNotNull(ApiConstants.ENABLED, enabled); + sc.setParametersIfNotNull(ApiConstants.ACTION, action); + sc.setParametersIfNotNull(RESOURCE_TYPE, resourceType); + sc.setParametersIfNotNull(RESOURCE_ID, resourceId); + Filter filter = new Filter(ResourceScheduleVO.class, ApiConstants.ID, false, offset, limit); + return searchAndCount(sc, filter); + } + + @Override + public SearchCriteria getSearchCriteriaForResource(ApiCommandResourceType resourceType, long resourceId) { + SearchCriteria sc = allSearch.create(); + sc.setParameters(RESOURCE_TYPE, resourceType); + sc.setParameters(RESOURCE_ID, resourceId); + return sc; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDao.java new file mode 100644 index 000000000000..f36ce8d0879c --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDao.java @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.schedule.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; +import org.apache.cloudstack.schedule.ResourceScheduleDetailVO; + +public interface ResourceScheduleDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDaoImpl.java new file mode 100644 index 000000000000..3757e41d0cfe --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduleDetailsDaoImpl.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.schedule.dao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.apache.cloudstack.schedule.ResourceScheduleDetailVO; + +public class ResourceScheduleDetailsDaoImpl extends ResourceDetailsDaoBase implements ResourceScheduleDetailsDao { + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ResourceScheduleDetailVO(resourceId, key, value, display)); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDao.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDao.java similarity index 57% rename from engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDao.java rename to engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDao.java index b8c808b5bfc9..16210802235f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDao.java @@ -16,22 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule.dao; +package org.apache.cloudstack.schedule.dao; -import com.cloud.utils.Pair; import com.cloud.utils.db.GenericDao; -import com.cloud.utils.db.SearchCriteria; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleVO; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.schedule.ResourceScheduledJobVO; +import java.util.Date; import java.util.List; -public interface VMScheduleDao extends GenericDao { - List listAllActiveSchedules(); +public interface ResourceScheduledJobDao extends GenericDao { + List listJobsToStart(ApiCommandResourceType resourceType, Date currentTimestamp); - long removeSchedulesForVmIdAndIds(Long vmId, List ids); + int expungeJobsForSchedules(List scheduleIds, Date dateAfter); - Pair, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit); + int expungeJobsBefore(ApiCommandResourceType resourceType, Date currentTimestamp); - SearchCriteria getSearchCriteriaForVMId(Long vmId); + ResourceScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDaoImpl.java similarity index 55% rename from engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java rename to engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDaoImpl.java index 2f08a41b92e4..02c1acb6c719 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduledJobDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/schedule/dao/ResourceScheduledJobDaoImpl.java @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cloudstack.vm.schedule.dao; +package org.apache.cloudstack.schedule.dao; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import org.apache.cloudstack.vm.schedule.VMScheduledJobVO; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.schedule.ResourceScheduledJobVO; import org.apache.commons.lang3.time.DateUtils; import org.springframework.stereotype.Component; @@ -31,62 +32,59 @@ import java.util.List; @Component -public class VMScheduledJobDaoImpl extends GenericDaoBase implements VMScheduledJobDao { +public class ResourceScheduledJobDaoImpl extends GenericDaoBase implements ResourceScheduledJobDao { - private final SearchBuilder jobsToStartSearch; + private final SearchBuilder jobsToStartSearch; + private final SearchBuilder expungeJobsBeforeSearch; + private final SearchBuilder expungeJobForScheduleSearch; + private final SearchBuilder scheduleAndTimestampSearch; - private final SearchBuilder expungeJobsBeforeSearch; + static final String SCHEDULED_TIMESTAMP = "scheduledTimestamp"; + static final String SCHEDULE_ID = "scheduleId"; + static final String RESOURCE_TYPE = "resourceType"; - private final SearchBuilder expungeJobForScheduleSearch; - - private final SearchBuilder scheduleAndTimestampSearch; - - static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp"; - - static final String VM_SCHEDULE_ID = "vm_schedule_id"; - - public VMScheduledJobDaoImpl() { + public ResourceScheduledJobDaoImpl() { super(); + jobsToStartSearch = createSearchBuilder(); + jobsToStartSearch.and(RESOURCE_TYPE, jobsToStartSearch.entity().getResourceType(), SearchCriteria.Op.EQ); jobsToStartSearch.and(SCHEDULED_TIMESTAMP, jobsToStartSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ); jobsToStartSearch.and("async_job_id", jobsToStartSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL); jobsToStartSearch.done(); expungeJobsBeforeSearch = createSearchBuilder(); + expungeJobsBeforeSearch.and(RESOURCE_TYPE, expungeJobsBeforeSearch.entity().getResourceType(), SearchCriteria.Op.EQ); expungeJobsBeforeSearch.and(SCHEDULED_TIMESTAMP, expungeJobsBeforeSearch.entity().getScheduledTime(), SearchCriteria.Op.LT); expungeJobsBeforeSearch.done(); expungeJobForScheduleSearch = createSearchBuilder(); - expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN); + expungeJobForScheduleSearch.and(SCHEDULE_ID, expungeJobForScheduleSearch.entity().getScheduleId(), SearchCriteria.Op.IN); expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ); expungeJobForScheduleSearch.done(); scheduleAndTimestampSearch = createSearchBuilder(); - scheduleAndTimestampSearch.and(VM_SCHEDULE_ID, scheduleAndTimestampSearch.entity().getVmScheduleId(), SearchCriteria.Op.EQ); + scheduleAndTimestampSearch.and(SCHEDULE_ID, scheduleAndTimestampSearch.entity().getScheduleId(), SearchCriteria.Op.EQ); scheduleAndTimestampSearch.and(SCHEDULED_TIMESTAMP, scheduleAndTimestampSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ); scheduleAndTimestampSearch.done(); } - /** - * Execution of job wouldn't be at exact seconds. So, we round off and then execute. - */ @Override - public List listJobsToStart(Date currentTimestamp) { + public List listJobsToStart(ApiCommandResourceType resourceType, Date currentTimestamp) { if (currentTimestamp == null) { currentTimestamp = new Date(); } Date truncatedTs = DateUtils.round(currentTimestamp, Calendar.MINUTE); - - SearchCriteria sc = jobsToStartSearch.create(); + SearchCriteria sc = jobsToStartSearch.create(); + sc.setParameters(RESOURCE_TYPE, resourceType); sc.setParameters(SCHEDULED_TIMESTAMP, truncatedTs); - Filter filter = new Filter(VMScheduledJobVO.class, "vmScheduleId", true, null, null); + Filter filter = new Filter(ResourceScheduledJobVO.class, "scheduleId", true, null, null); return search(sc, filter); } @Override - public int expungeJobsForSchedules(List vmScheduleIds, Date dateAfter) { - SearchCriteria sc = expungeJobForScheduleSearch.create(); - sc.setParameters(VM_SCHEDULE_ID, vmScheduleIds.toArray()); + public int expungeJobsForSchedules(List scheduleIds, Date dateAfter) { + SearchCriteria sc = expungeJobForScheduleSearch.create(); + sc.setParameters(SCHEDULE_ID, scheduleIds.toArray()); if (dateAfter != null) { sc.setParameters(SCHEDULED_TIMESTAMP, dateAfter); } @@ -94,16 +92,17 @@ public int expungeJobsForSchedules(List vmScheduleIds, Date dateAfter) { } @Override - public int expungeJobsBefore(Date date) { - SearchCriteria sc = expungeJobsBeforeSearch.create(); + public int expungeJobsBefore(ApiCommandResourceType resourceType, Date date) { + SearchCriteria sc = expungeJobsBeforeSearch.create(); + sc.setParameters(RESOURCE_TYPE, resourceType); sc.setParameters(SCHEDULED_TIMESTAMP, date); return expunge(sc); } @Override - public VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) { - SearchCriteria sc = scheduleAndTimestampSearch.create(); - sc.setParameters(VM_SCHEDULE_ID, scheduleId); + public ResourceScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) { + SearchCriteria sc = scheduleAndTimestampSearch.create(); + sc.setParameters(SCHEDULE_ID, scheduleId); sc.setParameters(SCHEDULED_TIMESTAMP, scheduledTimestamp); return findOneBy(sc); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java index 4cb40b5eaf63..5f32a3502328 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java @@ -202,7 +202,7 @@ public ImageStoreVO findOneByZoneAndProtocol(long dataCenterId, String protocol) sc.setParameters("dataCenterId", dataCenterId); sc.setParameters("protocol", protocol); sc.setParameters("role", DataStoreRole.Image); - Filter filter = new Filter(1); + Filter filter = new Filter(1, true); List results = listBy(sc, filter); return results.size() == 0 ? null : results.get(0); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java index ec40dc0dd683..d7e88bd31c3f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDetailsDaoImpl.java @@ -77,7 +77,7 @@ public Map getDetails(long storeId) { for (ImageStoreDetailVO detail : details) { String name = detail.getName(); String value = detail.getValue(); - if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.S3_SECRET_KEY)) { + if (name.equals(ApiConstants.KEY) || name.equals(ApiConstants.SECRET_KEY)) { value = DBEncryptionUtil.decrypt(value); } detailsMap.put(name, value); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java index 6aeee1ad1ccd..2d337fe07fc7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java @@ -51,6 +51,8 @@ public interface SnapshotDataStoreDao extends GenericDao listBySnapshotIdAndDataStoreRoleAndStateIn(long snapshotId, DataStoreRole role, ObjectInDataStoreStateMachine.State... state); + List listReadyByVolumeIdAndCheckpointPathNotNull(long volumeId); SnapshotDataStoreVO findOneBySnapshotId(long snapshotId, long zoneId); @@ -97,6 +99,8 @@ public interface SnapshotDataStoreDao extends GenericDao findByVolume(long snapshotId, long volumeId, DataStoreRole role); + void expungeBySnapshotIdAndStoreRole(long snapshotId, DataStoreRole role); + /** * List all snapshots in 'snapshot_store_ref' by volume and data store role. Therefore, it is possible to list all snapshots that are in the primary storage or in the secondary storage. */ diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index cdf903407c17..cb88e21cf9d6 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@ -63,11 +63,12 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase searchFilteringStoreIdEqStoreRoleEqStateNeqRefCntNeq; protected SearchBuilder searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq; private SearchBuilder stateSearch; - private SearchBuilder idStateNeqSearch; + private SearchBuilder idStateNinSearch; + private SearchBuilder idEqRoleEqStateInSearch; protected SearchBuilder snapshotVOSearch; private SearchBuilder snapshotCreatedSearch; private SearchBuilder dataStoreAndInstallPathSearch; @@ -75,7 +76,9 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase storeSnapshotDownloadStatusSearch; private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqKVMCheckpointNotNull; private SearchBuilder searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore; - private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq; + private SearchBuilder searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq; + + private SearchBuilder searchFilteringVolumeIdAndStateAndCreatedAfter; private SearchBuilder searchBySnapshotId; @@ -146,10 +149,15 @@ public boolean configure(String name, Map params) throws Configu stateSearch.done(); - idStateNeqSearch = createSearchBuilder(); - idStateNeqSearch.and(SNAPSHOT_ID, idStateNeqSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); - idStateNeqSearch.and(STATE, idStateNeqSearch.entity().getState(), SearchCriteria.Op.NEQ); - idStateNeqSearch.done(); + idStateNinSearch = createSearchBuilder(); + idStateNinSearch.and(SNAPSHOT_ID, idStateNinSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); + idStateNinSearch.and(STATE, idStateNinSearch.entity().getState(), SearchCriteria.Op.NOTIN); + idStateNinSearch.done(); + + idEqRoleEqStateInSearch = createSearchBuilder(); + idEqRoleEqStateInSearch.and(SNAPSHOT_ID, idEqRoleEqStateInSearch.entity().getSnapshotId(), SearchCriteria.Op.EQ); + idEqRoleEqStateInSearch.and(STORE_ROLE, idEqRoleEqStateInSearch.entity().getRole(), SearchCriteria.Op.EQ); + idEqRoleEqStateInSearch.and(STATE, idEqRoleEqStateInSearch.entity().getState(), SearchCriteria.Op.IN); snapshotVOSearch = snapshotDao.createSearchBuilder(); snapshotVOSearch.and(VOLUME_ID, snapshotVOSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); @@ -192,17 +200,24 @@ public boolean configure(String name, Map params) throws Configu searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.and(URL_CREATED_BEFORE, searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.entity().getExtractUrlCreated(), SearchCriteria.Op.LT); searchFilterStateAndDownloadUrlNotNullAndDownloadUrlCreatedBefore.done(); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq = createSearchBuilder(); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STATE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getState(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(VOLUME_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getVolumeId(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ROLE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getRole(), SearchCriteria.Op.EQ); - searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.and(STORE_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.entity().getDataStoreId(), SearchCriteria.Op.IN); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq = createSearchBuilder(); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STATE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getState(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(VOLUME_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getVolumeId(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STORE_ROLE, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getRole(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(STORE_ID, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getDataStoreId(), SearchCriteria.Op.IN); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.and(INSTALL_PATH, searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.entity().getInstallPath(), SearchCriteria.Op.EQ); + searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.done(); searchBySnapshotId = createSearchBuilder(); searchBySnapshotId.and(SNAPSHOT_ID, searchBySnapshotId.entity().getSnapshotId(), SearchCriteria.Op.EQ); searchBySnapshotId.and(STATE, searchBySnapshotId.entity().getState(), SearchCriteria.Op.EQ); searchBySnapshotId.done(); + searchFilteringVolumeIdAndStateAndCreatedAfter = createSearchBuilder(); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(STATE, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getState(), SearchCriteria.Op.EQ); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(VOLUME_ID, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getVolumeId(), SearchCriteria.Op.EQ); + searchFilteringVolumeIdAndStateAndCreatedAfter.and(CREATED, searchFilteringVolumeIdAndStateAndCreatedAfter.entity().getCreated(), SearchCriteria.Op.GT); + searchFilteringVolumeIdAndStateAndCreatedAfter.done(); return true; } @@ -350,7 +365,7 @@ public SnapshotDataStoreVO findParent(DataStoreRole role, Long storeId, Long zon if (kvmIncrementalSnapshot && Hypervisor.HypervisorType.KVM.equals(hypervisorType)) { sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqKVMCheckpointNotNull.create(); } else { - sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEq.create(); + sc = searchFilteringStoreIdInVolumeIdEqStoreRoleEqStateEqPathEq.create(); } sc.setParameters(VOLUME_ID, volumeId); @@ -387,6 +402,15 @@ public SnapshotDataStoreVO findBySnapshotIdAndDataStoreRoleAndState(long snapsho return findOneBy(sc); } + @Override + public List listBySnapshotIdAndDataStoreRoleAndStateIn(long snapshotId, DataStoreRole role, State... state) { + SearchCriteria sc = idEqRoleEqStateInSearch.create(); + sc.setParameters(SNAPSHOT_ID, snapshotId); + sc.setParameters(STORE_ROLE, role); + sc.setParameters(STATE, (Object[])state); + return listBy(sc); + } + @Override public SnapshotDataStoreVO findOneBySnapshotId(long snapshotId, long zoneId) { try (TransactionLegacy transactionLegacy = TransactionLegacy.currentTxn()) { @@ -454,6 +478,12 @@ public SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStore return findOneBy(sc); } + @Override + public void expungeBySnapshotIdAndStoreRole(long snapshotId, DataStoreRole role) { + SearchCriteria sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); + expunge(sc); + } + @Override public List listAllByVolumeAndDataStore(long volumeId, DataStoreRole role) { SearchCriteria sc = searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq.create(); @@ -480,7 +510,7 @@ public List findBySnapshotId(long snapshotId) { @Override public List findBySnapshotIdWithNonDestroyedState(long snapshotId) { - SearchCriteria sc = idStateNeqSearch.create(); + SearchCriteria sc = idStateNinSearch.create(); sc.setParameters(SNAPSHOT_ID, snapshotId); sc.setParameters(STATE, State.Destroyed.name()); return listBy(sc); @@ -488,7 +518,7 @@ public List findBySnapshotIdWithNonDestroyedState(long snap @Override public List findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId) { - SearchCriteria sc = idStateNeqSearch.create(); + SearchCriteria sc = idStateNinSearch.create(); sc.setParameters(SNAPSHOT_ID, snapshotId); sc.setParameters(STATE, State.Destroyed.name(), State.Hidden.name()); return listBy(sc); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java index 4735202a7623..82ba9445b25c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java @@ -29,4 +29,6 @@ public interface SharedFSDao extends GenericDao, StateDao listSharedFSToBeDestroyed(Date date); SharedFSVO findSharedFSByNameAccountDomain(String name, Long accountId, Long domainId); + + SharedFSVO findByVm(long vmId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java index da6220716715..dd23787f982f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java @@ -114,4 +114,14 @@ public SharedFSVO findSharedFSByNameAccountDomain(String name, Long accountId, L sc.setParameters("domainId", domainId); return findOneBy(sc); } + + @Override + public SharedFSVO findByVm(long vmId) { + SearchBuilder sb = createSearchBuilder(); + sb.and("vmId", sb.entity().getVmId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("vmId", vmId); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDaoImpl.java deleted file mode 100644 index db8c0c3068f8..000000000000 --- a/engine/schema/src/main/java/org/apache/cloudstack/vm/schedule/dao/VMScheduleDaoImpl.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.cloudstack.vm.schedule.dao; - -import com.cloud.utils.Pair; -import com.cloud.utils.db.Filter; -import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.vm.schedule.VMSchedule; -import org.apache.cloudstack.vm.schedule.VMScheduleVO; -import org.springframework.stereotype.Component; - -import java.util.Date; -import java.util.List; - -@Component -public class VMScheduleDaoImpl extends GenericDaoBase implements VMScheduleDao { - - private final SearchBuilder activeScheduleSearch; - - private final SearchBuilder scheduleSearchByVmIdAndIds; - - private final SearchBuilder scheduleSearch; - - public VMScheduleDaoImpl() { - super(); - activeScheduleSearch = createSearchBuilder(); - activeScheduleSearch.and(ApiConstants.ENABLED, activeScheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ); - activeScheduleSearch.and().op(activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.NULL); - activeScheduleSearch.or(ApiConstants.END_DATE, activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.GT); - activeScheduleSearch.cp(); - activeScheduleSearch.done(); - - scheduleSearchByVmIdAndIds = createSearchBuilder(); - scheduleSearchByVmIdAndIds.and(ApiConstants.ID, scheduleSearchByVmIdAndIds.entity().getId(), SearchCriteria.Op.IN); - scheduleSearchByVmIdAndIds.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearchByVmIdAndIds.entity().getVmId(), SearchCriteria.Op.EQ); - scheduleSearchByVmIdAndIds.done(); - - scheduleSearch = createSearchBuilder(); - scheduleSearch.and(ApiConstants.ID, scheduleSearch.entity().getId(), SearchCriteria.Op.EQ); - scheduleSearch.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearch.entity().getVmId(), SearchCriteria.Op.EQ); - scheduleSearch.and(ApiConstants.ACTION, scheduleSearch.entity().getAction(), SearchCriteria.Op.EQ); - scheduleSearch.and(ApiConstants.ENABLED, scheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ); - scheduleSearch.done(); - - } - - @Override - public List listAllActiveSchedules() { - // WHERE enabled = true AND (end_date IS NULL OR end_date > current_date) - SearchCriteria sc = activeScheduleSearch.create(); - sc.setParameters(ApiConstants.ENABLED, true); - sc.setParameters(ApiConstants.END_DATE, new Date()); - return search(sc, null); - } - - @Override - public long removeSchedulesForVmIdAndIds(Long vmId, List ids) { - SearchCriteria sc = scheduleSearchByVmIdAndIds.create(); - sc.setParameters(ApiConstants.ID, ids.toArray()); - sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId); - return remove(sc); - } - - @Override - public Pair, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit) { - SearchCriteria sc = scheduleSearch.create(); - - if (id != null) { - sc.setParameters(ApiConstants.ID, id); - } - if (enabled != null) { - sc.setParameters(ApiConstants.ENABLED, enabled); - } - if (action != null) { - sc.setParameters(ApiConstants.ACTION, action); - } - sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId); - - Filter filter = new Filter(VMScheduleVO.class, ApiConstants.ID, false, offset, limit); - return searchAndCount(sc, filter); - } - - @Override - public SearchCriteria getSearchCriteriaForVMId(Long vmId) { - SearchCriteria sc = scheduleSearch.create(); - sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId); - return sc; - } -} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index 1846c3c62a0e..a0dada545617 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -46,6 +46,7 @@ + @@ -73,5 +74,7 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 0656d5e3c440..932db538f30b 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -108,6 +108,7 @@ + @@ -116,6 +117,7 @@ + @@ -236,13 +238,11 @@ - - @@ -272,7 +272,12 @@ + + + + + @@ -284,8 +289,9 @@ - - + + + @@ -310,4 +316,18 @@ + + + + + + + + + + + + + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql index 3dd6c18f57c5..4405250f2711 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql @@ -31,7 +31,7 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); -- Create a new group for Usage Server related configurations -INSERT INTO `cloud`.`configuration_group` (`name`, `description`, `precedence`) VALUES ('Usage Server', 'Usage Server related configuration', 9); +INSERT IGNORE INTO `cloud`.`configuration_group` (`name`, `description`, `precedence`) VALUES ('Usage Server', 'Usage Server related configuration', 9); UPDATE `cloud`.`configuration_subgroup` set `group_id` = (SELECT `id` FROM `cloud`.`configuration_group` WHERE `name` = 'Usage Server'), `precedence` = 1 WHERE `name`='Usage'; UPDATE `cloud`.`configuration` SET `group_id` = (SELECT `id` FROM `cloud`.`configuration_group` WHERE `name` = 'Usage Server') where `subgroup_id` = (SELECT `id` FROM `cloud`.`configuration_subgroup` WHERE `name` = 'Usage'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql similarity index 93% rename from engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql rename to engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql index 5f257f2965bd..b63e918b389b 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100-cleanup.sql @@ -16,5 +16,5 @@ -- under the License. --; --- Schema upgrade cleanup from 4.20.1.0 to 4.21.0.0 +-- Schema upgrade cleanup from 4.20.4.0 to 4.21.0.0 --; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql similarity index 97% rename from engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql rename to engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql index 4c65f37e0fe0..bf7f3aeaf0f8 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42040to42100.sql @@ -16,7 +16,7 @@ -- under the License. --; --- Schema upgrade from 4.20.1.0 to 4.21.0.0 +-- Schema upgrade from 4.20.4.0 to 4.21.0.0 --; CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'max_backups', 'INT(8) UNSIGNED NOT NULL DEFAULT 0 COMMENT ''Maximum number of backups to be retained'''); @@ -687,22 +687,23 @@ CREATE TABLE IF NOT EXISTS `cloud`.`backup_details` ( UPDATE `cloud`.`backups` b INNER JOIN `cloud`.`vm_instance` vm ON b.vm_id = vm.id SET b.backed_volumes = ( - SELECT CONCAT("[", - GROUP_CONCAT( - CONCAT( - "{\"uuid\":\"", v.uuid, "\",", - "\"type\":\"", v.volume_type, "\",", - "\"size\":", v.`size`, ",", - "\"path\":\"", IFNULL(v.path, 'null'), "\",", - "\"deviceId\":", IFNULL(v.device_id, 'null'), ",", - "\"diskOfferingId\":\"", doff.uuid, "\",", - "\"minIops\":", IFNULL(v.min_iops, 'null'), ",", - "\"maxIops\":", IFNULL(v.max_iops, 'null'), - "}" - ) - SEPARATOR "," + SELECT COALESCE( + CAST( + JSON_ARRAYAGG( + JSON_OBJECT( + 'uuid', v.uuid, + 'type', v.volume_type, + 'size', v.size, + 'path', v.path, + 'deviceId', v.device_id, + 'diskOfferingId', doff.uuid, + 'minIops', v.min_iops, + 'maxIops', v.max_iops + ) + ) AS CHAR ), - "]") + '[]' + ) FROM `cloud`.`volumes` v LEFT JOIN `cloud`.`disk_offering` doff ON v.disk_offering_id = doff.id WHERE v.instance_id = vm.id @@ -711,22 +712,23 @@ SET b.backed_volumes = ( -- Add diskOfferingId, deviceId, minIops and maxIops to backup_volumes in vm_instance table UPDATE `cloud`.`vm_instance` vm SET vm.backup_volumes = ( - SELECT CONCAT("[", - GROUP_CONCAT( - CONCAT( - "{\"uuid\":\"", v.uuid, "\",", - "\"type\":\"", v.volume_type, "\",", - "\"size\":", v.`size`, ",", - "\"path\":\"", IFNULL(v.path, 'null'), "\",", - "\"deviceId\":", IFNULL(v.device_id, 'null'), ",", - "\"diskOfferingId\":\"", doff.uuid, "\",", - "\"minIops\":", IFNULL(v.min_iops, 'null'), ",", - "\"maxIops\":", IFNULL(v.max_iops, 'null'), - "}" - ) - SEPARATOR "," + SELECT COALESCE( + CAST( + JSON_ARRAYAGG( + JSON_OBJECT( + 'uuid', v.uuid, + 'type', v.volume_type, + 'size', v.size, + 'path', v.path, + 'deviceId', v.device_id, + 'diskOfferingId', doff.uuid, + 'minIops', v.min_iops, + 'maxIops', v.max_iops + ) + ) AS CHAR ), - "]") + '[]' + ) FROM `cloud`.`volumes` v LEFT JOIN `cloud`.`disk_offering` doff ON v.disk_offering_id = doff.id WHERE v.instance_id = vm.id diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index 858c46a7c1ee..fbb2fd079f9c 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -19,7 +19,6 @@ -- Schema upgrade from 4.21.0.0 to 4.22.0.0 --; - -- health check status as enum CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('router_health_check', 'check_result', 'check_result', 'varchar(16) NOT NULL COMMENT "check executions result: SUCCESS, FAILURE, WARNING, UNKNOWN"'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql index 54baf226ac43..2f104568c140 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210-cleanup.sql @@ -18,3 +18,9 @@ --; -- Schema upgrade cleanup from 4.22.0.0 to 4.22.1.0 --; + +-- Entries remaining on `cloud`.`resource_reservation` during the upgrade process are stale, so delete them. +-- This script was added to normalize volume/primary storage reservations that got stuck due to a bug on VM deployment, +-- but it is more interesting to introduce a smarter logic to clean these stale reservations in the future without the need +-- for upgrades (for instance, by having a heartbeat_time column for the reservations and automatically cleaning old entries). +DELETE FROM `cloud`.`resource_reservation`; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql index a8a3d3f7bd4f..baa20e9f0ad5 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql @@ -33,3 +33,29 @@ UPDATE `cloud`.`alert` SET type = 34 WHERE name = 'ALERT.VR.PRIVATE.IFACE.MTU'; -- Update configuration 'kvm.ssh.to.agent' description and is_dynamic fields UPDATE `cloud`.`configuration` SET description = 'True if the management server will restart the agent service via SSH into the KVM hosts after or during maintenance operations', is_dynamic = 1 WHERE name = 'kvm.ssh.to.agent'; + +-- Sanitize legacy network-level addressing fields for Public networks +UPDATE `cloud`.`networks` +SET `broadcast_uri` = NULL, + `gateway` = NULL, + `cidr` = NULL, + `ip6_gateway` = NULL, + `ip6_cidr` = NULL +WHERE `traffic_type` = 'Public'; + +UPDATE `cloud`.`vm_template` SET guest_os_id = 99 WHERE name = 'kvm-default-vm-import-dummy-template'; + +-- Update existing vm_template records with NULL type to "USER" +UPDATE `cloud`.`vm_template` SET `type` = 'USER' WHERE `type` IS NULL; + +-- remove unused config item +DELETE FROM `cloud`.`configuration` WHERE name = 'consoleproxy.cmd.port'; + +-- Drops the unused "backup_interval_type" column of the "cloud.backups" table +ALTER TABLE `cloud`.`backups` DROP COLUMN `backup_interval_type`; + +-- Update `user.password.reset.mail.template` configuration value to match new logic +UPDATE `cloud`.`configuration` +SET value = CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team') +WHERE name = 'user.password.reset.mail.template' + AND value IN (CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', 'http://{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team'), CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{domainUrl}}}{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team')); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index d330ecd0c0d5..ab5ac7b2b875 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -19,6 +19,12 @@ -- Schema upgrade from 4.22.1.0 to 4.23.0.0 --; +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider', 'domain_id', 'bigint unsigned DEFAULT NULL COMMENT "NULL for global provider, domain ID for domain-specific" AFTER `redirect_uri`'); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.oauth_provider', 'fk_oauth_provider__domain_id', '(`domain_id`)', '`domain`(`id`)'); +CALL `cloud`.`IDEMPOTENT_ADD_KEY`('i_oauth_provider__domain_id', 'cloud.oauth_provider', '(`domain_id`)'); + +CALL `cloud`.`IDEMPOTENT_ADD_UNIQUE_KEY`('cloud.oauth_provider', 'uk_oauth_provider__provider_domain', '(`provider`, `domain_id`)'); + CREATE TABLE `cloud`.`backup_offering_details` ( `id` bigint unsigned NOT NULL auto_increment, `backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id', @@ -34,6 +40,32 @@ CREATE TABLE `cloud`.`backup_offering_details` ( UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random'; UPDATE `cloud`.`configuration` SET value='firstfit' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_firstfit'; +-- Add Windows Server 2025 guest OS and mappings +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.1.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.2.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '7.0.3.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.1', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.2', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.0.3', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.1', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.2', 'windows2022srvNext_64Guest'); +CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025 (64-bit)', 'VMware', '8.0.3', 'windows2022srvNext_64Guest'); + +-- Create kubernetes_cluster_affinity_group_map table for CKS per-node-type affinity groups +CREATE TABLE IF NOT EXISTS `cloud`.`kubernetes_cluster_affinity_group_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `cluster_id` bigint unsigned NOT NULL COMMENT 'kubernetes cluster id', + `node_type` varchar(32) NOT NULL COMMENT 'CONTROL, WORKER, or ETCD', + `affinity_group_id` bigint unsigned NOT NULL COMMENT 'affinity group id', + PRIMARY KEY (`id`), + CONSTRAINT `fk_kubernetes_cluster_ag_map__cluster_id` FOREIGN KEY (`cluster_id`) REFERENCES `kubernetes_cluster`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kubernetes_cluster_ag_map__ag_id` FOREIGN KEY (`affinity_group_id`) REFERENCES `affinity_group`(`id`) ON DELETE CASCADE, + INDEX `i_kubernetes_cluster_ag_map__cluster_id`(`cluster_id`), + INDEX `i_kubernetes_cluster_ag_map__ag_id`(`affinity_group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- Create webhook_filter table DROP TABLE IF EXISTS `cloud`.`webhook_filter`; CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( @@ -49,3 +81,568 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( INDEX `i_webhook_filter__webhook_id`(`webhook_id`), CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- "api_keypair" table for API and secret keys +CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(40) UNIQUE NOT NULL, + `name` varchar(255) NOT NULL, + `domain_id` bigint(20) unsigned NOT NULL, + `account_id` bigint(20) unsigned NOT NULL, + `user_id` bigint(20) unsigned NOT NULL, + `start_date` datetime, + `end_date` datetime, + `description` varchar(1024), + `api_key` varchar(255) NOT NULL, + `secret_key` varchar(255) NOT NULL, + `created` datetime NOT NULL, + `removed` datetime, + PRIMARY KEY (`id`), + CONSTRAINT `fk_api_keypair__user_id` FOREIGN KEY(`user_id`) REFERENCES `cloud`.`user`(`id`), + CONSTRAINT `fk_api_keypair__account_id` FOREIGN KEY(`account_id`) REFERENCES `cloud`.`account`(`id`), + CONSTRAINT `fk_api_keypair__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `cloud`.`domain`(`id`) +); + +-- "api_keypair_permissions" table for API key pairs permissions +CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair_permissions` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(40) UNIQUE, + `sort_order` bigint(20) unsigned NOT NULL DEFAULT 0, + `rule` varchar(255) NOT NULL, + `api_keypair_id` bigint(20) unsigned NOT NULL, + `permission` varchar(255) NOT NULL, + `description` varchar(255), + PRIMARY KEY (`id`), + CONSTRAINT `fk_keypair_permissions__api_keypair_id` FOREIGN KEY(`api_keypair_id`) REFERENCES `cloud`.`api_keypair`(`id`) +); + +-- Populate "api_keypair" table with existing user API keys +INSERT INTO `cloud`.`api_keypair` (uuid, user_id, domain_id, account_id, api_key, secret_key, created, name) +SELECT UUID(), user.id, account.domain_id, account.id, user.api_key, user.secret_key, NOW(), 'Active key pair' +FROM `cloud`.`user` AS user +JOIN `cloud`.`account` AS account ON user.account_id = account.id +WHERE user.api_key IS NOT NULL AND user.secret_key IS NOT NULL; + +-- Drop API keys from user table +ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key; + +-- Grant access to the "deleteUserKeys" and "listUserKeyRules" APIs to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'deleteUserKeys', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'deleteUserKeys', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKeys', 'ALLOW'); + +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'listUserKeyRules', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'listUserKeyRules', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'listUserKeyRules', 'ALLOW'); + +-- Add conserve mode for VPC offerings +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' '); + +-- KMS HSM Profiles (Generic table for PKCS#11, KMIP, etc.) +-- Scoped to account (user-provided) or global/zone (admin-provided) +CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profiles` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` VARCHAR(40) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `protocol` VARCHAR(32) NOT NULL COMMENT 'Protocol for the HSM Profile', + + -- Scoping + `account_id` BIGINT UNSIGNED COMMENT 'null = admin-provided (available to all accounts)', + `domain_id` BIGINT UNSIGNED COMMENT 'null = zone/global scope', + `zone_id` BIGINT UNSIGNED COMMENT 'null = global scope', + + -- Metadata + `vendor_name` VARCHAR(64) COMMENT 'HSM vendor', + `enabled` BOOLEAN NOT NULL DEFAULT TRUE, + `is_public` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Public profile. Available to all accounts', + `created` DATETIME NOT NULL, + `removed` DATETIME, + + PRIMARY KEY (`id`), + UNIQUE KEY `uk_uuid` (`uuid`), + UNIQUE KEY `uk_account_name` (`account_id`, `name`, `removed`), + INDEX `idx_protocol_enabled` (`protocol`, `enabled`, `removed`), + INDEX `idx_scoping` (`account_id`, `domain_id`, `zone_id`, `removed`), + CONSTRAINT `fk_kms_hsm_profiles__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_hsm_profiles__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_hsm_profiles__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='HSM profiles for KMS providers'; + +-- KMS HSM Profile Details (Protocol-specific configuration) +CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profile_details` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `profile_id` BIGINT UNSIGNED NOT NULL COMMENT 'HSM profile ID', + `name` VARCHAR(255) NOT NULL COMMENT 'Config key (e.g. library_path, endpoint, pin, cert_content)', + `value` TEXT NOT NULL COMMENT 'Config value (encrypted if sensitive)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_profile_name` (`profile_id`, `name`), + CONSTRAINT `fk_kms_hsm_profile_details__profile_id` FOREIGN KEY (`profile_id`) REFERENCES `kms_hsm_profiles`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Details for HSM profiles (key-value configuration)'; + +-- KMS Keys (Key Encryption Key Metadata) +-- Account-scoped KEKs for envelope encryption +CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID - user-facing identifier', + `name` VARCHAR(255) NOT NULL COMMENT 'User-friendly name', + `description` VARCHAR(1024) COMMENT 'User description', + `kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID', + `purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT)', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning account', + `domain_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning domain', + `zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone where key is valid', + `algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm', + `key_bits` INT NOT NULL DEFAULT 256 COMMENT 'Key size in bits', + `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether the key is enabled for new cryptographic operations', + `hsm_profile_id` BIGINT UNSIGNED NOT NULL COMMENT 'Current HSM profile ID for this key', + `created` DATETIME NOT NULL COMMENT 'Creation timestamp', + `removed` DATETIME COMMENT 'Removal timestamp for soft delete', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_uuid` (`uuid`), + INDEX `idx_account_purpose` (`account_id`, `purpose`, `enabled`), + INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `enabled`), + INDEX `idx_zone_enabled` (`zone_id`, `enabled`), + CONSTRAINT `fk_kms_keys__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_keys__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_keys__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_keys__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Key (KEK) metadata - account-scoped keys for envelope encryption'; + +-- KMS KEK Versions (multiple KEKs per KMS key for gradual rotation) +-- Supports multiple KEK versions per logical KMS key during rotation +CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID', + `kms_key_id` BIGINT UNSIGNED NOT NULL COMMENT 'Reference to kms_keys table', + `version_number` INT NOT NULL COMMENT 'Version number (1, 2, 3, ...)', + `kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID for this version', + `status` VARCHAR(32) NOT NULL DEFAULT 'Active' COMMENT 'Active, Previous, Archived', + `hsm_profile_id` BIGINT UNSIGNED COMMENT 'HSM profile where this KEK version is stored', + `created` DATETIME NOT NULL COMMENT 'Creation timestamp', + `removed` DATETIME COMMENT 'Removal timestamp for soft delete', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_uuid` (`uuid`), + UNIQUE KEY `uk_kms_key_version` (`kms_key_id`, `version_number`, `removed`), + INDEX `idx_kms_key_status` (`kms_key_id`, `status`, `removed`), + INDEX `idx_kek_label` (`kek_label`), + CONSTRAINT `fk_kms_kek_versions__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_kek_versions__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KEK versions for a KMS key - supports gradual rotation'; + +-- KMS Wrapped Keys (Data Encryption Keys) +-- Generic table for wrapped DEKs - references kms_keys for metadata and kek_versions for specific KEK version +CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID', + `kms_key_id` BIGINT UNSIGNED COMMENT 'Reference to kms_keys table', + `kek_version_id` BIGINT UNSIGNED COMMENT 'Reference to kms_kek_versions table', + `zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone ID for zone-scoped keys', + `wrapped_blob` VARBINARY(4096) NOT NULL COMMENT 'Encrypted DEK material', + `created` DATETIME NOT NULL COMMENT 'Creation timestamp', + `removed` DATETIME COMMENT 'Removal timestamp for soft delete', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_uuid` (`uuid`), + INDEX `idx_kms_key_id` (`kms_key_id`, `removed`), + INDEX `idx_kek_version_id` (`kek_version_id`, `removed`), + INDEX `idx_zone_id` (`zone_id`, `removed`), + CONSTRAINT `fk_kms_wrapped_key__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_wrapped_key__kek_version_id` FOREIGN KEY (`kek_version_id`) REFERENCES `kms_kek_versions`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_kms_wrapped_key__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS wrapped encryption keys (DEKs) - references kms_keys for KEK metadata and kek_versions for specific version'; + +-- Add KMS key reference to volumes table (which KMS key was used) +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_key_id', 'BIGINT UNSIGNED COMMENT ''KMS key ID used for volume encryption'''); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_key_id', '(kms_key_id)', '`kms_keys`(`id`)'); + +-- Add KMS wrapped key reference to volumes table +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_wrapped_key_id', 'BIGINT UNSIGNED COMMENT ''KMS wrapped key ID for volume encryption'''); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_wrapped_key_id', '(kms_wrapped_key_id)', '`kms_wrapped_key`(`id`)'); + +-- KMS Database Provider KEK Objects (PKCS#11-like object storage) +-- Stores KEKs for the database KMS provider in a PKCS#11-compatible format +CREATE TABLE IF NOT EXISTS `cloud`.`kms_database_kek_objects` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Object handle (PKCS#11 CKA_HANDLE)', + `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID', + -- PKCS#11 Object Class (CKA_CLASS) + `object_class` VARCHAR(32) NOT NULL DEFAULT 'CKO_SECRET_KEY' COMMENT 'PKCS#11 object class (CKO_SECRET_KEY, CKO_PRIVATE_KEY, etc.)', + -- PKCS#11 Label (CKA_LABEL) - human-readable identifier + `label` VARCHAR(255) NOT NULL COMMENT 'PKCS#11 label (CKA_LABEL) - human-readable identifier', + -- PKCS#11 ID (CKA_ID) - application-defined identifier + `object_id` VARBINARY(64) COMMENT 'PKCS#11 object ID (CKA_ID) - application-defined identifier', + -- Key Type (CKA_KEY_TYPE) + `key_type` VARCHAR(32) NOT NULL DEFAULT 'CKK_AES' COMMENT 'PKCS#11 key type (CKK_AES, CKK_RSA, etc.)', + -- Key Material (CKA_VALUE) - encrypted KEK material + `key_material` VARBINARY(512) NOT NULL COMMENT 'PKCS#11 key value (CKA_VALUE) - encrypted KEK material', + -- Key Attributes (PKCS#11 boolean attributes) + `is_sensitive` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_SENSITIVE - key material is sensitive', + `is_extractable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_EXTRACTABLE - key can be extracted', + `is_token` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_TOKEN - object is on token (persistent)', + `is_private` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_PRIVATE - object is private', + `is_modifiable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_MODIFIABLE - object can be modified', + `is_copyable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_COPYABLE - object can be copied', + `is_destroyable` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_DESTROYABLE - object can be destroyed', + `always_sensitive` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_ALWAYS_SENSITIVE - key was always sensitive', + `never_extractable` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_NEVER_EXTRACTABLE - key was never extractable', + -- Key Metadata + `purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT)', + `key_bits` INT NOT NULL COMMENT 'Key size in bits (128, 192, 256)', + `algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm', + -- Validity Dates (PKCS#11 CKA_START_DATE, CKA_END_DATE) + `start_date` DATETIME COMMENT 'PKCS#11 CKA_START_DATE - key validity start', + `end_date` DATETIME COMMENT 'PKCS#11 CKA_END_DATE - key validity end', + -- Lifecycle + `created` DATETIME NOT NULL COMMENT 'Creation timestamp', + `last_used` DATETIME COMMENT 'Last usage timestamp', + `removed` DATETIME COMMENT 'Removal timestamp for soft delete', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_uuid` (`uuid`), + UNIQUE KEY `uk_label_removed` (`label`, `removed`), + INDEX `idx_purpose_removed` (`purpose`, `removed`), + INDEX `idx_key_type` (`key_type`, `removed`), + INDEX `idx_object_class` (`object_class`, `removed`), + INDEX `idx_created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Database Provider KEK Objects - PKCS#11-like object storage for KEKs'; + +--- Disable/enable NICs +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' '); + +--- Add URLs for OAuth provider +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','authorize_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Authorize URL for OAuth initialization'' '); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','token_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Token URL for OAuth finalization'' '); + +--- Quota tariff/usage mapping +CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tariff_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the tariff of the Quota usage detail calculated, foreign key to quota_tariff table', + `quota_usage_id` bigint(20) unsigned NOT NULL COMMENT 'ID of the aggregation of Quota usage details, foreign key to quota_usage table', + `quota_used` decimal(20,8) NOT NULL COMMENT 'Amount of quota used', + PRIMARY KEY (`id`), + CONSTRAINT `fk_quota_tariff_usage__tariff_id` FOREIGN KEY (`tariff_id`) REFERENCES `cloud_usage`.`quota_tariff` (`id`), + CONSTRAINT `fk_quota_tariff_usage__quota_usage_id` FOREIGN KEY (`quota_usage_id`) REFERENCES `cloud_usage`.`quota_usage` (`id`)); + +--- Per-VM ISO attachments. user_vm.iso_id remains as the primary/bootable ISO pointer. +CREATE TABLE IF NOT EXISTS `cloud`.`vm_iso_map` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `vm_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to user_vm', + `iso_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to vm_template (ISOs are templates of format ISO)', + `device_seq` int(10) unsigned NOT NULL COMMENT 'cdrom slot index used to derive the libvirt device label (3=hdc, 4=hdd)', + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uc_vm_iso_map__vm_iso` (`vm_id`, `iso_id`), + UNIQUE KEY `uc_vm_iso_map__vm_seq` (`vm_id`, `device_seq`), + CONSTRAINT `fk_vm_iso_map__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `cloud`.`user_vm` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_iso_map__iso_id` FOREIGN KEY (`iso_id`) REFERENCES `cloud`.`vm_template` (`id`) +); + +-- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1'); + +-- Creates the 'kvm.memory.dynamic.scaling.capacity' and, for already active ACS environments, +-- initializes it with the value of the setting 'vm.serviceoffering.ram.size.max' +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `default_value`, `updated`, `scope`, `is_dynamic`, `group_id`, `subgroup_id`, `display_text`, `description`) +SELECT 'Advanced', 'DEFAULT', 'CapacityManager', 'kvm.memory.dynamic.scaling.capacity', `cfg`.`value`, 0, NULL, 4, 1, 6, 27, + 'KVM memory dynamic scaling capacity', 'Defines the maximum memory capacity in MiB for which VMs can be dynamically scaled to with KVM. The ''kvm.memory.dynamic.scaling.capacity'' setting''s value will be used to define the value of the '''' element of domain XMLs. If it is set to a value less than or equal to ''0'', then the host''s memory capacity will be considered.' +FROM `cloud`.`configuration` `cfg` +WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.memory.dynamic.scaling.capacity') + AND `cfg`.`name` = 'vm.serviceoffering.ram.size.max'; + +-- Creates the 'kvm.cpu.dynamic.scaling.capacity' and, for already active ACS environments, +-- initializes it with the value of the setting 'vm.serviceoffering.cpu.cores.max' +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `default_value`, `updated`, `scope`, `is_dynamic`, `group_id`, `subgroup_id`, `display_text`, `description`) +SELECT 'Advanced', 'DEFAULT', 'CapacityManager', 'kvm.cpu.dynamic.scaling.capacity', `cfg`.`value`, 0, NULL, 4, 1, 6, 27, + 'KVM CPU dynamic scaling capacity', 'Defines the maximum vCPU capacity for which VMs can be dynamically scaled to with KVM. The ''kvm.cpu.dynamic.scaling.capacity'' setting''s value will be used to define the value of the '''' element of domain XMLs. If it is set to a value less than or equal to ''0'', then the host''s CPU cores capacity will be considered.' +FROM `cloud`.`configuration` `cfg` +WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.cpu.dynamic.scaling.capacity') + AND `cfg`.`name` = 'vm.serviceoffering.cpu.cores.max'; + +-- Generalise VM schedule tables into resource-agnostic resource_schedule / resource_scheduled_job. +-- Step 1: rename vm_schedule → resource_schedule, rename vm_id → resource_id, add resource_type column. +ALTER TABLE `cloud`.`vm_schedule` + DROP FOREIGN KEY `fk_vm_schedule__vm_id`, + DROP INDEX `i_vm_schedule__vm_id`, + DROP INDEX `i_vm_schedule__enabled_end_date`, + CHANGE COLUMN `vm_id` `resource_id` bigint unsigned NOT NULL COMMENT 'id of the scheduled resource', + ADD COLUMN `resource_type` varchar(64) NOT NULL DEFAULT 'VirtualMachine' COMMENT 'type of the scheduled resource' AFTER `uuid`; + +RENAME TABLE `cloud`.`vm_schedule` TO `cloud`.`resource_schedule`; + +ALTER TABLE `cloud`.`resource_schedule` + ADD INDEX `i_resource_schedule__resource` (`resource_type`, `resource_id`), + ADD INDEX `i_resource_schedule__enabled_end_date` (`enabled`, `end_date`); + +-- Step 2: rename vm_scheduled_job → resource_scheduled_job, rename columns. +ALTER TABLE `cloud`.`vm_scheduled_job` + DROP FOREIGN KEY `fk_vm_scheduled_job__vm_id`, + DROP FOREIGN KEY `fk_vm_scheduled_job__vm_schedule_id`, + DROP INDEX `i_vm_scheduled_job__vm_id`, + DROP INDEX `i_vm_scheduled_job__scheduled_timestamp`, + DROP INDEX `vm_schedule_id`, + CHANGE COLUMN `vm_id` `resource_id` bigint unsigned NOT NULL COMMENT 'id of the scheduled resource', + CHANGE COLUMN `vm_schedule_id` `schedule_id` bigint unsigned NOT NULL COMMENT 'id of the resource_schedule row', + ADD COLUMN `resource_type` varchar(64) NOT NULL DEFAULT 'VirtualMachine' COMMENT 'type of the scheduled resource' AFTER `uuid`; + +RENAME TABLE `cloud`.`vm_scheduled_job` TO `cloud`.`resource_scheduled_job`; + +ALTER TABLE `cloud`.`resource_scheduled_job` + ADD UNIQUE KEY `uc_resource_scheduled_job__schedule_timestamp` (`schedule_id`, `scheduled_timestamp`), + ADD INDEX `i_resource_scheduled_job__resource` (`resource_type`, `resource_id`), + ADD INDEX `i_resource_scheduled_job__scheduled_timestamp` (`scheduled_timestamp`), + ADD CONSTRAINT `fk_resource_scheduled_job__schedule_id` FOREIGN KEY (`schedule_id`) REFERENCES `resource_schedule`(`id`) ON DELETE CASCADE; + +-- Step 3: details table for action-specific parameters (used by the generic resource schedule API in Commit 2). +CREATE TABLE IF NOT EXISTS `cloud`.`resource_schedule_details` ( + `id` bigint unsigned NOT NULL auto_increment, + `schedule_id` bigint unsigned NOT NULL COMMENT 'id of the resource_schedule row', + `name` varchar(255) NOT NULL, + `value` varchar(1024) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'should this detail be visible to the end user', + PRIMARY KEY (`id`), + INDEX `i_resource_schedule_details__schedule_id` (`schedule_id`), + CONSTRAINT `fk_resource_schedule_details__schedule_id` FOREIGN KEY (`schedule_id`) REFERENCES `resource_schedule`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Step 4: rename CRUD event types from VM.SCHEDULE.{CREATE,UPDATE,DELETE} to the new generic SCHEDULE.{CREATE,UPDATE,DELETE}. +-- Action-execution events (VM.SCHEDULE.START, .STOP, .REBOOT, .FORCE_STOP, .FORCE_REBOOT) keep their existing names. +UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.CREATE' WHERE `type` = 'VM.SCHEDULE.CREATE'; +UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.UPDATE' WHERE `type` = 'VM.SCHEDULE.UPDATE'; +UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.DELETE' WHERE `type` = 'VM.SCHEDULE.DELETE'; + +-- Step 5: Rename the global configuration key for the scheduler +UPDATE `cloud`.`configuration` SET name='scheduler.jobs.expire.interval' WHERE name='vmscheduler.jobs.expire.interval'; + +-- Remove stale realhostip.com default values; domain has been dead since ~2015. +UPDATE `cloud`.`configuration` + SET value = NULL + WHERE name IN ('consoleproxy.url.domain', 'secstorage.ssl.cert.domain') + AND value IN ('realhostip.com', '*.realhostip.com'); + +-- Add management_server_details table to allow ManagementServer scope configs +CREATE TABLE IF NOT EXISTS `management_server_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `management_server_id` bigint unsigned NOT NULL COMMENT 'management server the detail is related to', + `name` varchar(255) NOT NULL COMMENT 'name of the detail', + `value` varchar(255) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_management_server_details__management_server_id` FOREIGN KEY `fk_management_server_details__management_server_id`(`management_server_id`) REFERENCES `mshost`(`id`) ON DELETE CASCADE, + KEY `i_management_server_details__name__value` (`name`(128),`value`(128)) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Add checkpoint tracking fields to backups table for incremental backup support +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'from_checkpoint_id', 'VARCHAR(255) DEFAULT NULL COMMENT "Previous active checkpoint id for incremental backups"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'to_checkpoint_id', 'VARCHAR(255) DEFAULT NULL COMMENT "New checkpoint id created for the next incremental backup"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'checkpoint_create_time', 'BIGINT DEFAULT NULL COMMENT "Checkpoint creation timestamp from libvirt"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'host_id', 'BIGINT UNSIGNED DEFAULT NULL COMMENT "Host where backup is running"'); + +-- Create image_transfer table for per-disk image transfers +CREATE TABLE IF NOT EXISTS `cloud`.`image_transfer`( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'uuid', + `account_id` bigint unsigned NOT NULL COMMENT 'Account ID', + `domain_id` bigint unsigned NOT NULL COMMENT 'Domain ID', + `data_center_id` bigint unsigned NOT NULL COMMENT 'Data Center ID', + `backup_id` bigint unsigned COMMENT 'Backup ID', + `volume_id` bigint unsigned NOT NULL COMMENT 'Volume ID', + `host_id` bigint unsigned NOT NULL COMMENT 'Host ID', + `transfer_url` varchar(255) COMMENT 'ImageIO transfer URL', + `file` varchar(255) COMMENT 'File for the file backend', + `phase` varchar(20) NOT NULL COMMENT 'Transfer phase: initializing, transferring, finished, failed', + `socket` varchar(255) COMMENT 'Unix socket for nbd backend', + `direction` varchar(20) NOT NULL COMMENT 'Direction: upload, download', + `backend` varchar(20) NOT NULL COMMENT 'Backend: nbd, file', + `progress` int COMMENT 'Transfer progress percentage (0-100)', + `signed_ticket_id` varchar(255) COMMENT 'Signed ticket ID from ImageIO', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + UNIQUE KEY `uuid` (`uuid`), + CONSTRAINT `fk_image_transfer__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_image_transfer__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_image_transfer__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + INDEX `i_image_transfer__backup_id`(`backup_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +--- Quota resource statement +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement'); + +-- Increase length of value of extension details from 255 to 4096 to support longer details value +CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_details', 'value', 'value', 'VARCHAR(4096)'); +CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.extension_resource_map_details', 'value', 'value', 'VARCHAR(4096)'); + +-- Add CustomAction service support to physical_network_service_providers +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.physical_network_service_providers', 'custom_action_service_provided', 'tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT "Is Custom Action service provided" AFTER `networkacl_service_provided`'); + +--- Gui theme login base domain +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.gui_themes', 'login_base_domain', 'TEXT DEFAULT NULL'); + +-- Add description for secondary IP addresses +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nic_secondary_ips', 'description', 'VARCHAR(2048) DEFAULT NULL'); + +--- Change nw_rate and mc_rate column types from smallint unsigned to int unsigned to support larger rate values +ALTER TABLE `cloud`.`service_offering` + MODIFY COLUMN `nw_rate` int unsigned DEFAULT 200 COMMENT 'network rate throttle mbits/s', + MODIFY COLUMN `mc_rate` int unsigned DEFAULT 10 COMMENT 'mcast rate throttle mbits/s'; + +ALTER TABLE `cloud`.`network_offerings` + MODIFY COLUMN `nw_rate` int unsigned COMMENT 'network rate throttle mbits/s', + MODIFY COLUMN `mc_rate` int unsigned COMMENT 'mcast rate throttle mbits/s'; + +-- Soft delete port forwarding, load balancing and firewall rules +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.firewall_rules', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.load_balancer_vm_map', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.load_balancer_cert_map', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.load_balancer_healthcheck_policies', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.load_balancer_stickiness_policies', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.global_load_balancer_lb_rule_map', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.elastic_lb_vm_map', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.tungsten_lb_health_monitor', 'removed', 'datetime DEFAULT NULL'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.global_load_balancing_rules', 'removed', 'datetime DEFAULT NULL'); + +ALTER TABLE `cloud`.`load_balancer_vm_map` +DROP KEY `load_balancer_id`, +ADD UNIQUE KEY `load_balancer_id` (`load_balancer_id`, `instance_id`, `instance_ip`, `removed`); + +ALTER TABLE `cloud`.`global_load_balancer_lb_rule_map` +DROP KEY `gslb_rule_id`, +ADD UNIQUE KEY `gslb_rule_id` (`gslb_rule_id`, `lb_rule_id`, `removed`); + +-- ====================================================================== +-- DNS Framework Schema +-- ====================================================================== + +-- DNS Server Table (Stores DNS Server Configurations) +CREATE TABLE IF NOT EXISTS `cloud`.`dns_server` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', + `uuid` varchar(40) COMMENT 'uuid of the dns server', + `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', + `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', + `url` varchar(1024) NOT NULL COMMENT 'dns server url', + `dns_username` varchar(255) COMMENT 'username or email for dns server credentials', + `dns_api_key` varchar(255) NOT NULL COMMENT 'api key or token for the dns server ', + `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', + `name_servers` varchar(1024) DEFAULT NULL COMMENT 'Comma separated list of name servers', + `is_public` tinyint(1) NOT NULL DEFAULT '0', + `public_domain_suffix` VARCHAR(255), + `state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled', + `domain_id` bigint unsigned COMMENT 'for domain-specific ownership', + `account_id` bigint(20) unsigned NOT NULL, + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + KEY `i_dns_server__account_id` (`account_id`), + CONSTRAINT `fk_dns_server__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- DNS Server Details Table +CREATE TABLE IF NOT EXISTS `cloud`.`dns_server_details` ( + `id` bigint unsigned UNIQUE NOT NULL AUTO_INCREMENT COMMENT 'id', + `dns_server_id` bigint unsigned NOT NULL COMMENT 'dns_server the detail is related to', + `name` varchar(255) NOT NULL COMMENT 'name of the detail', + `value` varchar(255) NOT NULL COMMENT 'value of the detail', + `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Should detail be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_dns_server_details__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- DNS Zone Table (Stores DNS Zone Metadata) +CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone', + `uuid` varchar(40) COMMENT 'uuid of the dns zone', + `name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)', + `dns_server_id` bigint unsigned NOT NULL COMMENT 'fk to dns_server.id', + `external_reference` VARCHAR(255) COMMENT 'id of external provider resource', + `domain_id` bigint unsigned COMMENT 'for domain-specific ownership', + `account_id` bigint unsigned COMMENT 'account id. foreign key to account table', + `description` varchar(1024) DEFAULT NULL, + `type` ENUM('Private', 'Public') NOT NULL DEFAULT 'Public', + `state` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Inactive', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), + KEY `i_dns_zone__dns_server` (`dns_server_id`), + KEY `i_dns_zone__account_id` (`account_id`), + CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- DNS Zone Network Map (One-to-Many Link) +CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping', + `uuid` varchar(40), + `dns_zone_id` bigint(20) unsigned NOT NULL, + `network_id` bigint(20) unsigned NOT NULL COMMENT 'network to which dns zone is associated to', + `sub_domain` varchar(255) DEFAULT NULL COMMENT 'Subdomain for auto-registration', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone_network_map__uuid` UNIQUE (`uuid`), + KEY `fk_dns_map__zone_id` (`dns_zone_id`), + KEY `fk_dns_map__network_id` (`network_id`), + CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- This is part of allowing firewall rules on public IP addresses in VPC network +ALTER TABLE `cloud`.`firewall_rules` MODIFY COLUMN `network_id` BIGINT UNSIGNED NULL; + +-- KBOSS + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_pool_ref` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `storage_pool_id` bigint unsigned NOT NULL COMMENT 'The storage ID. Foreign key that points to the storage_pool table.', + `volume_id` bigint unsigned NOT NULL COMMENT 'The volumes ID. Foreign key that points to the volumes table.', + `backup_delta_path` varchar(255) COMMENT 'Path of the created delta.', + `backup_parent_path` varchar(255) COMMENT 'Path of the created delta parent.', + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_pool_ref__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`), + CONSTRAINT `fk_internal_backup_pool_ref__storage_pool_id` FOREIGN KEY (`storage_pool_id`) REFERENCES `storage_pool`(`id`), + CONSTRAINT `fk_internal_backup_pool_ref__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) + ); + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_store_ref` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `volume_id` bigint unsigned NOT NULL COMMENT 'The volume ID. Foreign key that points to the volumes table.', + `device_id` bigint unsigned COMMENT 'device ID of the volume', + `path` varchar(255) COMMENT 'Path of the backup.', + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_store_ref__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`), + CONSTRAINT `fk_internal_backup_store_ref__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) + ); + +CREATE TABLE IF NOT EXISTS `cloud`.`internal_backup_service_job` ( + `id` bigint NOT NULL UNIQUE AUTO_INCREMENT, + `backup_id` bigint unsigned NOT NULL COMMENT 'The backup ID. Foreign key that points to the backups table.', + `instance_id` bigint unsigned NOT NULL COMMENT 'The instance ID. Foreign key that points to the vm_instance table.', + `account_id` bigint(20) unsigned COMMENT 'Account ID of the owner of the VM.', + `host_id` bigint unsigned COMMENT 'The host ID that is executing the compression. Foreign key that points to the host table.', + `zone_id` bigint unsigned NOT NULL COMMENT 'The zone ID of the where the VM is. Foreign key that points to the data_center table', + `attempts` int(32) unsigned NOT NULL DEFAULT 0, + `type` varchar(55) NOT NULL, + `created` datetime NOT NULL, + `scheduled_start_time` datetime NOT NULL, + `start_time` datetime, + `removed` datetime, + PRIMARY KEY (`id`), + CONSTRAINT `fk_internal_backup_service_job__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__instance_id` FOREIGN KEY (`instance_id`) REFERENCES `vm_instance`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_internal_backup_service_job__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE + ); + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'uncompressed_size', 'bigint unsigned'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'compression_status', 'varchar(55)'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'validation_status', 'varchar(55)'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'isolated', 'TINYINT(1) NOT NULL DEFAULT 0 COMMENT "Whether the scheduled backups will be isolated or not."'); + +UPDATE `cloud`.`configuration` SET `value`=CONCAT(`value`, ', backupValidationCommandTimeout, backupValidationScreenshotWait, backupValidationBootTimeout') +WHERE `name`='user.vm.readonly.details' AND `value` IS NOT NULL; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql new file mode 100644 index 000000000000..a1bc1a1141a9 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql @@ -0,0 +1,44 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- VIEW `cloud`.`dns_server_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_server_view`; +CREATE VIEW `cloud`.`dns_server_view` AS + SELECT + dns.id, + dns.uuid, + dns.name, + dns.provider_type, + dns.url, + dns.port, + dns.name_servers, + dns.is_public, + dns.public_domain_suffix, + dns.state, + dns.created, + dns.removed, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_server` dns + INNER JOIN + `cloud`.`account` account ON dns.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON dns.domain_id = domain.id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql new file mode 100644 index 000000000000..a41e003ae4f2 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql @@ -0,0 +1,45 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- VIEW `cloud`.`dns_zone_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_zone_view`; +CREATE VIEW `cloud`.`dns_zone_view` AS + SELECT + zone.id, + zone.uuid, + zone.name, + zone.dns_server_id, + zone.state, + zone.description, + server.uuid dns_server_uuid, + server.name dns_server_name, + server_account.account_name dns_server_account_name, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_zone` zone + INNER JOIN + `cloud`.`dns_server` server ON zone.dns_server_id = server.id + INNER JOIN + `cloud`.`account` server_account ON server.account_id = server_account.id + INNER JOIN + `cloud`.`account` account ON zone.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON zone.domain_id = domain.id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql index a12e02bcfdb8..d5f17606cb41 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_router_view.sql @@ -76,6 +76,7 @@ select nics.broadcast_uri broadcast_uri, nics.isolation_uri isolation_uri, nics.mtu mtu, + nics.enabled is_nic_enabled, vpc.id vpc_id, vpc.uuid vpc_uuid, vpc.name vpc_name, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql index 3173274623ed..5f49cffbf1f5 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql @@ -27,6 +27,7 @@ SELECT `cloud`.`gui_themes`.`description` AS `description`, `cloud`.`gui_themes`.`css` AS `css`, `cloud`.`gui_themes`.`json_configuration` AS `json_configuration`, + `cloud`.`gui_themes`.`login_base_domain` AS `login_base_domain`, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'commonName' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) common_names, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'domain' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) domains, (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'account' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) accounts, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql index d9f4e2671595..255ce866c427 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql @@ -64,6 +64,7 @@ SELECT guest_os_category.id guest_os_category_id, guest_os_category.uuid guest_os_category_uuid, guest_os_category.name guest_os_category_name, + (SELECT `value` FROM `cloud`.`host_details` `hd` WHERE `hd`.`host_id` = `cloud`.`host`.`id` AND `hd`.`name` = 'guest.os.rule') AS `guest_os_rule`, mem_caps.used_capacity memory_used_capacity, mem_caps.reserved_capacity memory_reserved_capacity, cpu_caps.used_capacity cpu_used_capacity, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql new file mode 100644 index 000000000000..a1fbd102630b --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.internal_backup_view.sql @@ -0,0 +1,47 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- VIEW `cloud`.`internal_backup_view`; + +DROP VIEW IF EXISTS `cloud`.`internal_backup_view`; +CREATE VIEW `cloud`.`internal_backup_view` AS +SELECT b.id, + b.uuid, + b.vm_id, + b.backed_volumes, + b.type, + b.date, + b.status, + b.compression_status, + b.backup_offering_id, + b.size, + b.protected_size, + b.zone_id, + MAX(CASE WHEN bd.name = 'image_store_id' THEN bd.value END) image_store_id, + MAX(CASE WHEN bd.name = 'parent_id' THEN bd.value END) parent_id, + MAX(CASE WHEN bd.name = 'end_of_chain' THEN bd.value END) end_of_chain, + MAX(CASE WHEN bd.name = 'current' THEN bd.value END) current, + COALESCE(MAX(CASE WHEN bd.name = 'isolated' THEN bd.value END), 'false') isolated, + nbpr.volume_id, + nbsr.path image_store_path +FROM backups b +LEFT JOIN backup_details bd ON b.id = bd.backup_id +LEFT JOIN backup_offering bo ON b.backup_offering_id = bo.id +LEFT JOIN internal_backup_store_ref nbsr ON b.id = nbsr.backup_id +LEFT JOIN internal_backup_pool_ref nbpr ON nbpr.volume_id = nbsr.volume_id +WHERE bo.provider='kboss' +GROUP BY b.id, nbsr.volume_id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql index 340cfa9055fb..dcba71e10985 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql @@ -29,8 +29,6 @@ select user.lastname, user.email, user.state, - user.api_key, - user.secret_key, user.created, user.removed, user.timezone, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index 94bc8640fd54..fbf126608eae 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -56,6 +56,7 @@ SELECT `vm_instance`.`display_vm` AS `display_vm`, `vm_instance`.`delete_protection` AS `delete_protection`, `guest_os`.`uuid` AS `guest_os_uuid`, + `guest_os`.`display_name` AS `guest_os_display_name`, `vm_instance`.`pod_id` AS `pod_id`, `host_pod_ref`.`uuid` AS `pod_uuid`, `vm_instance`.`private_ip_address` AS `private_ip_address`, @@ -79,6 +80,7 @@ SELECT `vm_template`.`format` AS `template_format`, `vm_template`.`display_text` AS `template_display_text`, `vm_template`.`enable_password` AS `password_enabled`, + `vm_template`.`extension_id` AS `template_extension_id`, `iso`.`id` AS `iso_id`, `iso`.`uuid` AS `iso_uuid`, `iso`.`name` AS `iso_name`, @@ -141,6 +143,8 @@ SELECT `nics`.`mac_address` AS `mac_address`, `nics`.`broadcast_uri` AS `broadcast_uri`, `nics`.`isolation_uri` AS `isolation_uri`, + `nics`.`enabled` AS `is_nic_enabled`, + `nic_details`.`value` AS `nic_dns_name`, `vpc`.`id` AS `vpc_id`, `vpc`.`uuid` AS `vpc_uuid`, `networks`.`uuid` AS `network_uuid`, @@ -185,7 +189,7 @@ SELECT `lease_expiry_action`.`value` AS `lease_expiry_action`, `lease_action_execution`.`value` AS `lease_action_execution` FROM - (((((((((((((((((((((((((((((((((((((`user_vm` + ((((((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) AND ISNULL(`vm_instance`.`removed`)))) JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) @@ -212,6 +216,7 @@ FROM LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `nic_details` ON ((`nic_details`.`nic_id` = `nics`.`id`) AND (`nic_details`.`name` = 'nicdnsname'))) LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) AND ISNULL(`vpc`.`removed`)))) diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.volume_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.volume_view.sql index ffeb93e8fa7a..8ba7e5c6df77 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.volume_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.volume_view.sql @@ -40,6 +40,10 @@ SELECT `volumes`.`chain_info` AS `chain_info`, `volumes`.`external_uuid` AS `external_uuid`, `volumes`.`encrypt_format` AS `encrypt_format`, + `volumes`.`kms_key_id` AS `kms_key_id`, + `kms_keys`.`uuid` AS `kms_key_uuid`, + `kms_keys`.`name` AS `kms_key_name`, + `volumes`.`kms_wrapped_key_id` AS `kms_wrapped_key_id`, `volumes`.`delete_protection` AS `delete_protection`, `account`.`id` AS `account_id`, `account`.`uuid` AS `account_uuid`, @@ -116,7 +120,7 @@ SELECT `resource_tag_domain`.`uuid` AS `tag_domain_uuid`, `resource_tag_domain`.`name` AS `tag_domain_name` FROM - ((((((((((((((((((`volumes` + (((((((((((((((((((`volumes` JOIN `account`ON ((`volumes`.`account_id` = `account`.`id`))) JOIN `domain`ON @@ -129,8 +133,10 @@ LEFT JOIN `vm_instance`ON ((`volumes`.`instance_id` = `vm_instance`.`id`))) LEFT JOIN `user_vm`ON ((`user_vm`.`id` = `vm_instance`.`id`))) -LEFT JOIN `volume_store_ref`ON +LEFT JOIN `volume_store_ref` ON ((`volumes`.`id` = `volume_store_ref`.`volume_id`))) +LEFT JOIN `kms_keys` ON + ((`volumes`.`kms_key_id` = `kms_keys`.`id`))) LEFT JOIN `service_offering`ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) LEFT JOIN `disk_offering`ON diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql index 751d8f91a259..3669bb10122b 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql @@ -38,6 +38,7 @@ select `vpc_offerings`.`sort_key` AS `sort_key`, `vpc_offerings`.`routing_mode` AS `routing_mode`, `vpc_offerings`.`specify_as_number` AS `specify_as_number`, + `vpc_offerings`.`conserve_mode` AS `conserve_mode`, group_concat(distinct `domain`.`id` separator ',') AS `domain_id`, group_concat(distinct `domain`.`uuid` separator ',') AS `domain_uuid`, group_concat(distinct `domain`.`name` separator ',') AS `domain_name`, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql new file mode 100644 index 000000000000..0646c3b6f919 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql @@ -0,0 +1,48 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- cloud_usage.quota_summary_view source + +-- Create view for quota summary +DROP VIEW IF EXISTS `cloud_usage`.`quota_summary_view`; +CREATE VIEW `cloud_usage`.`quota_summary_view` AS +SELECT + cloud_usage.quota_account.account_id AS account_id, + cloud_usage.quota_account.quota_balance AS quota_balance, + cloud_usage.quota_account.quota_balance_date AS quota_balance_date, + cloud_usage.quota_account.quota_enforce AS quota_enforce, + cloud_usage.quota_account.quota_min_balance AS quota_min_balance, + cloud_usage.quota_account.quota_alert_date AS quota_alert_date, + cloud_usage.quota_account.quota_alert_type AS quota_alert_type, + cloud_usage.quota_account.last_statement_date AS last_statement_date, + cloud.account.uuid AS account_uuid, + cloud.account.account_name AS account_name, + cloud.account.state AS account_state, + cloud.account.removed AS account_removed, + cloud.domain.id AS domain_id, + cloud.domain.uuid AS domain_uuid, + cloud.domain.name AS domain_name, + cloud.domain.path AS domain_path, + cloud.domain.removed AS domain_removed, + cloud.projects.uuid AS project_uuid, + cloud.projects.name AS project_name, + cloud.projects.removed AS project_removed +FROM + cloud_usage.quota_account + INNER JOIN cloud.account ON (cloud.account.id = cloud_usage.quota_account.account_id) + INNER JOIN cloud.domain ON (cloud.domain.id = cloud.account.domain_id) + LEFT JOIN cloud.projects ON (cloud.account.type = 5 AND cloud.account.id = cloud.projects.project_account_id); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql new file mode 100644 index 000000000000..7ac001384e8e --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_usage_view.sql @@ -0,0 +1,35 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- VIEW `cloud_usage`.`quota_usage_view`; + +DROP VIEW IF EXISTS `cloud_usage`.`quota_usage_view`; +CREATE VIEW `cloud_usage`.`quota_usage_view` AS +SELECT qu.id, + qu.usage_item_id, + qu.zone_id, + qu.account_id, + qu.domain_id, + qu.usage_type, + qu.quota_used, + qu.start_date, + qu.end_date, + cu.usage_id AS resource_id, + cu.network_id as network_id, + cu.offering_id as offering_id +FROM `cloud_usage`.`quota_usage` qu +INNER JOIN `cloud_usage`.`cloud_usage` cu ON (cu.id = qu.usage_item_id); diff --git a/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql b/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql new file mode 100644 index 000000000000..8c7737db0884 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql @@ -0,0 +1,40 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you 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. + +-- VIEW `cloud`.`nic_dns_view`; + +DROP VIEW IF EXISTS `cloud`.`nic_dns_view`; +CREATE VIEW `cloud`.`nic_dns_view` AS +SELECT + n.id AS id, + n.uuid AS uuid, + n.instance_id AS instance_id, + n.network_id AS network_id, + n.ip4_address AS ip4_address, + n.ip6_address AS ip6_address, + n.removed AS removed, + nd.value AS nic_dns_name, + map.dns_zone_id AS dns_zone_id, + map.sub_domain AS sub_domain +FROM + `cloud`.`nics` n + INNER JOIN + `cloud`.`dns_zone_network_map` map ON n.network_id = map.network_id + LEFT JOIN + `cloud`.`nic_details` nd ON n.id = nd.nic_id AND nd.name = 'nicdnsname' +WHERE + n.instance_id IS NOT NULL AND map.removed IS NULL; diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java index 9445efeb089c..6f153727ab76 100644 --- a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java @@ -41,6 +41,7 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; @@ -113,6 +114,69 @@ public void testListPoolIdsByVolumeCount_without_cluster_details() throws SQLExc verify(preparedStatementMock, times(1)).executeQuery(); } + @Test + public void findByInstanceAndNotState_queriesWithInstanceIdAndExcludedStates() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + VolumeVO mockedVO = Mockito.mock(VolumeVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + + volumeDao.findByInstanceAndNotStates(42L, Volume.State.Ready); + + Mockito.verify(sc).setParameters("instanceId", 42L); + Mockito.verify(sc).setParameters("state", (Object[]) new Volume.State[]{Volume.State.Ready}); + } + + @Test + public void findByInstanceAndNotStates_withMultipleExcludedStates_passesAllStatesToCriteria() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + VolumeVO mockedVO = Mockito.mock(VolumeVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + + volumeDao.findByInstanceAndNotStates(7L, Volume.State.Destroy, Volume.State.Expunged); + + Mockito.verify(sc).setParameters("instanceId", 7L); + Mockito.verify(sc).setParameters("state", + (Object[]) new Volume.State[]{Volume.State.Destroy, Volume.State.Expunged}); + } + + @Test + public void findByInstanceAndNotStates_returnsResultFromDao() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + VolumeVO vol = Mockito.mock(VolumeVO.class); + Mockito.doReturn(List.of(vol)).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(VolumeVO.class)); + + List result = volumeDao.findByInstanceAndNotStates(1L, Volume.State.Ready); + + Assert.assertEquals(1, result.size()); + Assert.assertSame(vol, result.get(0)); + } + + @Test + public void findByInstanceAndNotStates_noMatchingVolumes_returnsEmptyList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(VolumeVO.class)); + + List result = volumeDao.findByInstanceAndNotStates(99L, Volume.State.Ready); + + Assert.assertTrue(result.isEmpty()); + } + @Test public void testSearchRemovedByVmsNoVms() { Assert.assertTrue(CollectionUtils.isEmpty(volumeDao.searchRemovedByVms( @@ -141,5 +205,4 @@ public void testSearchRemovedByVms() { Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), Mockito.eq(false)); } - } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java index 27995eb179af..884398cf410d 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/DatabaseUpgradeCheckerTest.java @@ -44,6 +44,9 @@ import com.cloud.upgrade.dao.Upgrade41120to41200; import com.cloud.upgrade.dao.Upgrade41510to41520; import com.cloud.upgrade.dao.Upgrade41610to41700; +import com.cloud.upgrade.dao.Upgrade42020to42030; +import com.cloud.upgrade.dao.Upgrade42030to42040; +import com.cloud.upgrade.dao.Upgrade42040to42100; import com.cloud.upgrade.dao.Upgrade452to453; import com.cloud.upgrade.dao.Upgrade453to460; import com.cloud.upgrade.dao.Upgrade460to461; @@ -380,4 +383,43 @@ public void isNotStandalone() throws SQLException { assertFalse("DatabaseUpgradeChecker should not be a standalone component", checker.isStandalone()); } + @Test + public void testCalculateUpgradePath42010to42030() { + + final CloudStackVersion dbVersion = CloudStackVersion.parse("4.20.1.0"); + assertNotNull(dbVersion); + + final CloudStackVersion currentVersion = CloudStackVersion.parse("4.20.3.0"); + assertNotNull(currentVersion); + + final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); + final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); + + assertNotNull(upgrades); + assertEquals(1, upgrades.length); + assertTrue(upgrades[0] instanceof Upgrade42020to42030); + + assertArrayEquals(new String[]{"4.20.2.0", "4.20.3.0"}, upgrades[0].getUpgradableVersionRange()); + assertEquals(currentVersion.toString(), upgrades[0].getUpgradedVersion()); + } + + @Test + public void testCalculateUpgradePath42010to42100() { + + final CloudStackVersion dbVersion = CloudStackVersion.parse("4.20.1.0"); + assertNotNull(dbVersion); + + final CloudStackVersion currentVersion = CloudStackVersion.parse("4.21.0.0"); + assertNotNull(currentVersion); + + final DatabaseUpgradeChecker checker = new DatabaseUpgradeChecker(); + final DbUpgrade[] upgrades = checker.calculateUpgradePath(dbVersion, currentVersion); + + assertNotNull(upgrades); + assertEquals(3, upgrades.length); + assertTrue(upgrades[0] instanceof Upgrade42020to42030); + assertTrue(upgrades[1] instanceof Upgrade42030to42040); + assertTrue(upgrades[2] instanceof Upgrade42040to42100); + assertEquals(currentVersion.toString(), upgrades[2].getUpgradedVersion()); + } } diff --git a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java index 8028e78c9073..51db952eb613 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/SystemVmTemplateRegistrationTest.java @@ -570,18 +570,19 @@ public void updateRegisteredTemplateDetails_UpdatesTemplateSuccessfully() { SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + String templateName = "templateName"; + when(templateVO.getName()).thenReturn(templateName); GuestOSVO guestOS = Mockito.mock(GuestOSVO.class); when(templateDetails.getGuestOs()).thenReturn("Debian"); when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); - when(templateDetails.getName()).thenReturn("templateName"); when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); when(guestOSDao.findOneByDisplayName("Debian")).thenReturn(guestOS); when(guestOS.getId()).thenReturn(10L); when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); @@ -590,7 +591,7 @@ public void updateRegisteredTemplateDetails_UpdatesTemplateSuccessfully() { verify(vmTemplateDao).update(templateVO.getId(), templateVO); verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); } @Test @@ -620,16 +621,17 @@ public void updateRegisteredTemplateDetails_SkipsGuestOSUpdateWhenNotFound() { SystemVmTemplateRegistration.MetadataTemplateDetails templateDetails = Mockito.mock(SystemVmTemplateRegistration.MetadataTemplateDetails.class); VMTemplateVO templateVO = Mockito.mock(VMTemplateVO.class); + String templateName = "templateName"; + when(templateVO.getName()).thenReturn(templateName); when(templateDetails.getGuestOs()).thenReturn("NonExistentOS"); when(templateDetails.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); - when(templateDetails.getName()).thenReturn("templateName"); when(vmTemplateDao.findById(templateId)).thenReturn(templateVO); when(guestOSDao.findOneByDisplayName("NonExistentOS")).thenReturn(null); when(vmTemplateDao.update(templateVO.getId(), templateVO)).thenReturn(true); doNothing().when(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); doNothing().when(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); systemVmTemplateRegistration.updateRegisteredTemplateDetails(templateId, templateDetails, zoneId); @@ -637,7 +639,7 @@ public void updateRegisteredTemplateDetails_SkipsGuestOSUpdateWhenNotFound() { verify(vmTemplateDao).update(templateVO.getId(), templateVO); verify(systemVmTemplateRegistration).updateSystemVMEntries(templateId, Hypervisor.HypervisorType.KVM); verify(systemVmTemplateRegistration).updateConfigurationParams(Hypervisor.HypervisorType.KVM, - "templateName", zoneId); + templateName, zoneId); } @Test diff --git a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java similarity index 98% rename from engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java rename to engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java index 16908f6aaac0..5a2b55f03384 100644 --- a/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42010to42100Test.java +++ b/engine/schema/src/test/java/com/cloud/upgrade/dao/Upgrade42040to42100Test.java @@ -35,9 +35,9 @@ import com.cloud.utils.db.TransactionLegacy; @RunWith(MockitoJUnitRunner.class) -public class Upgrade42010to42100Test { +public class Upgrade42040to42100Test { @Spy - Upgrade42010to42100 upgrade; + Upgrade42040to42100 upgrade; @Mock private Connection conn; diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java b/engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java similarity index 57% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java rename to engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java index f36d5c49581d..d5b1fef7a766 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java +++ b/engine/schema/src/test/java/com/cloud/vm/VmIsoMapVOTest.java @@ -14,28 +14,28 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; +package com.cloud.vm; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -@RunWith(MockitoJUnitRunner.class) -public class StorageTest { +public class VmIsoMapVOTest { @Test - public void setTagsTestAddFieldTagsToCollection() { - Storage variable = new Storage(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); + public void testFullConstructorPopulatesAllFields() { + VmIsoMapVO row = new VmIsoMapVO(7L, 42L, 4); + Assert.assertEquals(7L, row.getVmId()); + Assert.assertEquals(42L, row.getIsoId()); + Assert.assertEquals(4, row.getDeviceSeq()); + Assert.assertNotNull(row.getCreated()); } @Test - public void setScopeTestAddFieldScopeToCollection() { - Storage variable = new Storage(); - variable.setScope(null);; - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("scope")); + public void testNoArgConstructorLeavesNonIdFieldsAtDefaults() { + VmIsoMapVO row = new VmIsoMapVO(); + Assert.assertEquals(0L, row.getVmId()); + Assert.assertEquals(0L, row.getIsoId()); + Assert.assertEquals(0, row.getDeviceSeq()); + Assert.assertNull(row.getCreated()); } } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 8145158dfa40..bbf775de27ad 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -27,6 +27,7 @@ import javax.inject.Inject; import com.cloud.agent.api.to.DiskTO; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -75,6 +76,7 @@ import com.cloud.storage.Snapshot.Type; import com.cloud.storage.SnapshotVO; import com.cloud.storage.StorageManager; +import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StoragePool; import com.cloud.storage.VolumeVO; @@ -108,6 +110,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { StorageCacheManager cacheMgr; @Inject VolumeDataStoreDao volumeDataStoreDao; + @Inject + ClvmPoolManager clvmPoolManager; @Inject StorageManager storageManager; @@ -187,7 +191,7 @@ protected Answer copyObject(DataObject srcData, DataObject destData, Host destHo srcForCopy = cacheData = cacheMgr.createCacheObject(srcData, destScope); } - CopyCommand cmd = new CopyCommand(srcForCopy.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), primaryStorageDownloadWait, + CopyCommand cmd = new CopyCommand(srcForCopy.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), primaryStorageDownloadWait, VirtualMachineManager.ExecuteInSequence.value()); EndPoint ep = destHost != null ? RemoteHostEndPoint.getHypervisorHostEndPoint(destHost) : selector.select(srcForCopy, destData); if (ep == null) { @@ -253,6 +257,43 @@ protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(DataTO return dataTO; } + /** + * Adds {@code 'xenserver.create.full.clone'} value for a given primary storage, whose HV is XenServer, on datastore's {@code fullCloneFlag} field + * @param dataTO Dest data store TO + * @return dataTO including fullCloneFlag, if provided + */ + protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(DataTO dataTO) { + if (dataTO != null && dataTO.getHypervisorType().equals(Hypervisor.HypervisorType.XenServer)){ + DataStoreTO dataStoreTO = dataTO.getDataStore(); + if (dataStoreTO != null && dataStoreTO instanceof PrimaryDataStoreTO){ + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) dataStoreTO; + primaryDataStoreTO.setFullCloneFlag(StorageManager.XenserverCreateCloneFull.valueIn(primaryDataStoreTO.getId())); + } + } + return dataTO; + } + + /** + * Dispatches to the per-hypervisor {@code addFullCloneAndDiskprovisiongStrictnessFlagOn*Dest} helper + * based on {@code dataTO.getHypervisorType()}. Returns {@code dataTO} unchanged for hypervisors + * that do not have a full-clone toggle. + * @param dataTO Dest data store TO + * @return dataTO including fullCloneFlag, if provided + */ + protected DataTO addFullCloneAndDiskprovisiongStrictnessFlagOnDest(DataTO dataTO) { + if (dataTO == null) { + return dataTO; + } + Hypervisor.HypervisorType hypervisorType = dataTO.getHypervisorType(); + if (Hypervisor.HypervisorType.VMware.equals(hypervisorType)) { + return addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(dataTO); + } + if (Hypervisor.HypervisorType.XenServer.equals(hypervisorType)) { + return addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(dataTO); + } + return dataTO; + } + protected Answer copyObject(DataObject srcData, DataObject destData) { return copyObject(srcData, destData, null); } @@ -309,7 +350,9 @@ protected Answer copyVolumeFromSnapshot(DataObject snapObj, DataObject volObj) { ep = selector.select(srcData, volObj); } - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); + updateLockHostForVolume(ep, volObj); + + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; if (ep == null) { @@ -331,8 +374,31 @@ protected Answer copyVolumeFromSnapshot(DataObject snapObj, DataObject volObj) { } } + private void updateLockHostForVolume(EndPoint ep, DataObject volObj) { + if (ep == null || !(volObj instanceof VolumeInfo)) { + return; + } + VolumeInfo volumeInfo = (VolumeInfo) volObj; + StoragePool destPool = (StoragePool) volObj.getDataStore(); + if (destPool == null || !ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) { + return; + } + Long hostId = ep.getId(); + Long existingHostId = clvmPoolManager.getClvmLockHostId( + volumeInfo.getId(), + volumeInfo.getUuid(), + volumeInfo.getPath(), + destPool, + true + ); + if (existingHostId == null) { + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + logger.debug("Set lock host ID {} for CLVM volume {} being created from snapshot", hostId, volumeInfo.getId()); + } + } + protected Answer cloneVolume(DataObject template, DataObject volume) { - CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); try { EndPoint ep = selector.select(volume, anyVolumeRequiresEncryption(volume)); Answer answer = null; @@ -379,6 +445,9 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) answer = new Answer(cmd, false, errMsg); } else { answer = ep.sendMessage(cmd); + if (answer != null && answer.getResult()) { + setClvmLockHostIdIfApplicable(destData, ep); + } } return answer; } @@ -416,7 +485,7 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) objOnImageStore.processEvent(Event.CopyingRequested); - CopyCommand cmd = new CopyCommand(objOnImageStore.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _copyvolumewait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(objOnImageStore.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _copyvolumewait, VirtualMachineManager.ExecuteInSequence.value()); EndPoint ep = selector.select(objOnImageStore, destData, encryptionRequired); if (ep == null) { String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired); @@ -434,6 +503,7 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) imageStore.delete(objOnImageStore); return answer; } + setClvmLockHostIdIfApplicable(destData, ep); } catch (Exception e) { if (imageStore.exists(objOnImageStore)) { objOnImageStore.processEvent(Event.OperationFailed); @@ -457,6 +527,9 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) answer = new Answer(cmd, false, errMsg); } else { answer = ep.sendMessage(cmd); + if (answer != null && answer.getResult()) { + setClvmLockHostIdIfApplicable(destData, ep); + } } // delete volume on cache store if (cacheData != null) { @@ -466,6 +539,17 @@ protected Answer copyVolumeBetweenPools(DataObject srcData, DataObject destData) } } + private void setClvmLockHostIdIfApplicable(DataObject destData, EndPoint ep) { + if (ep == null || !(destData instanceof VolumeInfo)) { + return; + } + VolumeInfo destVolume = (VolumeInfo) destData; + if (ClvmPoolManager.isClvmPoolType(destVolume.getStoragePoolType())) { + clvmPoolManager.setClvmLockHostId(destVolume.getId(), ep.getId()); + logger.debug("Set CLVM lock host {} for migrated volume {}", ep.getId(), destVolume.getUuid()); + } + } + private boolean canBypassSecondaryStorage(DataObject srcData, DataObject destData) { if (srcData instanceof VolumeInfo) { if (((VolumeInfo)srcData).isDirectDownload()) { @@ -581,6 +665,12 @@ protected Answer migrateVolumeToPool(DataObject srcData, DataObject destData) { volumeVo.setPoolId(destPool.getId()); volumeVo.setPoolType(destPool.getPoolType()); volumeVo.setLastPoolId(oldPoolId); + if (destPool.getPoolType() == StoragePoolType.CLVM) { + volumeVo.setFormat(ImageFormat.RAW); + } + if (ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) { + clvmPoolManager.setClvmLockHostId(volume.getId(), ep.getId()); + } // For SMB, pool credentials are also stored in the uri query string. We trim the query string // part here to make sure the credentials do not get stored in the db unencrypted. String folder = destPool.getPath(); @@ -660,7 +750,7 @@ protected Answer createTemplateFromSnapshot(DataObject srcData, DataObject destD ep = selector.select(srcData, destData); } - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _createprivatetemplatefromsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _createprivatetemplatefromsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; if (ep == null) { logger.error(NO_REMOTE_ENDPOINT_SSVM); @@ -698,7 +788,7 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) { Scope selectedScope = pickCacheScopeForCopy(srcData, destData); cacheData = cacheMgr.getCacheObject(srcData, selectedScope); - CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); cmd.setCacheTO(cacheData.getTO()); cmd.setOptions(options); EndPoint ep = selector.select(srcData, destData, encryptionRequired); @@ -709,7 +799,7 @@ protected Answer copySnapshot(DataObject srcData, DataObject destData) { answer = ep.sendMessage(cmd); } } else { - addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(destData.getTO()); + addFullCloneAndDiskprovisiongStrictnessFlagOnDest(destData.getTO()); CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), _backupsnapshotwait, VirtualMachineManager.ExecuteInSequence.value()); cmd.setOptions(options); EndPoint ep = selector.select(srcData, destData, StorageAction.BACKUPSNAPSHOT, encryptionRequired); @@ -758,9 +848,9 @@ public void copyAsync(Map volumeMap, VirtualMachineTO vmT private boolean anyVolumeRequiresEncryption(DataObject ... objects) { for (DataObject o : objects) { // this fails code smell for returning true twice, but it is more readable than combining all tests into one statement - if (o instanceof VolumeInfo && ((VolumeInfo) o).getPassphraseId() != null) { + if (o instanceof VolumeInfo && (((VolumeInfo) o).getPassphraseId() != null || ((VolumeInfo) o).getKmsKeyId() != null)) { return true; - } else if (o instanceof SnapshotInfo && ((SnapshotInfo) o).getBaseVolume().getPassphraseId() != null) { + } else if (o instanceof SnapshotInfo && (((SnapshotInfo) o).getBaseVolume().getPassphraseId() != null || ((SnapshotInfo) o).getBaseVolume().getKmsKeyId() != null)) { return true; } } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java index 947b4af8f690..867470dac040 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java @@ -144,12 +144,16 @@ protected boolean isDestinationNfsPrimaryStorageClusterWide(Map volumeDataStoreMap, VirtualMach String errMsg = null; boolean success = false; Map srcVolumeInfoToDestVolumeInfo = new HashMap<>(); + List samePoolClvmVolumes = new ArrayList<>(); try { if (srcHost.getHypervisorType() != HypervisorType.KVM) { @@ -2052,6 +2062,13 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach continue; } + if (sourceStoragePool.getId() == destStoragePool.getId() && + ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + logger.info("Same-pool CLVM migration for volume [{}]: skipping data copy.", srcVolumeInfo.getUuid()); + samePoolClvmVolumes.add(srcVolumeInfo); + continue; + } + if (!shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool)) { continue; } @@ -2071,6 +2088,13 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach setVolumeMigrationOptions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost, destStoragePool, migrationType); + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + destVolumeInfo.setDestinationHostId(destHost.getId()); + clvmPoolManager.setClvmLockHostId(destVolume.getId(), destHost.getId()); + logger.info("Set CLVM lock host {} for volume {} during migration to ensure creation on destination host", + destHost.getId(), destVolumeInfo.getUuid()); + } + // create a volume on the destination storage destDataStore.getDriver().createAsync(destDataStore, destVolumeInfo, null); @@ -2096,7 +2120,7 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach MigrateCommand.MigrateDiskInfo migrateDiskInfo; - boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; + boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem, StoragePoolType.CLVM, StoragePoolType.CLVM_NG) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; if (isNonManagedToNfs) { migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, @@ -2106,9 +2130,12 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } else { String backingPath = generateBackingPath(destStoragePool, destVolumeInfo); migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath); + migrateDiskInfo = updateMigrateDiskInfoForBlockDevice(migrateDiskInfo, destStoragePool); migrateDiskInfo.setSourceDiskOnStorageFileSystem(isStoragePoolTypeOfFile(sourceStoragePool)); migrateDiskInfoList.add(migrateDiskInfo); } + migrateDiskInfo.setSourcePoolType(sourceStoragePool.getPoolType()); + migrateDiskInfo.setDestPoolType(destVolumeInfo.getStoragePoolType()); prepareDiskWithSecretConsumerDetail(vmTO, srcVolumeInfo, destVolumeInfo.getPath()); migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); @@ -2116,6 +2143,8 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo); } + prepareDisksForMigrationForClvm(vmTO, volumeDataStoreMap, srcHost); + PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); Answer pfma; @@ -2132,6 +2161,25 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach throw new AgentUnavailableException("Operation timed out", destHost.getId()); } + for (VolumeInfo vol : samePoolClvmVolumes) { + StoragePoolVO samePoolClvmPool = _storagePoolDao.findById(vol.getPoolId()); + String vgName = samePoolClvmPool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, vol.getPath()); + logger.info("Activating CLVM volume [{}] in shared mode on dest host [{}] for same-pool migration.", + vol.getUuid(), destHost.getId()); + Answer activateAnswer = agentManager.send(destHost.getId(), + new ClvmLockTransferCommand(ClvmLockTransferCommand.Operation.ACTIVATE_SHARED, lvPath, vol.getUuid())); + if (activateAnswer == null || !activateAnswer.getResult()) { + throw new CloudRuntimeException(String.format( + "Failed to activate CLVM volume [%s] in shared mode on dest host [%s]: %s", + vol.getUuid(), destHost.getId(), + activateAnswer != null ? activateAnswer.getDetails() : "null answer")); + } + } + VMInstanceVO vm = _vmDao.findById(vmTO.getId()); boolean isWindows = _guestOsCategoryDao.findById(_guestOsDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); @@ -2141,6 +2189,9 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList); migrateCommand.setMigrateStorageManaged(managedStorageDestination); migrateCommand.setMigrateNonSharedInc(migrateNonSharedInc); + boolean hasClvmCrossPoolVolume = migrateStorage.values().stream() + .anyMatch(info -> ClvmPoolManager.isClvmPoolType(info.getSourcePoolType())); + migrateCommand.setClvmCrossPoolMigration(hasClvmCrossPoolVolume); Integer newVmCpuShares = ((PrepareForMigrationAnswer) pfma).getNewVmCpuShares(); if (newVmCpuShares != null) { @@ -2171,7 +2222,7 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } } - handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, srcHost, destHost); if (!success) { if (migrateAnswer == null) { @@ -2211,10 +2262,43 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } } + private void prepareDisksForMigrationForClvm(VirtualMachineTO vmTO, Map volumeDataStoreMap, Host srcHost) { + // For CLVM/CLVM_NG source pools, convert volumes from exclusive to shared mode + // on the source host BEFORE PrepareForMigrationCommand on the destination. + boolean hasClvmSource = volumeDataStoreMap.keySet().stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(p -> p != null && (p.getPoolType() == StoragePoolType.CLVM || p.getPoolType() == StoragePoolType.CLVM_NG)); + if (hasClvmSource && srcHost.getHypervisorType() == HypervisorType.KVM) { + logger.info("CLVM/CLVM_NG source pool detected for VM [{}], sending PreMigrationCommand to source host [{}] to convert volumes to shared mode.", vmTO.getName(), srcHost.getId()); + PreMigrationCommand preMigCmd = new PreMigrationCommand(vmTO, vmTO.getName()); + try { + Answer preMigAnswer = agentManager.send(srcHost.getId(), preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + logger.warn("PreMigrationCommand failed for CLVM/CLVM_NG VM [{}] on source host [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", vmTO.getName(), srcHost.getId(), details); + } else { + logger.info("Successfully converted CLVM/CLVM_NG volumes to shared mode on source host [{}] for VM [{}].", srcHost.getId(), vmTO.getName()); + } + } catch (Exception e) { + logger.warn("Failed to send PreMigrationCommand to source host [{}] for VM [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", srcHost.getId(), vmTO.getName(), e.getMessage()); + } + } else if (hasClvmSource) { + logger.debug("Skipping PreMigrationCommand for non-KVM hypervisor type: {} on host [{}]", srcHost.getHypervisorType(), srcHost.getId()); + } + } + private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host destHost, VMInstanceVO vmInstance, VolumeInfo srcVolumeInfo, StoragePoolVO sourceStoragePool, StoragePoolVO destStoragePool, DataStore destDataStore) { VMTemplateVO vmTemplate = _vmTemplateDao.findById(vmInstance.getTemplateId()); String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo); + + // Check if source is CLVM/CLVM_NG (block device storage) + // LinkedClone (VIR_MIGRATE_NON_SHARED_INC) only works for file → file migrations + // For block device sources, use FullClone (VIR_MIGRATE_NON_SHARED_DISK) + boolean sourceIsBlockDevice = sourceStoragePool.getPoolType() == StoragePoolType.CLVM || + sourceStoragePool.getPoolType() == StoragePoolType.CLVM_NG; + if (StringUtils.isNotBlank(srcVolumeBackingFile) && supportStoragePoolType(destStoragePool.getPoolType(), StoragePoolType.Filesystem) && + !sourceIsBlockDevice && srcVolumeInfo.getTemplateId() != null && Objects.nonNull(vmTemplate) && !Arrays.asList(KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME).contains(vmTemplate.getName())) { @@ -2222,8 +2306,12 @@ private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host de copyTemplateToTargetFilesystemStorageIfNeeded(srcVolumeInfo, sourceStoragePool, destDataStore, destStoragePool, destHost); return MigrationOptions.Type.LinkedClone; } - logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a " + - "template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + + if (sourceIsBlockDevice) { + logger.debug(String.format("Source storage pool [%s] is block device (CLVM/CLVM_NG). Using FullClone migration for volume [%s] to target storage pool [%s]. Template copy skipped as entire volume will be copied.", sourceStoragePool.getId(), srcVolumeInfo.getId(), destStoragePool.getId())); + } else { + logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + } return MigrationOptions.Type.FullClone; } @@ -2289,6 +2377,39 @@ protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo src MigrateCommand.MigrateDiskInfo.Source.DEV, destPath, backingPath); } + /** + * UpdatesMigrateDiskInfo for CLVM/CLVM_NG block devices by returning a new instance with corrected disk type, driver type, and source. + * For CLVM/CLVM_NG destinations, returns a new MigrateDiskInfo with BLOCK disk type, DEV source, and appropriate driver type (QCOW2 for CLVM_NG, RAW for CLVM). + * For other storage types, returns the original MigrateDiskInfo unchanged. + * + * @param migrateDiskInfo The original MigrateDiskInfo object + * @param destStoragePool The destination storage pool + * @return A new MigrateDiskInfo with updated values for CLVM/CLVM_NG, or the original for other storage types + */ + protected MigrateCommand.MigrateDiskInfo updateMigrateDiskInfoForBlockDevice(MigrateCommand.MigrateDiskInfo migrateDiskInfo, + StoragePoolVO destStoragePool) { + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + + MigrateCommand.MigrateDiskInfo.DriverType driverType = + (destStoragePool.getPoolType() == StoragePoolType.CLVM_NG) ? + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2 : + MigrateCommand.MigrateDiskInfo.DriverType.RAW; + + logger.debug("Updating MigrateDiskInfo for {} destination: setting BLOCK disk type, DEV source, and {} driver type", + destStoragePool.getPoolType(), driverType); + + return new MigrateCommand.MigrateDiskInfo( + migrateDiskInfo.getSerialNumber(), + MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, + driverType, + MigrateCommand.MigrateDiskInfo.Source.DEV, + migrateDiskInfo.getSourceText(), + migrateDiskInfo.getBackingStoreText()); + } + + return migrateDiskInfo; + } + /** * Sets the volume path as the iScsi name in case of a configured iScsi. */ @@ -2320,7 +2441,26 @@ String getVolumeBackingFile(VolumeInfo srcVolumeInfo) { return null; } - private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host destHost) { + private void sendClvmLockCommand(long hostId, StoragePoolVO pool, VolumeInfo volumeInfo, + ClvmLockTransferCommand.Operation operation) { + String vgName = pool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, volumeInfo.getPath()); + try { + Answer answer = agentManager.send(hostId, + new ClvmLockTransferCommand(operation, lvPath, volumeInfo.getUuid())); + if (answer == null || !answer.getResult()) { + String details = answer != null ? answer.getDetails() : "null answer"; + logger.warn("CLVM lock command [{}] failed for LV [{}] on host [{}]: {}", operation, lvPath, hostId, details); + } + } catch (AgentUnavailableException | OperationTimedoutException e) { + logger.warn("Exception sending CLVM lock command [{}] for LV [{}] on host [{}]: {}", operation, lvPath, hostId, e.getMessage()); + } + } + + private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host srcHost, Host destHost) { if (!success) { try { PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); @@ -2339,6 +2479,17 @@ private void handlePostMigration(boolean success, Map sr catch (Exception e) { logger.debug("Failed to disconnect one or more (original) dest volumes", e); } + + if (srcHost != null && srcHost.getHypervisorType() == HypervisorType.KVM) { + for (VolumeInfo srcVolumeInfo : srcVolumeInfoToDestVolumeInfo.keySet()) { + StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + if (srcPool == null || !ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + continue; + } + sendClvmLockCommand(srcHost.getId(), srcPool, srcVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + } + } } for (Map.Entry entry : srcVolumeInfoToDestVolumeInfo.entrySet()) { @@ -2349,12 +2500,13 @@ private void handlePostMigration(boolean success, Map sr if (success) { VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); - volumeVO.setFormat(ImageFormat.QCOW2); + StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); + volumeVO.setFormat(destPoolVO != null && destPoolVO.getPoolType() == StoragePoolType.CLVM + ? ImageFormat.RAW : ImageFormat.QCOW2); volumeVO.setLastId(srcVolumeInfo.getId()); if (Objects.equals(srcVolumeInfo.getDiskOfferingId(), destVolumeInfo.getDiskOfferingId())) { - StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); - StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); if (srcPoolVO != null && destPoolVO != null && ((srcPoolVO.isShared() && destPoolVO.isLocal()) || (srcPoolVO.isLocal() && destPoolVO.isShared()))) { Long offeringId = getSuitableDiskOfferingForVolumeOnPool(volumeVO, destPoolVO); @@ -2365,6 +2517,12 @@ private void handlePostMigration(boolean success, Map sr } _volumeDao.update(volumeVO.getId(), volumeVO); + if (destPoolVO != null && ClvmPoolManager.isClvmPoolType(destPoolVO.getPoolType()) + && (srcPoolVO == null || srcPoolVO.getId() != destPoolVO.getId())) { + sendClvmLockCommand(destHost.getId(), destPoolVO, destVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + clvmPoolManager.setClvmLockHostId(destVolumeInfo.getId(), destHost.getId()); + } _volumeService.copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(Event.OperationSucceeded, null, srcVolumeInfo, destVolumeInfo, false); @@ -2373,6 +2531,7 @@ private void handlePostMigration(boolean success, Map sr _snapshotDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); _snapshotDataStoreDao.updateVolumeIds(srcVolumeInfo.getId(), destVolumeInfo.getId()); } + internalBackupService.updateVolumeId(srcVolumeInfo.getId(), destVolumeInfo.getId()); } else { try { @@ -2438,7 +2597,11 @@ private VolumeVO duplicateVolumeOnAnotherStorage(Volume volume, StoragePoolVO st newVol.setLastPoolId(lastPoolId); newVol.setLastId(volume.getId()); - if (volume.getPassphraseId() != null) { + if (volume.getKmsKeyId() != null) { + newVol.setKmsKeyId(volume.getKmsKeyId()); + newVol.setKmsWrappedKeyId(volume.getKmsWrappedKeyId()); + newVol.setEncryptFormat(volume.getEncryptFormat()); + } else if (volume.getPassphraseId() != null) { newVol.setPassphraseId(volume.getPassphraseId()); newVol.setEncryptFormat(volume.getEncryptFormat()); } @@ -2565,8 +2728,7 @@ protected void verifyLiveMigrationForKVM(Map volumeDataSt throw new CloudRuntimeException("Destination storage pool with ID " + dataStore.getId() + " was not located."); } - boolean isSrcAndDestPoolPowerFlexStorage = srcStoragePoolVO.getPoolType().equals(Storage.StoragePoolType.PowerFlex) && destStoragePoolVO.getPoolType().equals(Storage.StoragePoolType.PowerFlex); - if (srcStoragePoolVO.isManaged() && !isSrcAndDestPoolPowerFlexStorage && srcStoragePoolVO.getId() != destStoragePoolVO.getId()) { + if (srcStoragePoolVO.isManaged() && srcStoragePoolVO.getId() != destStoragePoolVO.getId()) { throw new CloudRuntimeException("Migrating a volume online with KVM from managed storage is not currently supported."); } diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java index e167cc0a9653..86af81899e8f 100755 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java @@ -28,14 +28,18 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.any; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -95,12 +99,28 @@ private void overrideDefaultConfigValue(final ConfigKey configKey, final String f.set(configKey, value); } + private ClvmPoolManager injectMockedClvmPoolManager() throws Exception { + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Field clvmPoolManagerField = AncientDataMotionStrategy.class.getDeclaredField("clvmPoolManager"); + clvmPoolManagerField.setAccessible(true); + clvmPoolManagerField.set(strategy, clvmPoolManager); + return clvmPoolManager; + } + @Test public void testAddFullCloneFlagOnVMwareDest(){ strategy.addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(dataTO); verify(dataStoreTO).setFullCloneFlag(FULL_CLONE_FLAG); } + @Test + public void testAddFullCloneFlagOnXenServerDest() throws IllegalAccessException, NoSuchFieldException { + overrideDefaultConfigValue(StorageManager.XenserverCreateCloneFull, String.valueOf(FULL_CLONE_FLAG)); + when(dataTO.getHypervisorType()).thenReturn(HypervisorType.XenServer); + strategy.addFullCloneAndDiskprovisiongStrictnessFlagOnXenServerDest(dataTO); + verify(dataStoreTO).setFullCloneFlag(FULL_CLONE_FLAG); + } + @Test public void testAddFullCloneFlagOnNotVmwareDest(){ verify(dataStoreTO, never()).setFullCloneFlag(any(Boolean.class)); @@ -288,4 +308,185 @@ public void testCanBypassSecondaryStorageWithClusterWideNFSAndZoneWideNFSPoolsIn canBypassSecondaryStorage = (boolean) method.invoke(strategy, destVolumeInfo, srcVolumeInfo); Assert.assertTrue(canBypassSecondaryStorage); } + + @Test + public void testUpdateLockHostForVolume_CLVMPool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 123L; + Long volumeId = 456L; + String volumeUuid = "test-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("test-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_CLVM_NG_Pool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 789L; + Long volumeId = 101L; + String volumeUuid = "test-clvm-ng-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("test-clvm-ng-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + try { + method.invoke(strategy, endPoint, volumeInfo); + } catch (InvocationTargetException e) { + e.getCause().printStackTrace(); + throw e; + } + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_NonCLVMPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + // Create mock that implements both DataStore and StoragePool interfaces + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_ExistingLockHost_DoesNotOverwrite() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 555L; + Long existingHostId = 666L; + Long volumeId = 777L; + String volumeUuid = "existing-lock-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("existing-lock-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(existingHostId); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager).getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true)); + } + + @Test + public void testUpdateLockHostForVolume_NullEndPoint_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, null, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NonVolumeDataObject_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + SnapshotInfo snapshotInfo = Mockito.mock(SnapshotInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, snapshotInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NullPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } } diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java index 808c319b40f2..6f0776b27c8c 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java @@ -168,6 +168,8 @@ public Boolean supportStoragePoolType(StoragePoolType storagePoolType) { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); return supportedTypes.contains(storagePoolType); } @@ -505,6 +507,8 @@ public void validateSupportStoragePoolType() { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); for (StoragePoolType poolType : StoragePoolType.values()) { boolean isSupported = kvmNonManagedStorageDataMotionStrategy.supportStoragePoolType(poolType); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java index 45357fa64b2a..fc51eeba7b6e 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java @@ -369,4 +369,286 @@ public void validateIsStoragePoolTypeInListReturnsFalse() { assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.SharedMountPoint, listTypes)); } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM destination pool + * Should set driver type to RAW for CLVM + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial123", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial123", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM_NG destination pool + * Should set driver type to QCOW2 for CLVM_NG + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmNgDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial456", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.RAW, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + "/backing/path" + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM_NG); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial456", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + Assert.assertEquals("/backing/path", updatedDiskInfo.getBackingStoreText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with non-CLVM destination pool + * Should return original DiskInfo unchanged + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_NonClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial789", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.NetworkFilesystem); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertSame(originalDiskInfo, updatedDiskInfo); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.FILE, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.FILE, updatedDiskInfo.getSource()); + } + + /** + * Test supportStoragePoolType with CLVM and CLVM_NG types + */ + @Test + public void testSupportStoragePoolType_ClvmTypes() { + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM)); + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG)); + } + + /** + * Test configureMigrateDiskInfo with CLVM destination + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvm() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", null); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test configureMigrateDiskInfo with CLVM_NG destination and backing file + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvmNgWithBacking() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", "/dev/vg/backing-template"); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/backing-template", migrateDiskInfo.getBackingStoreText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test isStoragePoolTypeInList with CLVM types + */ + @Test + public void testIsStoragePoolTypeInList_WithClvmTypes() { + StoragePoolType[] clvmTypes = new StoragePoolType[] { + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.Filesystem + }; + + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM_NG, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.Filesystem, clvmTypes)); + assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.NetworkFilesystem, clvmTypes)); + } + + /** + * Test supportStoragePoolType with mixed CLVM and NFS types + */ + @Test + public void testSupportStoragePoolType_MixedClvmAndNfs() { + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM_NG, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.NetworkFilesystem, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG + )); + } + + /** + * Test internalCanHandle with CLVM source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmNgSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with both CLVM source and CLVM_NG destination + */ + @Test + public void testInternalCanHandle_ClvmToClvmNg() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG to CLVM migration + */ + @Test + public void testInternalCanHandle_ClvmNgToClvm() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } } diff --git a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java index e29e89cf431c..6e32df5d5e3c 100644 --- a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java +++ b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java @@ -295,6 +295,171 @@ public void handleSysTemplateDownload(HypervisorType hostHyper, Long dcId) { } } + private int countActiveSecStorageCopies(long templateId, long zoneId) { + List stores = _storeMgr.getImageStoresByScope(new ZoneScope(zoneId)); + if (stores == null || stores.isEmpty()) { + return 0; + } + int count = 0; + for (DataStore ds : stores) { + List rows = _vmTemplateStoreDao.listByTemplateStore(templateId, ds.getId()); + if (rows == null) { + continue; + } + for (TemplateDataStoreVO row : rows) { + State st = row.getState(); + Status ds_state = row.getDownloadState(); + if (st != State.Failed && st != State.Destroyed + && ds_state != Status.ABANDONED && ds_state != Status.DOWNLOAD_ERROR) { + count++; + break; + } + } + } + return count; + } + + /** + * Central gate for the secondary storage copy limit (secstorage.public/private.template.copy.max). + * Every template-landing path (periodic sync, cross-zone copy, register, upload) should consult this + * single method before placing another copy of a template on a secondary store in a zone, so the limit + * is enforced consistently instead of being re-implemented per call site. + * + * SYSTEM/ROUTING/BUILTIN templates and a limit of 0 mean "unlimited" (return true). The per-template, + * per-zone {@link GlobalLock} serializes concurrent placement decisions so racing SSVM syncs / copies + * cannot collectively exceed the limit. + */ + @Override + public boolean canCopyTemplateToImageStore(long templateId, long zoneId) { + VMTemplateVO template = _templateDao.findById(templateId); + if (template == null) { + return false; + } + int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId); + if (copyLimit <= 0) { + logger.debug("Template [{}] has no secondary storage copy limit in zone [{}] (limit={}); copy allowed.", + template.getUniqueName(), zoneId, copyLimit); + return true; + } + int count = countActiveSecStorageCopies(templateId, zoneId); + logger.debug("Template [{}] secstorage copy check in zone [{}]: count={}, limit={}", + template.getUniqueName(), zoneId, count, copyLimit); + return count < copyLimit; + } + + private boolean hasReachedSecStorageCopyLimit(VMTemplateVO template, long zoneId) { + return !canCopyTemplateToImageStore(template.getId(), zoneId); + } + + @Override + public void replicateTemplateUpToCap(long templateId, long zoneId) { + VMTemplateVO template = _templateDao.findById(templateId); + if (template == null) { + return; + } + int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId); + if (copyLimit <= 0) { + return; + } + int needed = copyLimit - countActiveSecStorageCopies(templateId, zoneId); + if (needed <= 0) { + return; + } + List stores = _storeMgr.getImageStoresByScope(new ZoneScope(zoneId)); + if (stores == null || stores.isEmpty()) { + return; + } + int kicked = 0; + for (DataStore store : stores) { + if (kicked >= needed) { + break; + } + if (hasActiveTemplateCopyOnStore(templateId, store.getId())) { + continue; + } + try { + storageOrchestrator.orchestrateTemplateCopyFromSecondaryStores(templateId, store); + kicked++; + } catch (Exception e) { + logger.warn("Failed to proactively replicate template [{}] to image store [{}] in zone [{}]: {}", + template.getUniqueName(), store.getName(), zoneId, e.getMessage()); + } + } + } + + private boolean hasActiveTemplateCopyOnStore(long templateId, long storeId) { + List rows = _vmTemplateStoreDao.listByTemplateStore(templateId, storeId); + if (rows == null) { + return false; + } + for (TemplateDataStoreVO row : rows) { + State st = row.getState(); + Status ds = row.getDownloadState(); + if (st != State.Failed && st != State.Destroyed + && ds != Status.ABANDONED && ds != Status.DOWNLOAD_ERROR) { + return true; + } + } + return false; + } + + @Override + public void enforceSecStorageCopyLimit(long templateId, long zoneId) { + VMTemplateVO template = _templateDao.findById(templateId); + if (template == null) { + return; + } + int copyLimit = _tmpltMgr.getSecStorageCopyLimit(template, zoneId); + if (copyLimit <= 0) { + return; + } + if (_tmpltMgr.verifyHeuristicRulesForZone(template, zoneId) != null) { + return; + } + GlobalLock lock = GlobalLock.getInternLock("template.copy.limit." + templateId + "." + zoneId); + try { + if (!lock.lock(30)) { + logger.warn("Could not acquire lock to enforce secondary storage copy limit for template [{}] in zone [{}].", + template.getUniqueName(), zoneId); + return; + } + List stores = _storeMgr.getImageStoresByScope(new ZoneScope(zoneId)); + if (stores == null) { + return; + } + List removable = new ArrayList<>(); + for (DataStore ds : stores) { + TemplateDataStoreVO ref = _vmTemplateStoreDao.findByStoreTemplate(ds.getId(), templateId); + if (ref != null + && ref.getState() == State.Ready + && ref.getDownloadState() == Status.DOWNLOADED + && (ref.getRefCnt() == null || ref.getRefCnt() == 0)) { + removable.add(ref); + } + } + int excess = removable.size() - copyLimit; + if (excess <= 0) { + return; + } + logger.info("Template [{}] has [{}] removable secondary storage copies in zone [{}], limit is [{}]; removing [{}] excess copies.", + template.getUniqueName(), removable.size(), zoneId, copyLimit, excess); + for (int i = 0; i < excess; i++) { + DataStore ds = _storeMgr.getDataStore(removable.get(i).getDataStoreId(), DataStoreRole.Image); + try { + deleteTemplateAsync(_templateFactory.getTemplate(templateId, ds)); + logger.info("Removed excess copy of template [{}] from image store [{}] to honor the secondary storage copy limit.", + template.getUniqueName(), ds.getName()); + } catch (Exception e) { + logger.warn("Failed to remove excess copy of template [{}] from image store [{}]: {}", + template.getUniqueName(), ds, e.getMessage()); + } + } + } finally { + lock.unlock(); + lock.releaseRef(); + } + } + protected boolean shouldDownloadTemplateToStore(VMTemplateVO template, DataStore store) { Long zoneId = store.getScope().getScopeId(); DataStore directedStore = _tmpltMgr.verifyHeuristicRulesForZone(template, zoneId); @@ -304,6 +469,12 @@ protected boolean shouldDownloadTemplateToStore(VMTemplateVO template, DataStore return false; } + if (zoneId != null && hasReachedSecStorageCopyLimit(template, zoneId)) { + logger.info("Skipping sync of template [{}] to image store [{}]: zone [{}] has reached the configured copy limit.", + template.getUniqueName(), store.getName(), zoneId); + return false; + } + if (template.isPublicTemplate()) { logger.debug("Download of template [{}] to image store [{}] cannot be skipped, as it is public.", template.getUniqueName(), store.getName()); @@ -328,8 +499,9 @@ protected boolean shouldDownloadTemplateToStore(VMTemplateVO template, DataStore return true; } - logger.info("Skipping download of template [{}] to image store [{}].", template.getUniqueName(), store.getName()); - return false; + logger.debug("Copying template [{}] to image store [{}] to reach the configured secondary storage copy limit in zone [{}].", + template.getUniqueName(), store.getName(), zoneId); + return true; } @Override @@ -531,10 +703,13 @@ public void handleTemplateSync(DataStore store) { && tmpltStore.getState() == State.Ready && tmpltStore.getInstallPath() == null) { logger.info("Keep fake entry in template store table for migration of previous NFS to object store"); - } else { + } else if (tmpltStore.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOADED + || tmpltStore.getState() == State.Ready) { logger.info("Removing leftover template {} entry from template store table", tmplt); - // remove those leftover entries _vmTemplateStoreDao.remove(tmpltStore.getId()); + } else { + logger.debug("Template {} entry on store {} is in pre-download state ({}/{}); not treating as leftover.", + tmplt, store, tmpltStore.getState(), tmpltStore.getDownloadState()); } } } @@ -556,7 +731,7 @@ public void handleTemplateSync(DataStore store) { availHypers.add(HypervisorType.None); // bug 9809: resume ISO // download. for (VMTemplateVO tmplt : toBeDownloaded) { - // if this is private template, skip sync to a new image store + // skip stores excluded by heuristic rules or already at the configured copy limit if (!shouldDownloadTemplateToStore(tmplt, store)) { continue; } @@ -580,6 +755,12 @@ public void handleTemplateSync(DataStore store) { } } + if (zoneId != null) { + for (VMTemplateVO tmplt : allTemplates) { + enforceSecStorageCopyLimit(tmplt.getId(), zoneId); + } + } + for (String uniqueName : templateInfos.keySet()) { TemplateProp tInfo = templateInfos.get(uniqueName); if (_tmpltMgr.templateIsDeleteable(tInfo.getId())) { @@ -965,6 +1146,15 @@ protected Void createTemplateCallback(AsyncCallbackDispatcher snapshotStatesAbleToDeleteSnapshot = Arrays.asList(Snapshot.State.Destroying, Snapshot.State.Destroyed, Snapshot.State.Error, Snapshot.State.Hidden); public SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneId) { - List snaps = snapshotStoreDao.listReadyBySnapshot(snapshotId, DataStoreRole.Image); + List snaps = snapshotStoreDao.listBySnapshotIdAndDataStoreRoleAndStateIn(snapshotId, DataStoreRole.Image, State.Ready, State.Hidden); for (SnapshotDataStoreVO ref : snaps) { if (zoneId == dataStoreMgr.getStoreZoneId(ref.getDataStoreId(), ref.getRole())) { return ref; @@ -643,6 +644,10 @@ public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperat return StrategyPriority.DEFAULT; } + if (isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)) { + return StrategyPriority.DEFAULT; + } + return StrategyPriority.CANT_HANDLE; } if (zoneId != null && SnapshotOperation.DELETE.equals(op)) { @@ -691,4 +696,32 @@ protected boolean isSnapshotStoredOnSameZoneStoreForQCOW2Volume(Snapshot snapsho dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); } + /** + * Checks if a CLVM volume snapshot is stored on secondary storage in the same zone. + * CLVM snapshots are backed up to secondary storage and removed from primary storage. + */ + protected boolean isSnapshotStoredOnSecondaryForCLVMVolume(Snapshot snapshot, VolumeVO volumeVO) { + if (volumeVO == null) { + return false; + } + + Long poolId = volumeVO.getPoolId(); + if (poolId == null) { + return false; + } + + StoragePool pool = (StoragePool) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return false; + } + + List snapshotStores = snapshotStoreDao.listReadyBySnapshot(snapshot.getId(), DataStoreRole.Image); + if (CollectionUtils.isEmpty(snapshotStores)) { + return false; + } + + return snapshotStores.stream().anyMatch(s -> Objects.equals( + dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); + } + } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java index 95345bdf9e0e..a94c04ff4aae 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotServiceImpl.java @@ -30,6 +30,7 @@ import com.cloud.storage.Volume; import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; import org.apache.cloudstack.engine.subsystem.api.storage.DataMotionService; @@ -116,6 +117,8 @@ public class SnapshotServiceImpl implements SnapshotService { ConfigurationDao _configDao; @Inject HostDao hostDao; + @Inject + private InternalBackupService internalBackupService; @Inject private HeuristicRuleHelper heuristicRuleHelper; @@ -603,6 +606,7 @@ protected Void revertSnapshotCallback(AsyncCallbackDispatcher volumes = volumeDao.findByInstance(vmId); + for (VolumeVO volume : volumes) { + StoragePool pool = primaryDataStoreDao.findById(volume.getPoolId()); + if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + logger.warn("Rejecting VM snapshot request: {} - VM is running on CLVM storage (pool: {}, poolType: CLVM)", + cantHandleLog, pool.getName()); + return true; + } + } + } + return false; + } + @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { UserVmVO vm = userVmDao.findById(vmId); String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm); + + if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if (State.Running.equals(vm.getState()) && !snapshotMemory) { logger.debug("{} as it is running and its memory will not be affected.", cantHandleLog, vm); return StrategyPriority.CANT_HANDLE; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java index 003065e394f5..14e6e2b12edc 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java @@ -18,6 +18,40 @@ */ package org.apache.cloudstack.storage.vmsnapshot; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.backup.BackupManagerImpl; +import org.apache.cloudstack.backup.BackupOfferingVO; +import org.apache.cloudstack.backup.InternalBackupService; +import org.apache.cloudstack.backup.InternalBackupStoragePoolVO; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.snapshot.SnapshotObject; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections.CollectionUtils; + import com.cloud.agent.api.Answer; import com.cloud.agent.api.VMSnapshotTO; import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer; @@ -26,7 +60,6 @@ import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand; import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer; import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand; -import com.cloud.agent.api.storage.SnapshotMergeTreeTO; import com.cloud.agent.api.to.DataTO; import com.cloud.configuration.Resource; import com.cloud.event.EventTypes; @@ -38,6 +71,7 @@ import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDao; import com.cloud.user.ResourceLimitService; import com.cloud.uservm.UserVm; import com.cloud.utils.DateUtil; @@ -49,29 +83,6 @@ import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.VMSnapshotVO; -import org.apache.cloudstack.backup.BackupOfferingVO; -import org.apache.cloudstack.backup.dao.BackupOfferingDao; -import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; -import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; -import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.snapshot.SnapshotObject; -import org.apache.cloudstack.storage.to.SnapshotObjectTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.commons.collections.CollectionUtils; - -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.stream.Collectors; public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStrategy { @@ -86,6 +97,16 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra @Inject protected BackupOfferingDao backupOfferingDao; + @Inject + private InternalBackupService internalBackupService; + + @Inject + private InternalBackupStoragePoolDao internalBackupStoragePoolDao; + + @Inject + private SnapshotDao snapshotDao; + + @Override public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { Map volumeInfoToSnapshotObjectMap = new HashMap<>(); @@ -137,11 +158,13 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs); } else if (!isCurrent && numberOfChildren == 1) { VMSnapshotVO childSnapshot = snapshotChildren.get(0); - volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId); + volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, hostId); } + Date removedDate = DateUtil.now(); for (SnapshotVO snapshotVO : volumeSnapshotVos) { snapshotVO.setState(Snapshot.State.Destroyed); + snapshotVO.setRemoved(removedDate); snapshotDao.update(snapshotVO.getId(), snapshotVO); } @@ -179,7 +202,9 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.RevertRequested); - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotBeingReverted); + internalBackupService.prepareVmForSnapshotRevert(vmSnapshot); + + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotBeingReverted.getId()); List volumeSnapshotTos = volumeSnapshots.stream() .map(snapshot -> (SnapshotObjectTO) snapshotDataFactory.getSnapshot(snapshot.getSnapshotId(), snapshot.getDataStoreId(), DataStoreRole.Primary).getTO()) .collect(Collectors.toList()); @@ -268,7 +293,7 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen VMSnapshotVO oldSibling = oldSiblings.get(0); logger.debug("Merging VM snapshot [{}] with [{}] as the former was hidden and only the latter depends on it.", oldParent.getUuid(), oldSibling.getUuid()); - snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, volumeTOs, hostId); + snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, hostId); } for (SnapshotVO snapshotVO : snapshotVos) { @@ -325,8 +350,13 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe List volumes = volumeDao.findByInstance(vmId); for (VolumeVO volume : volumes) { StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId()); + if (storagePoolVO.isManaged() && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(storagePoolVO.getStorageProviderName())) { + logger.debug("{} as the VM has a volume on ONTAP managed storage pool [{}]. " + + "ONTAP managed storage has its own dedicated VM snapshot strategy.", cantHandleLog, storagePoolVO.getName()); + return StrategyPriority.CANT_HANDLE; + } if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { - logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType())); + logger.debug("{} as the VM has a volume that is in a storage with unsupported type [{}].", cantHandleLog, storagePoolVO.getPoolType()); return StrategyPriority.CANT_HANDLE; } List snapshots = snapshotDao.listByVolumeIdAndTypeNotInAndStateNotRemoved(volume.getId(), Snapshot.Type.GROUP); @@ -338,8 +368,8 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe } BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId()); - if (backupOffering != null) { - logger.debug("{} as the VM has a backup offering. This strategy does not support snapshots on VMs with current backup providers.", cantHandleLog); + if (backupOffering != null && !backupOffering.getProvider().equals(BackupManagerImpl.KBOSS_BACKUP_PROVIDER)) { + logger.debug("{} as the VM has a backup offering for a provider that is not supported. This strategy only supports the KBOSS backup provider.", cantHandleLog); return StrategyPriority.CANT_HANDLE; } @@ -347,7 +377,7 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe } private List deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) { - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO); + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot((vmSnapshotVO.getId())); List volumeSnapshotTOList = volumeSnapshots.stream() .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) .collect(Collectors.toList()); @@ -368,7 +398,7 @@ private List deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) return snapshotVOList; } - private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List volumeObjectTOS, Long hostId) { + private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, Long hostId) { logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); List snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); @@ -378,18 +408,10 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO removeCurrentBackingChainSnapshotFromVmSnapshotList(snapshotGrandChildren, userVm); } - List snapshotMergeTreeToList = generateSnapshotMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren); - - if (childSnapshot.getCurrent() && !VirtualMachine.State.Running.equals(userVm.getState())) { - for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - snapshotMergeTreeToList.stream().filter(snapshotTree -> Objects.equals(((SnapshotObjectTO) snapshotTree.getParent()).getVolume().getId(), volumeObjectTO.getId())) - .findFirst() - .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))) - .addGrandChild(volumeObjectTO); - } - } + List deltaMergeTreeTOs = generateDeltaMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren, + !userVm.getState().equals(VirtualMachine.State.Running)); - MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeToList, userVm.getState(), userVm.getName()); + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVm.getState().equals(VirtualMachine.State.Running), userVm.getName()); Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); if (answer == null || !answer.getResult()) { throw new CloudRuntimeException(String.format("Failed to merge VM snapshot [%s] due to %s.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure")); @@ -397,15 +419,23 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO logger.debug("Updating metadata of VM snapshot [{}] and its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); List snapshotVOList = new ArrayList<>(); - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeToList) { - SnapshotObjectTO childTO = (SnapshotObjectTO) snapshotMergeTreeTO.getChild(); - SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); - - SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary); - childSnapshotDataStoreVO.setInstallPath(parentTO.getPath()); - snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + DataTO childTO = deltaMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) deltaMergeTreeTO.getParent(); + + if (childTO instanceof BackupDeltaTO) { + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(parentTO.getVolume().getVolumeId()); + backupDelta.setBackupDeltaParentPath(parentTO.getPath()); + logger.debug("The child was also a KBOSS backup delta, will update the backup delta metadata. Updating backupDeltaParentPath of backupDelta [{}] to [{}].", backupDelta.getId(), parentTO.getPath()); + internalBackupStoragePoolDao.update(backupDelta.getId(), backupDelta); + } else { + SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary); + childSnapshotDataStoreVO.setInstallPath(parentTO.getPath()); + logger.debug("Updating the child path [{}] to [{}].", childSnapshotDataStoreVO.getId(), parentTO.getPath()); + snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO); + } - snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), childSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary); + snapshotDataStoreDao.expungeBySnapshotIdAndStoreRole(parentTO.getId(), DataStoreRole.Primary); snapshotVOList.add(snapshotDao.findById(parentTO.getId())); } @@ -417,18 +447,32 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid()); - List snapshotMergeTreeTOList = new ArrayList<>(); - List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVo); + List deltaMergeTreeTOs = new ArrayList<>(); + List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVo.getId()); for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeObjectTO.getId())) .findFirst() .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))); DataTO parentSnapshot = snapshotDataFactory.getSnapshot(volumeParentSnapshot.getSnapshotId(), volumeParentSnapshot.getDataStoreId(), DataStoreRole.Primary).getTO(); - snapshotMergeTreeTOList.add(new SnapshotMergeTreeTO(parentSnapshot, volumeObjectTO, new ArrayList<>())); + + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(volumeObjectTO.getVolumeId()); + + if (backupDelta != null && backupDelta.getBackupDeltaPath().equals(volumeObjectTO.getPath())) { + logger.debug("The current volume delta is also a KBOSS backup delta. Will merge the snapshot delta of volume [{}] with the parent backup delta at [{}].", + volumeObjectTO.getUuid(), backupDelta.getBackupDeltaParentPath()); + BackupDeltaTO childTo = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, backupDelta.getBackupDeltaParentPath()); + ArrayList grandChildren = new ArrayList<>(); + if (userVmVO.getState().equals(VirtualMachine.State.Stopped)) { + grandChildren.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, backupDelta.getBackupDeltaPath())); + } + deltaMergeTreeTOs.add(new DeltaMergeTreeTO(volumeObjectTO, parentSnapshot, childTo, grandChildren)); + } else { + deltaMergeTreeTOs.add(new DeltaMergeTreeTO(volumeObjectTO, parentSnapshot, volumeObjectTO, new ArrayList<>())); + } } - MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeTOList, userVmVO.getState(), userVmVO.getName()); + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVmVO.getState().equals(VirtualMachine.State.Running), userVmVO.getName()); Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); if (answer == null || !answer.getResult()) { @@ -437,13 +481,20 @@ private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVo.getUuid()); List snapshotVOList = new ArrayList<>(); - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) { - VolumeObjectTO volumeObjectTO = (VolumeObjectTO) snapshotMergeTreeTO.getChild(); - SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); - - VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); - volumeVO.setPath(parentTO.getPath()); - volumeDao.update(volumeVO.getId(), volumeVO); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + DataTO dataTO = deltaMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) deltaMergeTreeTO.getParent(); + VolumeVO volumeVO = volumeDao.findById(parentTO.getVolume().getId()); + + if (dataTO instanceof BackupDeltaTO) { + logger.debug("The child of deltaMergeTree [{}] is a backupDeltaTO, thus, we will update the backup delta metadata.", deltaMergeTreeTO); + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(parentTO.getVolume().getVolumeId()); + backupDelta.setBackupDeltaParentPath(parentTO.getPath()); + internalBackupStoragePoolDao.update(backupDelta.getId(), backupDelta); + } else { + volumeVO.setPath(parentTO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + } snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), volumeVO.getPoolId(), DataStoreRole.Primary); snapshotVOList.add(snapshotDao.findById(parentTO.getId())); @@ -489,11 +540,12 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map> volumeTosAndNewPaths = volumeTOs.stream().map(volume -> new Pair<>(volume, UUID.randomUUID().toString())).collect(Collectors.toList()); + long virtualSize = createVolumeSnapshotMetadataAndCalculateVirtualSize(vmSnapshot, volumeInfoToSnapshotObjectMap, volumeTosAndNewPaths); VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, parentSnapshotTo, quiesceVm); - CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTOs, null, userVm.getState()); + CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTosAndNewPaths, null, userVm.getState()); logger.info("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}].", vmSnapshot.getUuid(), hostId); Answer answer = agentMgr.easySend(hostId, ccmd); @@ -503,8 +555,9 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, CreateDiskOnlyVmSnapshotAnswer answer, UserVm userVm, VMSnapshotVO vmSnapshotVO, long virtualSize, VMSnapshotVO parentSnapshotVo) throws NoTransitionException { logger.debug("Processing CreateDiskOnlyVMSnapshotCommand answer for disk-only VM snapshot [{}].", vmSnapshot.getUuid()); - Map> volumeUuidToSnapshotSizeAndNewVolumePathMap = answer.getMapVolumeToSnapshotSizeAndNewVolumePath(); + Map volumeUuidToSnapshotSize = answer.getMapVolumeToSnapshotSize(); long vmSnapshotSize = 0; for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) { VolumeVO volumeVO = (VolumeVO) volumeInfo.getVolume(); - Pair snapSizeAndNewVolumePath = volumeUuidToSnapshotSizeAndNewVolumePathMap.get(volumeVO.getUuid()); + Long snapSize = volumeUuidToSnapshotSize.get(volumeVO.getUuid()); SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo); snapshot.markBackedUp(); @@ -525,14 +578,15 @@ private VMSnapshot processCreateVmSnapshotAnswer(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, List volumeTOs) throws NoTransitionException { + private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, + List> volumeToAndNewPaths) throws NoTransitionException { long virtualSize = 0; - for (VolumeObjectTO volumeObjectTO : volumeTOs) { + for (Pair volumeToAndPath : volumeToAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeToAndPath.first(); VolumeInfo volumeInfo = volumeDataFactory.getVolume(volumeObjectTO.getId()); volumeInfo.stateTransit(Volume.Event.SnapshotRequested); virtualSize += volumeInfo.getSize(); @@ -577,61 +633,77 @@ private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSn snapshotOnPrimary.processEvent(Snapshot.Event.CreateRequested); snapshotOnPrimary.processEvent(ObjectInDataStoreStateMachine.Event.CreateOnlyRequested); + SnapshotDataStoreVO snapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotId(snapshot.getId()).get(0); + snapshotDataStoreVO.setInstallPath(volumeToAndPath.second()); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); volumeInfoToSnapshotObjectMap.put(volumeInfo, snapshotOnPrimary); } return virtualSize; } - private List generateSnapshotMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren) throws NoSuchElementException { + /** + * Generates the delta merge trees, taking internal backups into account. + * */ + private List generateDeltaMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren, boolean stoppedVm) throws NoSuchElementException { logger.debug("Generating list of Snapshot Merge Trees for the merge process of VM Snapshot [{}].", parent.getUuid()); - List snapshotMergeTrees = new ArrayList<>(); - List parentVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(parent); - List childVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(child); + List snapshotMergeTrees = new ArrayList<>(); + List parentVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(parent.getId()); + List childVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(child.getId()); List grandChildrenVolumeSnapshots = new ArrayList<>(); for (VMSnapshotVO grandChild : grandChildren) { - grandChildrenVolumeSnapshots.addAll(getVolumeSnapshotsAssociatedWithVmSnapshot(grandChild)); + grandChildrenVolumeSnapshots.addAll(vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(grandChild.getId())); } for (SnapshotDataStoreVO parentSnapshotDataStoreVO : parentVolumeSnapshots) { - DataTO parentTO = snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + VolumeObjectTO volumeObjectTO = parentTO.getVolume(); - DataTO childTO = childVolumeSnapshots.stream() + SnapshotDataStoreVO childVO = childVolumeSnapshots.stream() .filter(childSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), childSnapshot.getVolumeId())) - .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) .findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Could not find child snapshot of parent [%s].", parentSnapshotDataStoreVO.getSnapshotId()))); - List grandChildrenTOList = grandChildrenVolumeSnapshots.stream() - .filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId())) - .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) - .collect(Collectors.toList()); + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(childVO.getVolumeId()); + List grandChildrenTOList = new ArrayList<>(); + DataTO childTO = getChildAndGrandChildren(child, stoppedVm, parentSnapshotDataStoreVO, backupDelta, childVO, volumeObjectTO, grandChildrenTOList, + grandChildrenVolumeSnapshots); - snapshotMergeTrees.add(new SnapshotMergeTreeTO(parentTO, childTO, grandChildrenTOList)); + snapshotMergeTrees.add(new DeltaMergeTreeTO(volumeObjectTO, parentTO, childTO, grandChildrenTOList)); } - logger.debug("Generated the following list of Snapshot Merge Trees for the VM snapshot [{}]: [{}].", parent.getUuid(), snapshotMergeTrees); + logger.debug(String.format("Generated the following list of Snapshot Merge Trees for the VM snapshot [%s]: [%s].", parent.getUuid(), snapshotMergeTrees)); return snapshotMergeTrees; } /** - * For a given {@code VMSnapshotVO}, populates the {@code associatedVolumeSnapshots} list with all the volume snapshots that are - * part of the VMSnapshot. - * @param vmSnapshot the VMSnapshotVO that will have its size calculated - * @return the list that will be populated with the volume snapshots associated with the VM snapshot. + * Gets the correct children and grandchildren, taking KBOSS backups into account. * */ - private List getVolumeSnapshotsAssociatedWithVmSnapshot(VMSnapshotVO vmSnapshot) { - List associatedVolumeSnapshots = new ArrayList<>(); - List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT); - for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { - SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); - if (snapshot == null) { - throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid())); + private DataTO getChildAndGrandChildren(VMSnapshotVO child, boolean stoppedVm, SnapshotDataStoreVO parentSnapshotDataStoreVO, InternalBackupStoragePoolVO backupDelta, + SnapshotDataStoreVO childVO, VolumeObjectTO volumeObjectTO, List grandChildrenTOList, List grandChildrenVolumeSnapshots) { + + DataTO childTO; + if (backupDelta != null && backupDelta.getBackupDeltaPath().equals(childVO.getInstallPath())) { + logger.debug("The child snapshot delta is also a backup delta. We will set the backup delta parent path [{}] as the child and the backup delta path [{}] " + + "as the grand-child.", backupDelta.getBackupDeltaParentPath(), backupDelta.getBackupDeltaPath()); + childTO = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, backupDelta.getBackupDeltaParentPath()); + if (!child.getCurrent() && stoppedVm) { + grandChildrenTOList.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, backupDelta.getBackupDeltaPath())); } - associatedVolumeSnapshots.add(snapshot); + } else { + childTO = snapshotDataFactory.getSnapshot(childVO.getSnapshotId(), childVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + grandChildrenTOList.addAll(grandChildrenVolumeSnapshots.stream() + .filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId())) + .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) + .collect(Collectors.toList())); } - return associatedVolumeSnapshots; + + if (child.getCurrent() && stoppedVm) { + grandChildrenTOList.add(volumeObjectTO); + } + + return childTO; } /** diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java index 7199fce1d347..aced750bd320 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/ScaleIOVMSnapshotStrategy.java @@ -40,6 +40,7 @@ import org.apache.cloudstack.storage.datastore.util.ScaleIOUtil; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; +import org.apache.cloudstack.storage.datastore.api.Volume; import com.cloud.agent.api.VMSnapshotTO; import com.cloud.alert.AlertManager; @@ -200,11 +201,35 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { if (volumeIds != null && !volumeIds.isEmpty()) { List vmSnapshotDetails = new ArrayList(); vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), "SnapshotGroupId", snapshotGroupId, false)); + Map snapshotNameToSrcPathMap = new HashMap<>(); + for (Map.Entry entry : srcVolumeDestSnapshotMap.entrySet()) { + snapshotNameToSrcPathMap.put(entry.getValue(), entry.getKey()); + } + + for (String snapshotVolumeId : volumeIds) { + // Use getVolume() to fetch snapshot volume details and get its name + Volume snapshotVolume = client.getVolume(snapshotVolumeId); + if (snapshotVolume == null) { + throw new CloudRuntimeException("Cannot find snapshot volume with id: " + snapshotVolumeId); + } + String snapshotName = snapshotVolume.getName(); + + // Match back to source volume path + String srcVolumePath = snapshotNameToSrcPathMap.get(snapshotName); + if (srcVolumePath == null) { + throw new CloudRuntimeException("Cannot match snapshot " + snapshotName + " to a source volume"); + } + + // Find the matching VolumeObjectTO by path + VolumeObjectTO matchedVolume = volumeTOs.stream() + .filter(v -> ScaleIOUtil.getVolumePath(v.getPath()).equals(srcVolumePath)) + .findFirst() + .orElseThrow(() -> new CloudRuntimeException("Cannot find source volume for path: " + srcVolumePath)); - for (int index = 0; index < volumeIds.size(); index++) { - String volumeSnapshotName = srcVolumeDestSnapshotMap.get(ScaleIOUtil.getVolumePath(volumeTOs.get(index).getPath())); - String pathWithScaleIOVolumeName = ScaleIOUtil.updatedPathWithVolumeName(volumeIds.get(index), volumeSnapshotName); - vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), "Vol_" + volumeTOs.get(index).getId() + "_Snapshot", pathWithScaleIOVolumeName, false)); + String pathWithScaleIOVolumeName = ScaleIOUtil.updatedPathWithVolumeName(snapshotVolumeId, snapshotName); + vmSnapshotDetails.add(new VMSnapshotDetailsVO(vmSnapshot.getId(), + "Vol_" + matchedVolume.getId() + "_Snapshot", + pathWithScaleIOVolumeName, false)); } vmSnapshotDetailsDao.saveDetails(vmSnapshotDetails); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index e3f28a7012c2..fd306414ad49 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -37,6 +37,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; @@ -95,6 +97,9 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy { @Inject VMSnapshotDetailsDao vmSnapshotDetailsDao; + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + @Override public boolean configure(String name, Map params) throws ConfigurationException { return super.configure(name, params); @@ -111,7 +116,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { FreezeThawVMAnswer freezeAnswer = null; FreezeThawVMCommand thawCmd = null; FreezeThawVMAnswer thawAnswer = null; - List forRollback = new ArrayList<>(); + List snapshotsForRollback = new ArrayList<>(); long startFreeze = 0; try { vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshotVO, VMSnapshot.Event.CreateRequested); @@ -165,7 +170,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { logger.info("The virtual machine is frozen"); for (VolumeInfo vol : vinfos) { long startSnapshtot = System.nanoTime(); - SnapshotInfo snapInfo = createDiskSnapshot(vmSnapshot, forRollback, vol); + SnapshotInfo snapInfo = createDiskSnapshot(vmSnapshot, snapshotsForRollback, vol); if (snapInfo == null) { thawAnswer = (FreezeThawVMAnswer) agentMgr.send(hostId, thawCmd); @@ -222,7 +227,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } } if (!result) { - for (SnapshotInfo snapshotInfo : forRollback) { + for (SnapshotInfo snapshotInfo : snapshotsForRollback) { rollbackDiskSnapshot(snapshotInfo); } try { @@ -345,6 +350,13 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { } } + Long vmId = vmSnapshot.getVmId(); + UserVmVO vm = userVmDao.findById(vmId); + String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm); + if (vm != null && isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if ( SnapshotManager.VmStorageSnapshotKvm.value() && userVm.getHypervisorType() == Hypervisor.HypervisorType.KVM && vmSnapshot.getType() == VMSnapshot.Type.Disk) { return StrategyPriority.HYPERVISOR; @@ -367,6 +379,17 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe return StrategyPriority.CANT_HANDLE; } + for (VolumeVO volume : volumeDao.findByInstance(vmId)) { + List snapshots = snapshotDataStoreDao.listReadyByVolumeIdAndCheckpointPathNotNull(volume.getId()); + if (CollectionUtils.isNotEmpty(snapshots)) { + logger.debug( + "{} as VM has a volume with incremental snapshots {}. Incremental volume snapshots and StorageVmSnapshotStrategy are not compatible," + + " as restoring VM snapshots will erase the bitmaps and destroy snapshot chains.", + cantHandleLog, snapshots); + return StrategyPriority.CANT_HANDLE; + } + } + if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) { return StrategyPriority.HYPERVISOR; } @@ -388,10 +411,16 @@ private long elapsedTime(long startTime) { //Rollback if one of disks snapshot fails protected void rollbackDiskSnapshot(SnapshotInfo snapshotInfo) { + if (snapshotInfo == null) { + return; + } Long snapshotID = snapshotInfo.getId(); SnapshotVO snapshot = snapshotDao.findById(snapshotID); + if (snapshot == null) { + return; + } deleteSnapshotByStrategy(snapshot); - logger.debug("Rollback is executed: deleting snapshot with id:" + snapshotID); + logger.debug("Rollback is executed: deleting snapshot with id: {}", snapshotID); } protected void deleteSnapshotByStrategy(SnapshotVO snapshot) { @@ -434,7 +463,7 @@ protected void revertDiskSnapshot(VMSnapshot vmSnapshot) { } } - protected SnapshotInfo createDiskSnapshot(VMSnapshot vmSnapshot, List forRollback, VolumeInfo vol) { + protected SnapshotInfo createDiskSnapshot(VMSnapshot vmSnapshot, List snapshotsForRollback, VolumeInfo vol) { String snapshotName = vmSnapshot.getId() + "_" + vol.getUuid(); SnapshotVO snapshot = new SnapshotVO(vol.getDataCenterId(), vol.getAccountId(), vol.getDomainId(), vol.getId(), vol.getDiskOfferingId(), snapshotName, (short) Snapshot.Type.GROUP.ordinal(), Snapshot.Type.GROUP.name(), vol.getSize(), vol.getMinIops(), vol.getMaxIops(), Hypervisor.HypervisorType.KVM, null); @@ -448,6 +477,7 @@ protected SnapshotInfo createDiskSnapshot(VMSnapshot vmSnapshot, List()); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInDifferentZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(112L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsOneMatches() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore3 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2, snapshotStore3)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolNullZoneIds() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolVolumeNullDataCenter() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(1L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsAllInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } } diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java index da377f96ec32..365ba3d4eb31 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.UUID; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -39,6 +40,10 @@ import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshot; @RunWith(MockitoJUnitRunner.class) public class DefaultVMSnapshotStrategyTest { @@ -46,6 +51,8 @@ public class DefaultVMSnapshotStrategyTest { VolumeDao volumeDao; @Mock PrimaryDataStoreDao primaryDataStoreDao; + @Mock + UserVmDao userVmDao; @Spy @InjectMocks @@ -85,7 +92,7 @@ public void testUpdateVolumePath() { Mockito.when(vol2.getChainInfo()).thenReturn(newVolChain); Mockito.when(vol2.getSize()).thenReturn(vmSnapshotChainSize); Mockito.when(vol2.getId()).thenReturn(volumeId); - VolumeVO volumeVO = new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT); + VolumeVO volumeVO = new VolumeVO("name", 0L, 0L, 0L, 0L, 0L, "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); volumeVO.setPoolId(oldPoolId); volumeVO.setChainInfo(oldVolChain); volumeVO.setPath(oldVolPath); @@ -103,4 +110,110 @@ public void testUpdateVolumePath() { Assert.assertEquals(vmSnapshotChainSize, persistedVolume.getVmSnapshotChainSize()); Assert.assertEquals(newVolChain, persistedVolume.getChainInfo()); } + + @Test + public void testCanHandleRunningVMOnClvmStorageCantHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + List volumes = List.of(volumeOnClvm); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE for running VM on CLVM storage", + StrategyPriority.CANT_HANDLE, result); + } + + @Test + public void testCanHandleStoppedVMOnClvmStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Stopped); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + Assert.assertEquals("Should return DEFAULT for stopped VM on CLVM storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMOnNfsStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnNfs = createVolume(vmId, 1L); + List volumes = List.of(volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(nfsPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return DEFAULT for running VM on NFS storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMWithMixedStorageClvmAndNfsCantHandle() { + // Arrange - VM has volumes on both CLVM and NFS + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + VolumeVO volumeOnNfs = createVolume(vmId, 2L); + List volumes = List.of(volumeOnClvm, volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE if any volume is on CLVM storage for running VM", + StrategyPriority.CANT_HANDLE, result); + } + + private VolumeVO createVolume(Long vmId, Long poolId) { + VolumeVO volume = new VolumeVO("volume", 0L, 0L, 0L, 0L, 0L, + "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); + volume.setInstanceId(vmId); + volume.setPoolId(poolId); + return volume; + } + + private StoragePoolVO createStoragePool(String name, Storage.StoragePoolType poolType) { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool.getName()).thenReturn(name); + Mockito.when(pool.getPoolType()).thenReturn(poolType); + return pool; + } } diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java index 050c0246abaf..7584ae1c986c 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyKVMTest.java @@ -44,6 +44,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.test.utils.SpringUtils; @@ -155,7 +156,7 @@ public void setUp() throws Exception { @Test public void testCreateDiskSnapshotBasedOnStrategy() throws Exception { VMSnapshotVO vmSnapshot = Mockito.mock(VMSnapshotVO.class); - List forRollback = new ArrayList<>(); + List snapshotsForRollback = new ArrayList<>(); VolumeInfo vol = Mockito.mock(VolumeInfo.class); SnapshotInfo snapshotInfo = Mockito.mock(SnapshotInfo.class); SnapshotStrategy strategy = Mockito.mock(SnapshotStrategy.class); @@ -179,7 +180,7 @@ public void testCreateDiskSnapshotBasedOnStrategy() throws Exception { VMSnapshotDetailsVO vmDetails = new VMSnapshotDetailsVO(vmSnapshot.getId(), volUuid, String.valueOf(snapshot.getId()), false); when(vmSnapshotDetailsDao.persist(any())).thenReturn(vmDetails); - info = vmStrategy.createDiskSnapshot(vmSnapshot, forRollback, vol); + info = vmStrategy.createDiskSnapshot(vmSnapshot, snapshotsForRollback, vol); assertNotNull(info); } @@ -442,5 +443,10 @@ public BackupOfferingDao backupOfferingDao() { public BackupManager backupManager() { return Mockito.mock(BackupManager.class); } + + @Bean + public SnapshotDataStoreDao snapshotDataStoreDao() { + return Mockito.mock(SnapshotDataStoreDao.class); + } } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java index cd9ab3adb210..4057f7a051bf 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java @@ -145,10 +145,10 @@ protected List reorderPoolsByCapacity(DeploymentPlan plan, List, Map> result = capacityDao.orderHostsByFreeCapacity(zoneId, clusterId, capacityType); List poolIdsByCapacity = result.first(); @@ -185,7 +185,7 @@ protected List reorderPoolsByNumberOfVolumes(DeploymentPlan plan, L Long clusterId = plan.getClusterId(); List poolIdsByVolCount = volumeDao.listPoolIdsByVolumeCount(dcId, podId, clusterId, account.getAccountId()); - logger.debug(String.format("List of pools in ascending order of number of volumes for account [%s] is [%s].", account, poolIdsByVolCount)); + logger.debug("List of pools in ascending order of number of volumes for account [{}] is [{}].", account, poolIdsByVolCount); // now filter the given list of Pools by this ordered list Map poolMap = new HashMap<>(); @@ -206,16 +206,11 @@ protected List reorderPoolsByNumberOfVolumes(DeploymentPlan plan, L @Override public List reorderPools(List pools, VirtualMachineProfile vmProfile, DeploymentPlan plan, DiskProfile dskCh) { - if (logger.isTraceEnabled()) { - logger.trace("reordering pools"); - } if (pools == null) { - logger.trace("There are no pools to reorder; returning null."); + logger.info("There are no pools to reorder."); return null; } - if (logger.isTraceEnabled()) { - logger.trace(String.format("reordering %d pools", pools.size())); - } + logger.info("Reordering [{}] pools", pools.size()); Account account = null; if (vmProfile.getVirtualMachine() != null) { account = vmProfile.getOwner(); @@ -224,9 +219,7 @@ public List reorderPools(List pools, VirtualMachinePro pools = reorderStoragePoolsBasedOnAlgorithm(pools, plan, account); if (vmProfile.getVirtualMachine() == null) { - if (logger.isTraceEnabled()) { - logger.trace("The VM is null, skipping pools reordering by disk provisioning type."); - } + logger.info("The VM is null, skipping pool reordering by disk provisioning type."); return pools; } @@ -240,14 +233,10 @@ public List reorderPools(List pools, VirtualMachinePro List reorderStoragePoolsBasedOnAlgorithm(List pools, DeploymentPlan plan, Account account) { String volumeAllocationAlgorithm = VolumeOrchestrationService.VolumeAllocationAlgorithm.value(); - logger.debug("Using volume allocation algorithm {} to reorder pools.", volumeAllocationAlgorithm); + logger.info("Using volume allocation algorithm {} to reorder pools.", volumeAllocationAlgorithm); if (volumeAllocationAlgorithm.equals("random") || (account == null)) { reorderRandomPools(pools); } else if (StringUtils.equalsAny(volumeAllocationAlgorithm, "userdispersing", "firstfitleastconsumed")) { - if (logger.isTraceEnabled()) { - logger.trace("Using reordering algorithm {}", volumeAllocationAlgorithm); - } - if (volumeAllocationAlgorithm.equals("userdispersing")) { pools = reorderPoolsByNumberOfVolumes(plan, pools, account); } else { @@ -259,16 +248,15 @@ List reorderStoragePoolsBasedOnAlgorithm(List pools, D void reorderRandomPools(List pools) { StorageUtil.traceLogStoragePools(pools, logger, "pools to choose from: "); - if (logger.isTraceEnabled()) { - logger.trace("Shuffle this so that we don't check the pools in the same order. Algorithm == 'random' (or no account?)"); - } - StorageUtil.traceLogStoragePools(pools, logger, "pools to shuffle: "); + logger.trace("Shuffle this so that we don't check the pools in the same order. Algorithm == 'random' (or no account?)"); + logger.debug("Pools to shuffle: [{}]", pools); Collections.shuffle(pools, secureRandom); - StorageUtil.traceLogStoragePools(pools, logger, "shuffled list of pools to choose from: "); + logger.debug("Shuffled list of pools to choose from: [{}]", pools); } private List reorderPoolsByDiskProvisioningType(List pools, DiskProfile diskProfile) { if (diskProfile != null && diskProfile.getProvisioningType() != null && !diskProfile.getProvisioningType().equals(Storage.ProvisioningType.THIN)) { + logger.info("Reordering [{}] pools by disk provisioning type [{}].", pools.size(), diskProfile.getProvisioningType()); List reorderedPools = new ArrayList<>(); int preferredIndex = 0; for (StoragePool pool : pools) { @@ -282,22 +270,28 @@ private List reorderPoolsByDiskProvisioningType(List p reorderedPools.add(preferredIndex++, pool); } } + logger.debug("Reordered list of pools by disk provisioning type [{}]: [{}]", diskProfile.getProvisioningType(), reorderedPools); return reorderedPools; } else { + if (diskProfile == null) { + logger.info("Reordering pools by disk provisioning type wasn't necessary, since no disk profile was found."); + } else { + logger.debug("Reordering pools by disk provisioning type wasn't necessary, since the provisioning type is [{}].", diskProfile.getProvisioningType()); + } return pools; } } protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, DeploymentPlan plan) { - logger.debug(String.format("Checking if storage pool [%s] is suitable to disk [%s].", pool, dskCh)); + logger.debug("Checking if storage pool [{}] is suitable to disk [{}].", pool, dskCh); if (avoid.shouldAvoid(pool)) { - logger.debug(String.format("StoragePool [%s] is in avoid set, skipping this pool to allocation of disk [%s].", pool, dskCh)); + logger.debug("StoragePool [{}] is in avoid set, skipping this pool to allocation of disk [{}].", pool, dskCh); return false; } if (dskCh.requiresEncryption() && !pool.getPoolType().supportsEncryption()) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Storage pool type '%s' doesn't support encryption required for volume, skipping this pool", pool.getPoolType())); + logger.debug("Storage pool type '[{}]' doesn't support encryption required for volume, skipping this pool", pool.getPoolType()); } return false; } @@ -319,8 +313,8 @@ protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, } if (!checkDiskProvisioningSupport(dskCh, pool)) { - logger.debug(String.format("Storage pool [%s] does not have support to disk provisioning of disk [%s].", pool, ReflectionToStringBuilderUtils.reflectOnlySelectedFields(dskCh, - "type", "name", "diskOfferingId", "templateId", "volumeId", "provisioningType", "hyperType"))); + logger.debug("Storage pool [{}] does not have support to disk provisioning of disk [{}].", pool, ReflectionToStringBuilderUtils.reflectOnlySelectedFields(dskCh, + "type", "name", "diskOfferingId", "templateId", "volumeId", "provisioningType", "hyperType")); return false; } @@ -332,7 +326,7 @@ protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, HostVO plannedHost = hostDao.findById(plan.getHostId()); if (!storageMgr.checkIfHostAndStoragePoolHasCommonStorageAccessGroups(plannedHost, pool)) { if (logger.isDebugEnabled()) { - logger.debug(String.format("StoragePool %s and host %s does not have matching storage access groups", pool, plannedHost)); + logger.debug("StoragePool [{}] and host [{}] does not have matching storage access groups", pool, plannedHost); } return false; } @@ -343,13 +337,13 @@ protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, if (!isTempVolume) { volume = volumeDao.findById(dskCh.getVolumeId()); if (!storageMgr.storagePoolCompatibleWithVolumePool(pool, volume)) { - logger.debug(String.format("Pool [%s] is not compatible with volume [%s], skipping it.", pool, volume)); + logger.debug("Pool [{}] is not compatible with volume [{}], skipping it.", pool, volume); return false; } } if (pool.isManaged() && !storageUtil.managedStoragePoolCanScale(pool, plan.getClusterId(), plan.getHostId())) { - logger.debug(String.format("Cannot allocate pool [%s] to volume [%s] because the max number of managed clustered filesystems has been exceeded.", pool, volume)); + logger.debug("Cannot allocate pool [{}] to volume [{}] because the max number of managed clustered filesystems has been exceeded.", pool, volume); return false; } @@ -358,13 +352,13 @@ protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, requestVolumeDiskProfilePairs.add(new Pair<>(volume, dskCh)); if (dskCh.getHypervisorType() == HypervisorType.VMware) { if (pool.getPoolType() == Storage.StoragePoolType.DatastoreCluster && storageMgr.isStoragePoolDatastoreClusterParent(pool)) { - logger.debug(String.format("Skipping allocation of pool [%s] to volume [%s] because this pool is a parent datastore cluster.", pool, volume)); + logger.debug("Skipping allocation of pool [{}] to volume [{}] because this pool is a parent datastore cluster.", pool, volume); return false; } if (pool.getParent() != 0L) { StoragePoolVO datastoreCluster = storagePoolDao.findById(pool.getParent()); if (datastoreCluster == null || (datastoreCluster != null && datastoreCluster.getStatus() != StoragePoolStatus.Up)) { - logger.debug(String.format("Skipping allocation of pool [%s] to volume [%s] because this pool is not in [%s] state.", datastoreCluster, volume, StoragePoolStatus.Up)); + logger.debug("Skipping allocation of pool [{}] to volume [{}] because this pool is not in [{}] state.", datastoreCluster, volume, StoragePoolStatus.Up); return false; } } @@ -374,11 +368,11 @@ protected boolean filter(ExcludeList avoid, StoragePool pool, DiskProfile dskCh, storageMgr.isStoragePoolCompliantWithStoragePolicy(dskCh.getDiskOfferingId(), pool) : storageMgr.isStoragePoolCompliantWithStoragePolicy(requestVolumeDiskProfilePairs, pool); if (!isStoragePoolStoragePolicyCompliance) { - logger.debug(String.format("Skipping allocation of pool [%s] to volume [%s] because this pool is not compliant with the storage policy required by the volume.", pool, volume)); + logger.debug("Skipping allocation of pool [{}] to volume [{}] because this pool is not compliant with the storage policy required by the volume.", pool, volume); return false; } } catch (StorageUnavailableException e) { - logger.warn(String.format("Could not verify storage policy compliance against storage pool %s due to exception %s", pool.getUuid(), e.getMessage())); + logger.warn("Could not verify storage policy compliance against storage pool [{}] due to exception [{}]", pool.getUuid(), e.getMessage()); return false; } } @@ -427,19 +421,19 @@ private boolean checkHypervisorCompatibility(HypervisorType hyperType, Volume.Ty protected void logDisabledStoragePools(long dcId, Long podId, Long clusterId, ScopeType scope) { List disabledPools = storagePoolDao.findDisabledPoolsByScope(dcId, podId, clusterId, scope); if (disabledPools != null && !disabledPools.isEmpty()) { - logger.trace(String.format("Ignoring pools [%s] as they are in disabled state.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(disabledPools))); + logger.trace("Ignoring pools [{}] as they are in disabled state.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(disabledPools)); } } protected void logStartOfSearch(DiskProfile dskCh, VirtualMachineProfile vmProfile, DeploymentPlan plan, int returnUpTo, boolean bypassStorageTypeCheck){ - logger.trace(String.format("%s is looking for storage pools that match the VM's disk profile [%s], virtual machine profile [%s] and " - + "deployment plan [%s]. Returning up to [%d] and bypassStorageTypeCheck [%s].", this.getClass().getSimpleName(), dskCh, vmProfile, plan, returnUpTo, bypassStorageTypeCheck)); + logger.trace("[{}] is looking for storage pools that match the VM's disk profile [{}], virtual machine profile [{}] and " + + "deployment plan [{}]. Returning up to [{}] and bypassStorageTypeCheck [{}].", this.getClass().getSimpleName(), dskCh, vmProfile, plan, returnUpTo, bypassStorageTypeCheck); } protected void logEndOfSearch(List storagePoolList) { - logger.debug(String.format("%s is returning [%s] suitable storage pools [%s].", this.getClass().getSimpleName(), storagePoolList.size(), - Arrays.toString(storagePoolList.toArray()))); + logger.debug("[{}] is returning [{}] suitable storage pools [{}].", this.getClass().getSimpleName(), storagePoolList.size(), + Arrays.toString(storagePoolList.toArray())); } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java new file mode 100644 index 000000000000..7475f5619528 --- /dev/null +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/backup/BackupObject.java @@ -0,0 +1,198 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.storage.backup; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.utils.component.ComponentContext; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.InternalBackupJoinVO; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +public class BackupObject implements DataObject { + + private long id; + private String uuid; + private Long zoneId; + private Long size; + private long physicalSize; + private DataStore dataStore; + private String imageStorePath; + private Backup.Status status; + private Backup.CompressionStatus compressionStatus; + + @Inject + InternalBackupJoinDao internalBackupJoinDao; + @Inject + DataStoreManager storeManager; + + public BackupObject() { + + } + + public static BackupObject getBackupObject(InternalBackupJoinVO internalBackupJoinVO) { + BackupObject backupObject = ComponentContext.inject(BackupObject.class); + backupObject.configure(internalBackupJoinVO); + return backupObject; + } + + private void configure(InternalBackupJoinVO internalBackupJoin) { + this.id = internalBackupJoin.getId(); + this.uuid = internalBackupJoin.getUuid(); + this.zoneId = internalBackupJoin.getZoneId(); + this.size = internalBackupJoin.getProtectedSize(); + this.physicalSize = internalBackupJoin.getSize(); + this.imageStorePath = internalBackupJoin.getImageStorePath(); + this.status = internalBackupJoin.getStatus(); + this.compressionStatus = internalBackupJoin.getCompressionStatus(); + this.dataStore = storeManager.getDataStore(internalBackupJoin.getImageStoreId(), DataStoreRole.Image); + } + + public List> getChildren() { + List> children = new ArrayList<>(); + + List backups = internalBackupJoinDao.listByParentId(id); + while (CollectionUtils.isNotEmpty(backups)) { + children.add(backups.stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + backups = internalBackupJoinDao.listByParentId(backups.get(0).getId()); + } + + return children; + } + + public List> getParents(long parentId) { + LinkedList> parents = new LinkedList<>(); + + List backups = internalBackupJoinDao.listById(parentId); + while (CollectionUtils.isNotEmpty(backups)) { + parents.addFirst(backups.stream().map(BackupObject::getBackupObject).collect(Collectors.toList())); + backups = internalBackupJoinDao.listById(backups.get(0).getParentId()); + } + + return parents; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUri() { + return ""; + } + + @Override + public DataTO getTO() { + DataTO to = dataStore.getDriver().getTO(this); + if (to == null) { + return new BackupDeltaTO(id, dataStore.getTO(), Hypervisor.HypervisorType.KVM, imageStorePath); + } + return to; + } + + @Override + public DataStore getDataStore() { + return dataStore; + } + + @Override + public Long getSize() { + return size; + } + + @Override + public long getPhysicalSize() { + return physicalSize; + } + + @Override + public DataObjectType getType() { + return DataObjectType.BACKUP; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event) { + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event, Answer answer) { + } + + @Override + public void incRefCount() { + } + + @Override + public void decRefCount() { + } + + @Override + public Long getRefCount() { + return 0L; + } + + @Override + public String getName() { + return ""; + } + + public Long getZoneId() { + return zoneId; + } + + @Override + public String toString() { + return uuid; + } + + public Backup.CompressionStatus getCompressionStatus() { + return compressionStatus; + } + + public Backup.Status getStatus() { + return status; + } +} diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java index 061d18dc3769..7eee32b9f1b5 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java @@ -32,8 +32,12 @@ import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; +import com.cloud.storage.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.user.Account; import com.cloud.utils.Pair; +import com.cloud.utils.db.QueryBuilder; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -46,6 +50,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.LocalHostEndpoint; import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import org.springframework.stereotype.Component; @@ -59,8 +64,8 @@ import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage.TemplateType; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import com.cloud.utils.db.DB; -import com.cloud.utils.db.QueryBuilder; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; @@ -75,6 +80,12 @@ public class DefaultEndPointSelector implements EndPointSelector { private HostDao hostDao; @Inject private DedicatedResourceDao dedicatedResourceDao; + @Inject + private PrimaryDataStoreDao _storagePoolDao; + @Inject + private VolumeDetailsDao _volDetailsDao; + @Inject + private ClvmPoolManager clvmPoolManager; private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support"; private final String findOneHostOnPrimaryStorage = "select t.id from " @@ -264,6 +275,27 @@ public EndPoint select(DataObject srcData, DataObject destData) { @Override public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) { + if (destData instanceof VolumeInfo) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy"); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + + // Check if SOURCE is a CLVM volume with active lock (for operations copying FROM CLVM to secondary storage) + if (srcData instanceof VolumeInfo) { + VolumeInfo srcVolume = (VolumeInfo) srcData; + DataStore srcStore = srcVolume.getDataStore(); + if (srcStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(srcVolume, pool, "copy operation"); + if (clvmEp != null) { + return clvmEp; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set DataStore srcStore = srcData.getDataStore(); DataStore destStore = destData.getDataStore(); if (moveBetweenPrimaryImage(srcStore, destStore)) { @@ -305,7 +337,6 @@ public EndPoint select(DataObject srcData, DataObject destData, StorageAction ac @Override public EndPoint select(DataObject srcData, DataObject destData, StorageAction action, boolean encryptionRequired) { - logger.error("IR24 select BACKUPSNAPSHOT from primary to secondary {} dest={}", srcData, destData); if (action == StorageAction.BACKUPSNAPSHOT && srcData.getDataStore().getRole() == DataStoreRole.Primary) { SnapshotInfo srcSnapshot = (SnapshotInfo)srcData; VolumeInfo volumeInfo = srcSnapshot.getBaseVolume(); @@ -314,6 +345,17 @@ public EndPoint select(DataObject srcData, DataObject destData, StorageAction ac if (vm != null && vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); } + // For CLVM pools, the snapshot LVM device only exists on the lock-holder host. + // Route the backup CopyCommand to that same host regardless of VM state. + DataStore srcStore = volumeInfo.getDataStore(); + if (srcStore != null && srcStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId()); + logger.debug("Checking if CLVM store and lock-holder routing applicable for snapshot {}", srcSnapshot.getUuid()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volumeInfo, pool, "snapshot backup"); + if (clvmEp != null) { + return clvmEp; + } + } } if (srcSnapshot.getHypervisorType() == Hypervisor.HypervisorType.VMware) { if (vm != null) { @@ -388,18 +430,103 @@ private List listUpAndConnectingSecondaryStorageVmHost(Long dcId) { return sc.list(); } + /** + * Selects endpoint for CLVM volumes with destination host hint. + * This ensures volumes are created on the correct host with exclusive locks. + * + * @param volume The volume to check for CLVM routing + * @param operation Description of the operation (for logging) + * @return EndPoint for the destination host if CLVM routing applies, null otherwise + */ + private EndPoint selectClvmEndpointIfApplicable(VolumeInfo volume, String operation) { + DataStore store = volume.getDataStore(); + + if (store.getRole() != DataStoreRole.Primary) { + return null; + } + + + // Check if this is a CLVM pool + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + if (Volume.State.Allocated == volume.getState()) { + // Check if destination host hint is set + Long destHostId = volume.getDestinationHostId(); + if (destHostId == null) { + return null; + } + + logger.info("CLVM {}: routing volume {} to destination host {} for optimal exclusive lock placement", + operation, volume.getUuid(), destHostId); + + EndPoint ep = getEndPointFromHostId(destHostId); + if (ep != null) { + return ep; + } + } + + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + return null; + } + logger.info("CLVM {}: routing existing volume {} to live lock-holder host {}", + operation, volume.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for lock host {}, falling back to default selection", lockHostId); + return null; + } + @Override public EndPoint select(DataObject object, boolean encryptionSupportRequired) { DataStore store = object.getDataStore(); + + // This ensures volumes are created on the correct host with exclusive locks + String operation = ""; + if (DataStoreRole.Primary == store.getRole()) { + VolumeInfo volume = null; + if (object instanceof VolumeInfo) { + volume = (VolumeInfo) object; + operation = "volume creation"; + } else if (object instanceof SnapshotInfo) { + volume = ((SnapshotInfo) object).getBaseVolume(); + operation = "snapshot creation"; + } + + if (volume != null) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable(volume, operation); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set if (store.getRole() == DataStoreRole.Primary) { return findEndPointInScope(store.getScope(), findOneHostOnPrimaryStorage, store.getId(), encryptionSupportRequired); } throw new CloudRuntimeException(String.format("Storage role %s doesn't support encryption", store.getRole())); } + @Override public EndPoint select(DataObject object) { DataStore store = object.getDataStore(); + + // For CLVM volumes, check if there's a lock host ID to route to + if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) { + VolumeInfo volume = (VolumeInfo) object; + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "operation"); + if (clvmEp != null) { + return clvmEp; + } + } + EndPoint ep = select(store); if (ep != null) { return ep; @@ -493,6 +620,19 @@ public EndPoint select(DataObject object, StorageAction action, boolean encrypti } case DELETEVOLUME: { VolumeInfo volume = (VolumeInfo) object; + + // For CLVM volumes, route to the host holding the exclusive lock + if (volume.getHypervisorType() == Hypervisor.HypervisorType.KVM) { + DataStore store = volume.getDataStore(); + if (store.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "deletion"); + if (clvmEp != null) { + return clvmEp; + } + } + } + if (volume.getHypervisorType() == Hypervisor.HypervisorType.VMware) { VirtualMachine vm = volume.getAttachedVM(); if (vm != null) { @@ -540,6 +680,14 @@ protected EndPoint getEndPointForSnapshotOperationsInKvm(SnapshotInfo snapshotIn if (vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); + } else if (vm.getState() == VirtualMachine.State.Stopped) { + StoragePoolVO pool = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + EndPoint ep = getApplicableEndpointForClvm(snapshotInfo, volumeInfo); + if (ep != null) { + return ep; + } + } } Long hostId = vm.getLastHostId(); @@ -552,6 +700,20 @@ protected EndPoint getEndPointForSnapshotOperationsInKvm(SnapshotInfo snapshotIn return select(snapshotInfo, encryptionRequired); } + private EndPoint getApplicableEndpointForClvm(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo) { + Long lockHostId = getClvmLockHostId(volumeInfo); + if (lockHostId != null) { + logger.debug("CLVM snapshot operation: routing snapshot [{}] to lock-holder host [{}]", + snapshotInfo.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for CLVM lock host {}, falling back", lockHostId); + } + return null; + } + @Override public EndPoint select(Scope scope, Long storeId) { return findEndPointInScope(scope, findOneHostOnPrimaryStorage, storeId); @@ -589,4 +751,46 @@ public List selectAll(DataStore store) { } return endPoints; } + + protected EndPoint tryRouteToClvmLockHolder(VolumeInfo volume, StoragePoolVO pool, String operation) { + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + logger.debug("No CLVM lock host tracked for volume {}, using default endpoint selection", volume.getUuid()); + return null; + } + logger.info("Routing CLVM volume {} {} to lock holder host {}", volume.getUuid(), operation, lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for CLVM lock host {}, falling back to default selection", lockHostId); + return null; + } + + /** + * Gets the CLVM lock host ID for a volume by querying actual LVM state. + * + * @param volume The CLVM volume + * @return Host ID holding the lock, or null if not found + */ + protected Long getClvmLockHostId(VolumeInfo volume) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (lockHostId != null) { + logger.debug("Found actual lock host {} for volume {} via LVM query", lockHostId, volume.getUuid()); + } + + return lockHostId; + } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java index 55551772a08a..d5b96be71ee8 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/helper/VMSnapshotHelperImpl.java @@ -26,9 +26,16 @@ import javax.inject.Inject; import com.cloud.uservm.UserVm; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; @@ -64,6 +71,12 @@ public class VMSnapshotHelperImpl implements VMSnapshotHelper { @Inject VolumeDataFactory volumeDataFactory; + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + StateMachine2 _vmSnapshottateMachine; public VMSnapshotHelperImpl() { @@ -115,10 +128,14 @@ public List getVolumeTOList(Long vmId) { List volumeTOs = new ArrayList(); List volumeVos = volumeDao.findByInstance(vmId); VolumeInfo volumeInfo = null; - for (VolumeVO volume : volumeVos) { - volumeInfo = volumeDataFactory.getVolume(volume.getId()); + try { + for (VolumeVO volume : volumeVos) { + volumeInfo = volumeDataFactory.getVolume(volume.getId()); - volumeTOs.add((VolumeObjectTO)volumeInfo.getTO()); + volumeTOs.add((VolumeObjectTO)volumeInfo.getTO()); + } + } catch (NullPointerException npe) { + throw new CloudRuntimeException(String.format("Unable to get list of volumeTOs for VM [%s]. Have the volumes already been created on the storage?", vmId), npe); } return volumeTOs; } @@ -150,6 +167,26 @@ public VMSnapshotTO getSnapshotWithParents(VMSnapshotVO snapshot) { return result; } + /** + * For a given {@code vmSnapshotId}, gets the list with all the volume snapshots that are part of the VMSnapshot. + * + * @param vmSnapshotId the id of the VM snapshot; + * @return the list that will be populated with the volume snapshots associated with the VM snapshot. + */ + @Override + public List getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(long vmSnapshotId) { + List associatedVolumeSnapshots = new ArrayList<>(); + List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshotId, VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT); + for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { + SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshotId)); + } + associatedVolumeSnapshots.add(snapshot); + } + return associatedVolumeSnapshots; + } + @Override public StoragePoolVO getStoragePoolForVM(UserVm vm) { List rootVolumes = volumeDao.findReadyRootVolumesByInstance(vm.getId()); diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java index a2e9eff2a08a..26b39e30776f 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java @@ -230,8 +230,10 @@ protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher caller = context.getParentCallback(); - if (answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR || - answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.ABANDONED || answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.UNKNOWN) { + if (VMTemplateStorageResourceAssoc.ERROR_DOWNLOAD_STATES.contains(answer.getDownloadStatus())) { CreateCmdResult result = new CreateCmdResult(null, null); result.setSuccess(false); result.setResult(answer.getErrorString()); @@ -285,19 +286,22 @@ protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher caller = context.getParentCallback(); - if (answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_ERROR || - answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.ABANDONED || answer.getDownloadStatus() == VMTemplateStorageResourceAssoc.Status.UNKNOWN) { + if (VMTemplateStorageResourceAssoc.ERROR_DOWNLOAD_STATES.contains(answer.getDownloadStatus())) { CreateCmdResult result = new CreateCmdResult(null, null); result.setSuccess(false); result.setResult(answer.getErrorString()); diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java index dbb606b44a8a..51edd62326dd 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/datastore/ImageStoreHelper.java @@ -131,7 +131,7 @@ public ImageStoreVO createImageStore(Map params, Map getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(long vmSnapshotId); + StoragePoolVO getStoragePoolForVM(UserVm vm); Storage.StoragePoolType getStoragePoolType(Long poolId); diff --git a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java index 9f01ab162ba7..ca124ec30f04 100644 --- a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java +++ b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java @@ -17,20 +17,41 @@ package org.apache.cloudstack.storage.endpoint; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeDetailVO; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.StorageAction; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mockStatic; + @RunWith(MockitoJUnitRunner.class) public class DefaultEndPointSelectorTest { @@ -46,12 +67,55 @@ public class DefaultEndPointSelectorTest { @Mock private DataStore datastoreMock; - @Spy - private DefaultEndPointSelector defaultEndPointSelectorSpy; + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private PrimaryDataStoreDao _storagePoolDao; + + @Mock + private VolumeDetailsDao _volDetailsDao; + + @Mock + private VolumeDetailVO volumeDetailVOMock; + + @Mock + private EndPoint endPointMock; + + @Mock + ClvmPoolManager clvmPoolManager; + + @Mock + HostDao hostDao; + + static MockedStatic remoteHostEndPointMock; + + @InjectMocks + private DefaultEndPointSelector defaultEndPointSelectorSpy = Mockito.spy(new DefaultEndPointSelector()); + + private static final Long VOLUME_ID = 1L; + private static final Long HOST_ID = 10L; + private static final Long DEST_HOST_ID = 20L; + private static final Long STORE_ID = 100L; + private static final String VOLUME_UUID = "test-volume-uuid"; + + @BeforeClass + public static void init() { + remoteHostEndPointMock = mockStatic(RemoteHostEndPoint.class); + } + + @AfterClass + public static void close() { + remoteHostEndPointMock.close(); + } @Before public void setup() { Mockito.doReturn(volumeInfoMock).when(snapshotInfoMock).getBaseVolume(); + + // Common volume mock setup + Mockito.when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + Mockito.when(volumeInfoMock.getUuid()).thenReturn(VOLUME_UUID); } @Test @@ -197,4 +261,293 @@ public void getEndPointForSnapshotOperationsInKvmTestVolumeAttachedToStoppedVmAn Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(snapshotInfoMock, false); } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(DEST_HOST_ID); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM_NG() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithoutDestinationHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(null); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_NonCLVMPool() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_SnapshotWithBaseVolumeDestHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(snapshotInfoMock.getBaseVolume()).thenReturn(volumeInfoMock); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(snapshotInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVM_NG_WithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(volumeInfoMock, false); + } + + @Test + public void testSelectWithAction_DeleteVolume_NonCLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(_volDetailsDao, Mockito.never()).findDetail(Mockito.anyLong(), Mockito.anyString()); + } + + @Test + public void testSelectObject_CLVMVolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVM_NG_VolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVMVolumeWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + RemoteHostEndPoint ep = Mockito.mock(RemoteHostEndPoint.class); + Host lockHost = Mockito.mock(Host.class); + remoteHostEndPointMock.when(() -> RemoteHostEndPoint.getHypervisorHostEndPoint(lockHost)).thenReturn(ep); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithInvalidLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithEmptyLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithoutDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + DataStore srcStoreMock = Mockito.mock(DataStore.class); + + Mockito.when(srcDataMock.getDataStore()).thenReturn(srcStoreMock); + Mockito.when(srcStoreMock.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointForImageMove( + srcStoreMock, datastoreMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).findEndPointForImageMove(srcStoreMock, datastoreMock, false); + } + } diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java index 7de9000782ec..7644d4688f7e 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java @@ -37,6 +37,7 @@ import com.cloud.network.dao.NetworkVO; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -139,6 +140,18 @@ public boolean hostConnect(long hostId, long poolId) throws StorageConflictExcep Map nfsMountOpts = storageManager.getStoragePoolNFSMountOpts(pool, null).first(); Optional.ofNullable(nfsMountOpts).ifPresent(detailsMap::putAll); + + // Propagate CLVM secure zero-fill setting to the host + // Note: This is done during host connection (agent start, MS restart, host reconnection) + // so the setting is non-dynamic. Changes require host reconnection to take effect. + if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + Boolean clvmSecureZeroFill = ClvmPoolManager.CLVMSecureZeroFill.valueIn(poolId); + if (clvmSecureZeroFill != null) { + detailsMap.put("clvmsecurezerofill", String.valueOf(clvmSecureZeroFill)); + logger.debug("Added CLVM secure zero-fill setting: {} for storage pool: {}", clvmSecureZeroFill, pool); + } + } + ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, pool, detailsMap); cmd.setWait(modifyStoragePoolCommandWait); HostVO host = hostDao.findById(hostId); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java index 43218b3f6a02..de85067aae57 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java @@ -24,11 +24,13 @@ import com.cloud.dc.VsphereStoragePolicyVO; import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.storage.StorageManager; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.utils.Pair; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionStatus; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.framework.kms.KMSException; import org.apache.cloudstack.secret.dao.PassphraseDao; import org.apache.cloudstack.secret.PassphraseVO; import com.cloud.service.dao.ServiceOfferingDetailsDao; @@ -46,6 +48,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.kms.KMSManager; +import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CreateObjectAnswer; import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager; @@ -98,6 +102,10 @@ public class VolumeObject implements VolumeInfo { @Inject VolumeDataStoreDao volumeStoreDao; @Inject + KMSManager kmsManager; + @Inject + KMSWrappedKeyDao kmsWrappedKeyDao; + @Inject ObjectInDataStoreManager objectInStoreMgr; @Inject ResourceLimitService resourceLimitMgr; @@ -126,6 +134,7 @@ public class VolumeObject implements VolumeInfo { private boolean directDownload; private String vSphereStoragePolicyId; private boolean followRedirects; + private Long destinationHostId; // For CLVM: hints where volume should be created private List checkpointPaths; private Set checkpointImageStoreUrls; @@ -361,6 +370,30 @@ public void setDirectDownload(boolean directDownload) { this.directDownload = directDownload; } + @Override + public Long getDestinationHostId() { + // If not in memory, try to load from the database (volume_details table) + // For CLVM volumes, this uses the CLVM_LOCK_HOST_ID, which serves a dual purpose: + // 1. During creation: hints where to create the volume + // 2. After creation: tracks which host holds the exclusive lock + if (destinationHostId == null && volumeVO != null) { + VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeVO.getId(), ClvmPoolManager.CLVM_LOCK_HOST_ID); + if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) { + try { + destinationHostId = Long.parseLong(detail.getValue()); + } catch (NumberFormatException e) { + logger.warn("Invalid CLVM lock host ID value in volume_details for volume {}: {}", volumeVO.getUuid(), detail.getValue()); + } + } + } + return destinationHostId; + } + + @Override + public void setDestinationHostId(Long hostId) { + this.destinationHostId = hostId; + } + public void update() { volumeDao.update(volumeVO.getId(), volumeVO); volumeVO = volumeDao.findById(volumeVO.getId()); @@ -900,6 +933,26 @@ public void setPassphraseId(Long id) { volumeVO.setPassphraseId(id); } + @Override + public Long getKmsKeyId() { + return volumeVO.getKmsKeyId(); + } + + @Override + public void setKmsKeyId(Long id) { + volumeVO.setKmsKeyId(id); + } + + @Override + public Long getKmsWrappedKeyId() { + return volumeVO.getKmsWrappedKeyId(); + } + + @Override + public void setKmsWrappedKeyId(Long id) { + volumeVO.setKmsWrappedKeyId(id); + } + /** * Removes passphrase reference from underlying volume. Also removes the associated passphrase entry if it is the last user. */ @@ -929,9 +982,29 @@ public void doInTransactionWithoutResult(TransactionStatus status) { /** * Looks up passphrase from underlying volume. - * @return passphrase as bytes + * Supports both legacy passphrase-based encryption and KMS-based encryption. + * @return passphrase/DEK as base64-encoded bytes (UTF-8 bytes of base64 string) */ public byte[] getPassphrase() { + // First check for KMS-encrypted volume + if (volumeVO.getKmsWrappedKeyId() != null) { + try { + // Unwrap the DEK from KMS (returns raw binary bytes) + byte[] dekBytes = kmsManager.unwrapKey(volumeVO.getKmsWrappedKeyId()); + // Base64-encode the DEK for consistency with legacy passphrases + // and for use with qemu-img which expects base64 format + String base64Dek = java.util.Base64.getEncoder().encodeToString(dekBytes); + // Zeroize the raw DEK bytes + java.util.Arrays.fill(dekBytes, (byte) 0); + // Return UTF-8 bytes of the base64 string + return base64Dek.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } catch (KMSException e) { + logger.error("Failed to unwrap KMS key for volume {}: {}", volumeVO, e.getMessage(), e); + throw e; + } + } + + // Fallback to legacy passphrase-based encryption PassphraseVO passphrase = passphraseDao.findById(volumeVO.getPassphraseId()); if (passphrase != null) { return passphrase.getPassphrase(); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 7132d61ed3b0..6502de70a1ff 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -32,10 +32,13 @@ import javax.inject.Inject; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd; +import org.apache.cloudstack.backup.InternalBackupService; import org.apache.cloudstack.engine.cloud.entity.api.VolumeEntity; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; @@ -46,6 +49,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; @@ -64,6 +68,7 @@ import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.framework.async.AsyncRpcContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.kms.KMSManager; import org.apache.cloudstack.secret.dao.PassphraseDao; import org.apache.cloudstack.storage.RemoteHostEndPoint; import org.apache.cloudstack.storage.command.CommandResult; @@ -83,6 +88,7 @@ import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -221,6 +227,13 @@ public class VolumeServiceImpl implements VolumeService { private PassphraseDao passphraseDao; @Inject protected DiskOfferingDao diskOfferingDao; + @Inject + ClvmPoolManager clvmPoolManager; + @Inject + private InternalBackupService internalBackupService; + + @Inject + private KMSManager kmsManager; public VolumeServiceImpl() { } @@ -395,9 +408,9 @@ public AsyncCallFuture expungeVolumeAsync(VolumeInfo volume) { } // Find out if the volume is at state of download_in_progress on secondary storage - VolumeDataStoreVO volumeStore = _volumeStoreDao.findByVolume(volume.getId()); - if (volumeStore != null) { - if (volumeStore.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_IN_PROGRESS) { + VolumeDataStoreVO volumeOnImageStore = _volumeStoreDao.findByVolume(volume.getId()); + if (volumeOnImageStore != null) { + if (volumeOnImageStore.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOAD_IN_PROGRESS) { String msg = String.format("Volume: %s is currently being uploaded; can't delete it.", volume); logger.debug(msg); result.setSuccess(false); @@ -416,10 +429,10 @@ public AsyncCallFuture expungeVolumeAsync(VolumeInfo volume) { if (!volumeExistsOnPrimary(vol)) { // not created on primary store - if (volumeStore == null) { + if (volumeOnImageStore == null) { // also not created on secondary store if (logger.isDebugEnabled()) { - logger.debug("Marking volume that was never created as destroyed: " + vol); + logger.debug("Marking volume that was never created as destroyed: {}", vol); } VMTemplateVO template = templateDao.findById(vol.getTemplateId()); if (template != null && !template.isDeployAsIs()) { @@ -435,8 +448,21 @@ public AsyncCallFuture expungeVolumeAsync(VolumeInfo volume) { if (volume.getDataStore().getRole() == DataStoreRole.Image) { // no need to change state in volumes table volume.processEventOnly(Event.DestroyRequested); + if (volumeOnImageStore == null) { + logger.debug("Volume {} doesn't exist on image store, no need to delete", vol); + future.complete(result); + return future; + } } else if (volume.getDataStore().getRole() == DataStoreRole.Primary) { + if (vol.getState() == Volume.State.Expunging) { + logger.info("Volume {} is already in Expunging, retrying", volume); + } volume.processEvent(Event.ExpungeRequested); + if (!volumeExistsOnPrimary(vol)) { + logger.debug("Volume {} doesn't exist on primary storage, no need to delete", vol); + future.complete(result); + return future; + } } DeleteVolumeContext context = new DeleteVolumeContext<>(null, vo, future); @@ -457,13 +483,11 @@ public void ensureVolumeIsExpungeReady(long volumeId) { private boolean volumeExistsOnPrimary(VolumeVO vol) { Long poolId = vol.getPoolId(); - if (poolId == null) { return false; } PrimaryDataStore primaryStore = dataStoreMgr.getPrimaryDataStore(poolId); - if (primaryStore == null) { return false; } @@ -473,8 +497,7 @@ private boolean volumeExistsOnPrimary(VolumeVO vol) { } String volumePath = vol.getPath(); - - if (volumePath == null || volumePath.trim().isEmpty()) { + if (StringUtils.isBlank(volumePath)) { return false; } @@ -492,6 +515,13 @@ public Void deleteVolumeCallback(AsyncCallbackDispatcher snapStoreVOs = _snapshotStoreDao.listAllByVolumeAndDataStore(vo.getId(), DataStoreRole.Primary); for (SnapshotDataStoreVO snapStoreVo : snapStoreVOs) { @@ -1337,6 +1369,13 @@ private void createManagedVolumeCopyTemplateAsync(VolumeInfo volumeInfo, Primary primaryDataStore.setDetails(details); grantAccess(volumeInfo, destHost, primaryDataStore); + if (DataStoreProvider.ONTAP_PLUGIN_NAME.equals(primaryDataStore.getStorageProviderName())) { + // For Netapp ONTAP iscsiName or Lun path is available only after grantAccess + volumeInfo = volFactory.getVolume(volumeInfo.getId(), primaryDataStore); + String managedStoreTarget = ObjectUtils.defaultIfNull(volumeInfo.get_iScsiName(), volumeInfo.getUuid()); + details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget); + primaryDataStore.setDetails(details); + } try { motionSrv.copyAsync(srcTemplateInfo, destTemplateInfo, destHost, caller); @@ -1665,7 +1704,7 @@ public void destroyVolume(long volumeId) { if (vol.getAttachedVM() == null || vol.getAttachedVM().getType() == VirtualMachine.Type.User) { // Decrement the resource count for volumes and primary storage belonging user VM's only - _resourceLimitMgr.decrementVolumeResourceCount(vol.getAccountId(), vol.isDisplay(), vol.getSize(), diskOfferingDao.findById(vol.getDiskOfferingId())); + _resourceLimitMgr.decrementVolumeResourceCount(vol.getAccountId(), vol.isDisplay(), vol.getSize(), diskOfferingDao.findById(vol.getDiskOfferingId()), null); } } @@ -1744,6 +1783,11 @@ protected VolumeVO duplicateVolumeOnAnotherStorage(Volume volume, StoragePool po newVol.setPoolType(pool.getPoolType()); newVol.setLastPoolId(lastPoolId); newVol.setPodId(pool.getPodId()); + if (volume.getKmsKeyId() != null) { + newVol.setKmsKeyId(volume.getKmsKeyId()); + newVol.setKmsWrappedKeyId(volume.getKmsWrappedKeyId()); + newVol.setEncryptFormat(volume.getEncryptFormat()); + } if (volume.getPassphraseId() != null) { newVol.setPassphraseId(volume.getPassphraseId()); newVol.setEncryptFormat(volume.getEncryptFormat()); @@ -2960,4 +3004,179 @@ public void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, Account protected String buildVolumePath(long accountId, long volumeId) { return String.format("%s/%s/%s", TemplateConstants.DEFAULT_VOLUME_ROOT_DIR, accountId, volumeId); } + + @Override + public boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId) { + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + if (pool == null) { + logger.error("Cannot transfer volume lock for volume {}: storage pool not found", volume.getUuid()); + return false; + } + + logger.info("Transferring CLVM lock for volume {} (pool: {}) from host {} to host {}", + volume.getUuid(), pool.getName(), sourceHostId, destHostId); + + return clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), volume.getPath(), + pool, sourceHostId, destHostId); + } + + @Override + public Long findVolumeLockHost(VolumeInfo volume) { + if (volume == null) { + logger.warn("Cannot find volume lock host: volume is null"); + return null; + } + + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (lockHostId != null) { + logger.debug("Found actual lock host {} for volume {}", lockHostId, volume.getUuid()); + return lockHostId; + } + + Long instanceId = volume.getInstanceId(); + if (instanceId != null) { + VMInstanceVO vmInstance = vmDao.findById(instanceId); + if (vmInstance != null && vmInstance.getHostId() != null) { + logger.debug("Volume {} is attached to VM {} on host {}", + volume.getUuid(), vmInstance.getUuid(), vmInstance.getHostId()); + return vmInstance.getHostId(); + } + } + + if (pool != null && pool.getClusterId() != null) { + List hosts = _hostDao.findByClusterId(pool.getClusterId()); + if (hosts != null && !hosts.isEmpty()) { + for (HostVO host : hosts) { + if (host.getStatus() == com.cloud.host.Status.Up) { + logger.debug("Using fallback: first UP host {} in cluster {} for volume {}", + host.getId(), pool.getClusterId(), volume.getUuid()); + return host.getId(); + } + } + } + } + + logger.warn("Could not determine lock host for volume {}", volume.getUuid()); + return null; + } + + @Override + public VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId) { + if (volume == null) { + throw new CloudRuntimeException("Cannot perform CLVM lock migration: volume is null"); + } + + String volumeUuid = volume.getUuid(); + logger.info("Starting CLVM lock migration for volume {} (id: {}) to host {}", + volumeUuid, volume.getUuid(), destHostId); + + Long sourceHostId = findVolumeLockHost(volume); + if (sourceHostId == null) { + logger.warn("Could not determine source host for CLVM volume {} lock, assuming volume is not exclusively locked", + volumeUuid); + sourceHostId = destHostId; + } + + if (sourceHostId.equals(destHostId)) { + logger.info("CLVM volume {} already has lock on destination host {}, no migration needed", + volumeUuid, destHostId); + return volume; + } + + logger.info("Migrating CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + boolean success = transferVolumeLock(volume, sourceHostId, destHostId); + if (!success) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volumeUuid, sourceHostId, destHostId)); + } + + logger.info("Successfully migrated CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + return volFactory.getVolume(volume.getId()); + } + + @Override + public boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType) { + if (volumePoolType == null || vmPoolType == null) { + logger.debug("Cannot check if both pools are CLVM type: one or both pool types are null"); + return false; + } + return ClvmPoolManager.isClvmPoolType(volumePoolType) && + ClvmPoolManager.isClvmPoolType(vmPoolType); + } + + @Override + public boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId) { + if (volumePoolType != null && !ClvmPoolManager.isClvmPoolType(volumePoolType)) { + return false; + } + + if (volumePoolId == null || !volumePoolId.equals(vmPoolId)) { + Long volumeLockHostId = findVolumeLockHost(volumeToAttach); + if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) { + logger.info("CLVM cross-pool lock transfer required: Volume {} on pool {} lock is on host {} but VM is on host {}", + volumeToAttach.getUuid(), volumePoolId, volumeLockHostId, vmHostId); + return true; + } + return false; + } + + Long volumeLockHostId = findVolumeLockHost(volumeToAttach); + + if (volumeLockHostId == null) { + VolumeVO volumeVO = _volumeDao.findById(volumeToAttach.getId()); + if (volumeVO != null && volumeVO.getState() == Volume.State.Ready && volumeVO.getInstanceId() == null) { + logger.debug("CLVM volume {} is detached on same pool, lock transfer may be needed", + volumeToAttach.getUuid()); + return true; + } + } + + if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) { + logger.info("CLVM lock transfer required: Volume {} lock is on host {} but VM is on host {}", + volumeToAttach.getUuid(), volumeLockHostId, vmHostId); + return true; + } + + return false; + } + + @Override + public boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath) { + if (!areBothPoolsClvmType(volumePoolType, vmPoolType)) { + return false; + } + + String volumeVgName = extractVgNameFromPath(volumePoolPath); + String vmVgName = extractVgNameFromPath(vmPoolPath); + + if (volumeVgName != null && volumeVgName.equals(vmVgName)) { + logger.info("CLVM lightweight migration detected: Volume is in same VG ({}), only lock transfer needed (no data copy)", volumeVgName); + return true; + } + + return false; + } + + private String extractVgNameFromPath(String poolPath) { + if (poolPath == null) { + return null; + } + return poolPath.startsWith("/") ? poolPath.substring(1) : poolPath; + } } diff --git a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java new file mode 100644 index 000000000000..8d355263a6c3 --- /dev/null +++ b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java @@ -0,0 +1,555 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.volume; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.eq; + +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; + +/** + * Tests for CLVM lock management methods in VolumeServiceImpl. + */ +@RunWith(MockitoJUnitRunner.class) +public class VolumeServiceImplClvmTest { + + @Spy + @InjectMocks + private VolumeServiceImpl volumeService; + + @Mock + private VolumeDao volumeDao; + + @Mock + private PrimaryDataStoreDao storagePoolDao; + + @Mock + private HostDao _hostDao; + + @Mock + private VMInstanceDao vmDao; + + @Mock + private VolumeDataFactory volFactory; + + @Mock + private VolumeInfo volumeInfoMock; + + @Mock + private VolumeVO volumeVOMock; + + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private HostVO hostVOMock; + + @Mock + private VMInstanceVO vmInstanceVOMock; + + @Mock + private ClvmPoolManager clvmPoolManager; + + private static final Long VOLUME_ID = 1L; + private static final Long POOL_ID_1 = 100L; + private static final Long POOL_ID_2 = 200L; + private static final Long HOST_ID_1 = 10L; + private static final Long HOST_ID_2 = 20L; + private static final String POOL_PATH_VG1 = "/vg1"; + + @Before + public void setup() { + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getUuid()).thenReturn("test-volume-uuid"); + when(volumeInfoMock.getPath()).thenReturn("test-volume-path"); + + volumeService.storagePoolDao = storagePoolDao; + volumeService._hostDao = _hostDao; + volumeService.vmDao = vmDao; + volumeService.volFactory = volFactory; + volumeService._volumeDao = volumeDao; + volumeService.clvmPoolManager = clvmPoolManager; + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_MixedCLVMAndCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVMOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVM_NGOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_BothNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem)); + } + + @Test + public void testAreBothPoolsClvmType_NullVolumePoolType() { + assertFalse(volumeService.areBothPoolsClvmType(null, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_NullVmPoolType() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, null)); + } + + @Test + public void testAreBothPoolsClvmType_BothNull() { + assertFalse(volumeService.areBothPoolsClvmType(null, null)); + } + + + @Test + public void testIsLockTransferRequired_NonCLVMPool() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools_LockOnDifferentHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_2); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools_LockOnSameHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools_NoLockHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullPoolIds_LockOnDifferentHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_2); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + null, POOL_ID_1, HOST_ID_1)); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, null, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullPoolIds_NoLockHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + null, POOL_ID_1, HOST_ID_1)); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, null, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Ready); + when(volumeVOMock.getInstanceId()).thenReturn(null); // Detached + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeNotReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Allocated); + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLockTransferRequired_SameHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullVmHostId() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, null)); + } + + @Test + public void testIsLockTransferRequired_CLVM_NG_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLightweightMigrationNeeded_NonCLVMPools() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_OneCLVMOneNFS() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_NoSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_MixedSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_MixedCLVM_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVolumePath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVmPath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", null)); + } + + @Test + public void testIsLightweightMigrationNeeded_BothPathsNull() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, null)); + } + + @Test + public void testIsLightweightMigrationNeeded_ComplexVGNames() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-01")); + + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-02")); + } + + @Test + public void testTransferVolumeLock_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + + assertTrue(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_Failure() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_PoolNotFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(null); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testFindVolumeLockHost_NullVolume() { + Long result = volumeService.findVolumeLockHost(null); + assertNull(result); + } + + @Test + public void testFindVolumeLockHost_ExplicitLockFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FromAttachedVM() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(100L); + when(vmDao.findById(100L)).thenReturn(vmInstanceVOMock); + when(vmInstanceVOMock.getUuid()).thenReturn("vm-uuid"); + when(vmInstanceVOMock.getHostId()).thenReturn(HOST_ID_1); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FallbackToClusterHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(10L); + when(hostVOMock.getId()).thenReturn(HOST_ID_1); + when(hostVOMock.getStatus()).thenReturn(com.cloud.host.Status.Up); + when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.singletonList(hostVOMock)); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_NoHostFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(10L); + when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.emptyList()); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertNull(result); + } + + @Test + public void testPerformLockMigration_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + when(volFactory.getVolume(VOLUME_ID)).thenReturn(volumeInfoMock); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + assertNotNull(result); + } + + @Test + public void testPerformLockMigration_SameHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_1); + assertEquals(volumeInfoMock, result); + } + + @Test + public void testPerformLockMigration_SourceHostNull() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(null); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + assertNotNull(result); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_NullVolume() { + volumeService.performLockMigration(null, HOST_ID_2); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_TransferFails() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + } +} diff --git a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java index 7c5692564c99..c9c48dbb179f 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -119,10 +119,10 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { byte[] decodedUserData = null; // If GET, use 4K. If POST, support up to 1M. - if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) { - decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); - } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) { + if (BaseCmd.HTTPMethod.POST.equals(httpmethod)) { decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST); + } else { + decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); } // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. diff --git a/framework/cluster/pom.xml b/framework/cluster/pom.xml index 2dd28e8e628f..75bcaf9a7ddf 100644 --- a/framework/cluster/pom.xml +++ b/framework/cluster/pom.xml @@ -48,6 +48,12 @@ cloud-api ${project.version} + + org.apache.cloudstack + cloud-engine-schema + ${project.version} + compile + diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java new file mode 100644 index 000000000000..fcaa2a22e341 --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.cluster; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "management_server_details") +public class ManagementServerHostDetailVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + long id; + + @Column(name = "management_server_id") + long resourceId; + + @Column(name = "name") + String name; + + @Column(name = "value") + String value; + + @Column(name = "display") + private boolean display = true; + + public ManagementServerHostDetailVO(long poolId, String name, String value, boolean display) { + this.resourceId = poolId; + this.name = name; + this.value = value; + this.display = display; + } + + public ManagementServerHostDetailVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java new file mode 100644 index 000000000000..24fd60d21b3c --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.cluster.dao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +import com.cloud.cluster.ManagementServerHostDetailVO; +import com.cloud.utils.db.GenericDao; + +public interface ManagementServerHostDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java new file mode 100644 index 000000000000..5865bee0926b --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.cluster.dao; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ScopedConfigStorage; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +import com.cloud.cluster.ManagementServerHostDetailVO; + +public class ManagementServerHostDetailsDaoImpl extends ResourceDetailsDaoBase implements ManagementServerHostDetailsDao, ScopedConfigStorage { + + public ManagementServerHostDetailsDaoImpl() { + } + + @Override + public ConfigKey.Scope getScope() { + return ConfigKey.Scope.ManagementServer; + } + + @Override + public String getConfigValue(long id, String key) { + ManagementServerHostDetailVO vo = findDetail(id, key); + return vo == null ? null : vo.getValue(); + } + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ManagementServerHostDetailVO(resourceId, key, value, display)); + } +} diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java index c19ec751153e..fd007f12957b 100644 --- a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ConfigKey.java @@ -269,6 +269,17 @@ public String toString() { private String _defaultValueIfEmpty = null; + private boolean _strictScope = false; + + public boolean isStrictScope() { + return _strictScope; + } + + public ConfigKey withStrictScope() { + this._strictScope = true; + return this; + } + public static void init(ConfigDepotImpl depot) { s_depot = depot; } @@ -420,12 +431,27 @@ protected T valueInGlobalOrAvailableParentScope(Scope scope, Long id) { return value(); } + /** + * @deprecated + * Still used by some external code, but use {@link ConfigKey#valueInScope(Scope, Long)} instead. + */ + public T valueInDomain(Long domainId) { + return valueInScope(Scope.Domain, domainId); + } + public T valueInScope(Scope scope, Long id) { + return valueInScope(scope, id, false); + } + + public T valueInScope(Scope scope, Long id, boolean strictScope) { if (id == null) { - return value(); + return strictScope ? null : value(); } String value = s_depot != null ? s_depot.getConfigStringValue(_name, scope, id) : null; if (value == null) { + if (strictScope) { + return null; + } return valueInGlobalOrAvailableParentScope(scope, id); } logger.trace("Scope({}) value for config ({}): {}", scope, _name, _value); diff --git a/framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java new file mode 100644 index 000000000000..4fcbe4151d30 --- /dev/null +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.framework.config; + +import java.util.function.Consumer; + +public class ValidatedConfigKey extends ConfigKey { + private final Consumer validator; + + public ValidatedConfigKey(String category, Class type, String name, String defaultValue, String description, boolean dynamic, Scope scope, String parent, Consumer validator) { + super(category, type, name, defaultValue, description, dynamic, scope, parent); + this.validator = validator; + } + + public Consumer getValidator() { + return validator; + } + + public void validateValue(String value) { + if (validator != null) { + validator.accept((T) value); + } + } +} diff --git a/framework/db/src/main/java/com/cloud/utils/db/Filter.java b/framework/db/src/main/java/com/cloud/utils/db/Filter.java index 90e42952a990..375e508c55f1 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/Filter.java +++ b/framework/db/src/main/java/com/cloud/utils/db/Filter.java @@ -57,7 +57,18 @@ public Filter(Class clazz, String field, boolean ascending) { } public Filter(long limit) { - _orderBy = " ORDER BY RAND()"; + this(limit, false); + } + + /** + * Constructor for creating a filter with random ordering + * @param limit the maximum number of results to return + * @param randomize if true, orders results randomly + */ + public Filter(long limit, boolean randomize) { + if (randomize) { + _orderBy = " ORDER BY RAND()" ; + } _limit = limit; } diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index 87d9f11d20b0..dcd863465d1b 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -347,7 +347,7 @@ public List lockRows(final SearchCriteria sc, final Filter filter, final b @Override @DB() public T lockOneRandomRow(final SearchCriteria sc, final boolean exclusive) { - final Filter filter = new Filter(1); + final Filter filter = new Filter(1, true); final List beans = search(sc, filter, exclusive, true); return beans.isEmpty() ? null : beans.get(0); } @@ -927,7 +927,7 @@ public Class getEntityBeanType() { @DB() protected T findOneIncludingRemovedBy(final SearchCriteria sc) { - Filter filter = new Filter(1); + Filter filter = new Filter(1, true); List results = searchIncludingRemoved(sc, filter, null, false); assert results.size() <= 1 : "Didn't the limiting worked?"; return results.size() == 0 ? null : results.get(0); @@ -1335,7 +1335,7 @@ public int batchExpunge(final SearchCriteria sc, final Long batchSize) { Filter filter = null; final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); if (batchSizeFinal > 0) { - filter = new Filter(null, batchSizeFinal); + filter = new Filter(batchSizeFinal); } int expunged = 0; int currentExpunged = 0; diff --git a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java index 15807eb26d42..3323d5c4d829 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java +++ b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java @@ -205,6 +205,12 @@ public void setJoinParameters(String joinName, String conditionName, Object... p } + public void setJoinParametersIfNotNull(String joinName, String conditionName, Object... params) { + if (ArrayUtils.isNotEmpty(params) && (params.length > 1 || params[0] != null)) { + setJoinParameters(joinName, conditionName, params); + } + } + public SearchCriteria getJoin(String joinName) { return _joins.get(joinName).getT(); } diff --git a/framework/db/src/main/java/com/cloud/utils/db/SequenceFetcher.java b/framework/db/src/main/java/com/cloud/utils/db/SequenceFetcher.java index f902fda3bf14..a59b73e5cee1 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/SequenceFetcher.java +++ b/framework/db/src/main/java/com/cloud/utils/db/SequenceFetcher.java @@ -64,7 +64,7 @@ public T getNextSequence(Class clazz, TableGenerator tg, Object key, bool try { return future.get(); } catch (Exception e) { - logger.warn("Unable to get sequeunce for " + tg.table() + ":" + tg.pkColumnValue(), e); + logger.warn("Unable to get sequence for " + tg.table() + ":" + tg.pkColumnValue(), e); return null; } } diff --git a/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java b/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java index 079611ab69fb..9f040e7a5e34 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java @@ -41,4 +41,62 @@ public void testAddOrderBy() { Assert.assertTrue(filter.getOrderBy().split(",").length == 3); Assert.assertTrue(filter.getOrderBy().split(",")[2].trim().toLowerCase().equals("test.fld_int asc")); } + + + @Test + public void testFilterWithLimitOnly() { + Filter filter = new Filter(5); + + Assert.assertEquals(Long.valueOf(5), filter.getLimit()); + Assert.assertNull(filter.getOrderBy()); + Assert.assertNull(filter.getOffset()); + } + + @Test + public void testFilterWithLimitAndRandomizeFalse() { + Filter filter = new Filter(10, false); + + Assert.assertEquals(Long.valueOf(10), filter.getLimit()); + Assert.assertNull(filter.getOrderBy()); + Assert.assertNull(filter.getOffset()); + } + + @Test + public void testFilterWithLimitAndRandomizeTrue() { + Filter filter = new Filter(3, true); + + Assert.assertNull(filter.getLimit()); + Assert.assertNotNull(filter.getOrderBy()); + Assert.assertTrue(filter.getOrderBy().contains("ORDER BY RAND()")); + Assert.assertTrue(filter.getOrderBy().contains("LIMIT 3")); + Assert.assertEquals(" ORDER BY RAND() LIMIT 3", filter.getOrderBy()); + } + + @Test + public void testFilterRandomizeWithDifferentLimits() { + Filter filter1 = new Filter(1, true); + Filter filter10 = new Filter(10, true); + Filter filter100 = new Filter(100, true); + + Assert.assertEquals(" ORDER BY RAND() LIMIT 1", filter1.getOrderBy()); + Assert.assertEquals(" ORDER BY RAND() LIMIT 10", filter10.getOrderBy()); + Assert.assertEquals(" ORDER BY RAND() LIMIT 100", filter100.getOrderBy()); + } + + @Test + public void testFilterConstructorBackwardsCompatibility() { + // Test that Filter(long) behaves differently now (no ORDER BY RAND()) + // compared to Filter(long, true) which preserves old behavior + Filter simpleLimitFilter = new Filter(1); + Filter randomFilter = new Filter(1, true); + + // Simple limit filter should just set limit + Assert.assertEquals(Long.valueOf(1), simpleLimitFilter.getLimit()); + Assert.assertNull(simpleLimitFilter.getOrderBy()); + + // Random filter should set orderBy with RAND() + Assert.assertNull(randomFilter.getLimit()); + Assert.assertNotNull(randomFilter.getOrderBy()); + Assert.assertTrue(randomFilter.getOrderBy().contains("RAND()")); + } } diff --git a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java index 308600341c3f..ebf514f532f7 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java @@ -20,6 +20,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import org.junit.Assert; import org.junit.Before; @@ -263,4 +264,71 @@ public void multiJoinSameTableTest() { " INNER JOIN tableA tableA2Alias ON tableC.column3=tableA2Alias.column2 " + " INNER JOIN tableA tableA3Alias ON tableD.column4=tableA3Alias.column3 AND tableD.column5=? ", joinString.toString()); } + + + @Test + public void testLockOneRandomRowUsesRandomFilter() { + // Create a mock DAO to test lockOneRandomRow behavior + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + + // Capture the filter passed to the search method + final Filter[] capturedFilter = new Filter[1]; + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenAnswer(invocation -> { + capturedFilter[0] = invocation.getArgument(1); + return new ArrayList(); + }); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + testDao.lockOneRandomRow(sc, true); + + // Verify that the filter uses random ordering + Assert.assertNotNull(capturedFilter[0]); + Assert.assertNotNull(capturedFilter[0].getOrderBy()); + Assert.assertTrue(capturedFilter[0].getOrderBy().contains("ORDER BY RAND()")); + Assert.assertTrue(capturedFilter[0].getOrderBy().contains("LIMIT 1")); + } + + @Test + public void testLockOneRandomRowReturnsNullOnEmptyResult() { + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenReturn(new ArrayList()); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + DbTestVO result = testDao.lockOneRandomRow(sc, true); + + Assert.assertNull(result); + } + + @Test + public void testLockOneRandomRowReturnsFirstElement() { + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + DbTestVO expectedResult = new DbTestVO(); + List resultList = new ArrayList<>(); + resultList.add(expectedResult); + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenReturn(resultList); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + DbTestVO result = testDao.lockOneRandomRow(sc, true); + + Assert.assertNotNull(result); + Assert.assertEquals(expectedResult, result); + } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java index 5ab54149645e..9d76a7e6ec25 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java @@ -83,6 +83,12 @@ public class CreateExtensionCmd extends BaseCmd { description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue") protected Map details; + @Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING, + description = "Resource detail names as comma separated string that should be reserved and not visible " + + "to end users", + since = "4.22.1") + protected String reservedResourceDetails; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -115,6 +121,10 @@ public Map getDetails() { return convertDetailsToMap(details); } + public String getReservedResourceDetails() { + return reservedResourceDetails; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java index 4426f259380b..453578a95d75 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java @@ -70,6 +70,11 @@ public class ListExtensionsCmd extends BaseListCmd { + " When no parameters are passed, all the details are returned.") private List details; + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, + description = "Type of the extension (e.g. Orchestrator, NetworkOrchestrator)", + since = "4.23.0") + private String type; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -82,6 +87,10 @@ public Long getExtensionId() { return extensionId; } + public String getType() { + return type; + } + public EnumSet getDetails() throws InvalidParameterValueException { if (CollectionUtils.isEmpty(details)) { return EnumSet.of(ApiConstants.ExtensionDetails.all); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java index dea09cf16833..9e4c2cc27331 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java @@ -116,6 +116,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "Running custom action"; + return "Running custom action with ID: " + getResourceUuid(ApiConstants.CUSTOM_ACTION_ID); } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java index ded07d2dd323..5baaea1709db 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java @@ -78,6 +78,12 @@ public class UpdateExtensionCmd extends BaseCmd { "if false or not set, no action)") private Boolean cleanupDetails; + @Parameter(name = ApiConstants.RESERVED_RESOURCE_DETAILS, type = CommandType.STRING, + description = "Resource detail names as comma separated string that should be reserved and not visible " + + "to end users", + since = "4.22.1") + protected String reservedResourceDetails; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -106,6 +112,10 @@ public Boolean isCleanupDetails() { return cleanupDetails; } + public String getReservedResourceDetails() { + return reservedResourceDetails; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java new file mode 100644 index 000000000000..6c755ecd1a54 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmd.java @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.extensions.api; + +import java.util.EnumSet; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; + +import com.cloud.user.Account; + +@APICommand(name = "updateRegisteredExtension", + description = "Update details for an extension registered with a resource", + responseObject = ExtensionResponse.class, + responseHasSensitiveInfo = false, + entityType = {Extension.class}, + authorized = {RoleType.Admin}, + since = "4.23.0") +public class UpdateRegisteredExtensionCmd extends BaseCmd { + + @Inject + ExtensionsManager extensionsManager; + + @Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true, + entityType = ExtensionResponse.class, description = "ID of the extension") + private Long extensionId; + + @Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, + description = "ID of the resource where the extension is registered") + private String resourceId; + + @Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, + description = "Type of the resource") + private String resourceType; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, + description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue") + protected Map details; + + @Parameter(name = ApiConstants.CLEAN_UP_DETAILS, + type = CommandType.BOOLEAN, + description = "Optional boolean field, which indicates if details should be cleaned up or not " + + "(If set to true, details removed for this registration, details field ignored; " + + "if false or not set, details can be updated through details map)") + private Boolean cleanupDetails; + + public Long getExtensionId() { + return extensionId; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceType() { + return resourceType; + } + + public Map getDetails() { + return convertDetailsToMap(details); + } + + public Boolean isCleanupDetails() { + return cleanupDetails; + } + + @Override + public void execute() throws ServerApiException { + Extension extension = extensionsManager.updateRegisteredExtensionWithResource(this); + ExtensionResponse response = extensionsManager.createExtensionResponse(extension, + EnumSet.of(ApiConstants.ExtensionDetails.all)); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.Extension; + } + + @Override + public Long getApiResourceId() { + return getExtensionId(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java index 3355457ed25b..9adb625b19ed 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java @@ -16,6 +16,9 @@ // under the License. package org.apache.cloudstack.framework.extensions.dao; +import java.util.List; + +import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; import com.cloud.utils.db.GenericDao; @@ -23,4 +26,8 @@ public interface ExtensionDao extends GenericDao { ExtensionVO findByName(String name); + + List listByType(Extension.Type type); + + ExtensionVO findByNameAndType(String name, Extension.Type type); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java index 8e17199de6ca..f3c3173585b2 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java @@ -17,6 +17,9 @@ package org.apache.cloudstack.framework.extensions.dao; +import java.util.List; + +import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; import com.cloud.utils.db.GenericDaoBase; @@ -39,7 +42,21 @@ public ExtensionDaoImpl() { public ExtensionVO findByName(String name) { SearchCriteria sc = AllFieldSearch.create(); sc.setParameters("name", name); + return findOneBy(sc); + } + + @Override + public List listByType(Extension.Type type) { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("type", type); + return listBy(sc); + } + @Override + public ExtensionVO findByNameAndType(String name, Extension.Type type) { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("name", name); + sc.setParameters("type", type); return findOneBy(sc); } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java index 930ef8675531..b9fadb76d068 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java @@ -28,5 +28,9 @@ public interface ExtensionResourceMapDao extends GenericDao listResourceIdsByExtensionIdAndType(long extensionId,ExtensionResourceMap.ResourceType resourceType); + ExtensionResourceMapVO findResourceByExtensionIdAndResourceIdAndType(long extensionId, long resourceId, ExtensionResourceMap.ResourceType resourceType); + + List listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType); + + List listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java index 6f19ef8b8b66..11c64e15cd21 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java @@ -55,6 +55,17 @@ public ExtensionResourceMapVO findByResourceIdAndType(long resourceId, return findOneBy(sc); } + + @Override + public ExtensionResourceMapVO findResourceByExtensionIdAndResourceIdAndType(long extensionId, long resourceId, + ExtensionResourceMap.ResourceType resourceType) { + SearchCriteria sc = genericSearch.create(); + sc.setParameters("extensionId", extensionId); + sc.setParameters("resourceId", resourceId); + sc.setParameters("resourceType", resourceType); + return findOneBy(sc); + } + @Override public List listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType) { GenericSearchBuilder sb = createSearchBuilder(Long.class); @@ -67,4 +78,15 @@ public List listResourceIdsByExtensionIdAndType(long extensionId, Extensio sc.setParameters("resourceType", resourceType); return customSearch(sc, null); } + + @Override + public List listResourceIdsByType(ExtensionResourceMap.ResourceType resourceType) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getResourceId()); + sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + return customSearch(sc, null); + } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java index 1b1a175c5975..cd033badff15 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java @@ -42,6 +42,7 @@ import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateRegisteredExtensionCmd; import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; import com.cloud.agent.api.Answer; @@ -65,6 +66,8 @@ public interface ExtensionsManager extends Manager { Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd); + Extension updateRegisteredExtensionWithResource(UpdateRegisteredExtensionCmd cmd); + Extension updateExtension(UpdateExtensionCmd cmd); Extension registerExtensionWithResource(RegisterExtensionCmd cmd); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java index 4171b9615fea..a209acf6442e 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -61,6 +61,7 @@ import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionCustomAction; import org.apache.cloudstack.extension.ExtensionHelper; +import org.apache.cloudstack.extension.NetworkCustomActionProvider; import org.apache.cloudstack.extension.ExtensionResourceMap; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -75,6 +76,7 @@ import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateRegisteredExtensionCmd; import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand; import org.apache.cloudstack.framework.extensions.command.ExtensionRoutingUpdateCommand; import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; @@ -120,11 +122,27 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; import com.cloud.host.Host; -import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.ExternalProvisioner; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Network; +import com.cloud.network.Network.Capability; +import com.cloud.network.Network.Service; +import com.cloud.network.NetworkModel; +import com.cloud.network.PhysicalNetworkServiceProvider; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkServiceMapDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkServiceProviderDao; +import com.cloud.network.dao.PhysicalNetworkServiceProviderVO; +import com.cloud.network.element.NetworkElement; +import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao; +import com.cloud.network.vpc.dao.VpcServiceMapDao; +import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; import com.cloud.org.Cluster; import com.cloud.serializer.GsonHelper; import com.cloud.storage.dao.VMTemplateDao; @@ -141,13 +159,15 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallbackWithException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.VmDetailConstants; -import com.cloud.vm.dao.VMInstanceDao; public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsManager, ExtensionHelper, PluggableService, Configurable { @@ -171,6 +191,12 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana @Inject ClusterDao clusterDao; + @Inject + PhysicalNetworkDao physicalNetworkDao; + + @Inject + PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao; + @Inject AgentManager agentMgr; @@ -189,9 +215,6 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana @Inject ExtensionCustomActionDetailsDao extensionCustomActionDetailsDao; - @Inject - VMInstanceDao vmInstanceDao; - @Inject VirtualMachineManager virtualMachineManager; @@ -210,12 +233,35 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana @Inject VMTemplateDao templateDao; + @Inject + NetworkDao networkDao; + + @Inject + NetworkServiceMapDao networkServiceMapDao; + + @Inject + VpcServiceMapDao vpcServiceMapDao; + + @Inject + NetworkOfferingServiceMapDao networkOfferingServiceMapDao; + + @Inject + VpcOfferingServiceMapDao vpcOfferingServiceMapDao; + + @Inject + NetworkModel networkModel; + @Inject RoleService roleService; @Inject AccountService accountService; + // Map of in-built extension names and their reserved resource details that shouldn't be accessible to end-users + protected static final Map> INBUILT_RESERVED_RESOURCE_DETAILS = Map.of( + "proxmox", List.of("proxmox_vmid") + ); + private ScheduledExecutorService extensionPathStateCheckExecutor; protected String getDefaultExtensionRelativePath(String name) { @@ -244,7 +290,7 @@ protected String getValidatedExtensionRelativePath(String name, String relativeP protected Pair getResultFromAnswersString(String answersStr, Extension extension, ManagementServerHostVO msHost, String op) { - Answer[] answers = null; + Answer[] answers; try { answers = GsonHelper.getGson().fromJson(answersStr, Answer[].class); } catch (Exception e) { @@ -334,6 +380,39 @@ protected Pair getChecksumForExtensionPathOnMSPeer(Extension ex return getResultFromAnswersString(answersStr, extension, msHost, "get path checksum"); } + protected List buildExtensionResourceDetailsArray(long extensionResourceMapId, + Map details) { + List detailsList = new ArrayList<>(); + if (MapUtils.isEmpty(details)) { + return detailsList; + } + for (Map.Entry entry : details.entrySet()) { + boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase()); + detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(), + entry.getValue(), display)); + } + return detailsList; + } + + protected void appendHiddenExtensionResourceDetails(long extensionResourceMapId, + List detailsList) { + if (CollectionUtils.isEmpty(detailsList)) { + return; + } + Map hiddenDetails = extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapId, false); + if (MapUtils.isEmpty(hiddenDetails)) { + return; + } + Set requestedKeys = detailsList.stream() + .map(ExtensionResourceMapDetailsVO::getName) + .collect(Collectors.toSet()); + hiddenDetails.forEach((key, value) -> { + if (!requestedKeys.contains(key)) { + detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, key, value, false)); + } + }); + } + protected List getParametersListFromMap(String actionName, Map parametersMap) { if (MapUtils.isEmpty(parametersMap)) { return Collections.emptyList(); @@ -355,26 +434,48 @@ protected void unregisterExtensionWithCluster(String clusterUuid, Long extension unregisterExtensionWithCluster(cluster, extensionId); } - protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType resourceType, String resourceUuid) { + protected Extension getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType resourceType, String resourceUuid) { Object object = entityManager.findByUuid(resourceType.getAssociatedClass(), resourceUuid); if (object == null) { return null; } - Long clusterId = null; - if (resourceType == ExtensionCustomAction.ResourceType.VirtualMachine) { + if (ExtensionCustomAction.ResourceType.VirtualMachine.equals(resourceType)) { VirtualMachine vm = (VirtualMachine) object; Pair clusterHostId = virtualMachineManager.findClusterAndHostIdForVm(vm, false); - clusterId = clusterHostId.first(); - } - if (clusterId == null) { + Long clusterId = clusterHostId.first(); + if (clusterId == null) { + return null; + } + ExtensionResourceMapVO mapVO = + extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster); + if (mapVO == null) { + return null; + } + return extensionDao.findById(mapVO.getExtensionId()); + } else if (ExtensionCustomAction.ResourceType.Network.equals(resourceType)) { + Network network = (Network) object; + Long physicalNetworkId = network.getPhysicalNetworkId(); + if (physicalNetworkId == null) { + return null; + } + // Use provider-based lookup: match the network's service-map providers + // against extension names registered on the physical network. + // This correctly handles multiple different extensions on the same physical network. + String providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), Service.CustomAction); + if (providerName != null) { + return getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName); + } return null; - } - ExtensionResourceMapVO mapVO = - extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster); - if (mapVO == null) { + } else if (ExtensionCustomAction.ResourceType.Vpc.equals(resourceType)) { + Vpc vpc = (Vpc) object; + // Find extension via the VPC's CustomAction service provider + String providerName = vpcServiceMapDao.getProviderForServiceInVpc(vpc.getId(), Service.CustomAction); + if (providerName != null) { + return extensionDao.findByNameAndType(providerName, Extension.Type.NetworkOrchestrator); + } return null; } - return extensionDao.findById(mapVO.getExtensionId()); + return null; } protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension, @@ -563,6 +664,25 @@ protected void checkExtensionPathState(Extension extension, List reservedResourceDetails) { + ExtensionVO vo = extensionDao.findById(extensionId); + if (vo == null || vo.isUserDefined()) { + return; + } + String lowerName = StringUtils.defaultString(vo.getName()).toLowerCase(); + Optional>> match = INBUILT_RESERVED_RESOURCE_DETAILS.entrySet().stream() + .filter(e -> lowerName.contains(e.getKey().toLowerCase())) + .findFirst(); + if (match.isPresent()) { + Set existing = new HashSet<>(reservedResourceDetails); + for (String detailKey : match.get().getValue()) { + if (existing.add(detailKey)) { + reservedResourceDetails.add(detailKey); + } + } + } + } + @Override public String getExtensionsPath() { return externalProvisioner.getExtensionsPath(); @@ -577,6 +697,7 @@ public Extension createExtension(CreateExtensionCmd cmd) { String relativePath = cmd.getPath(); final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); final String stateStr = cmd.getState(); + final String reservedResourceDetails = cmd.getReservedResourceDetails(); ExtensionVO extensionByName = extensionDao.findByName(name); if (extensionByName != null) { throw new CloudRuntimeException("Extension by name already exists"); @@ -624,6 +745,10 @@ public Extension createExtension(CreateExtensionCmd cmd) { ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, String.valueOf(orchestratorRequiresPrepareVm), false)); } + if (StringUtils.isNotBlank(reservedResourceDetails)) { + detailsVOList.add(new ExtensionDetailsVO(extension.getId(), + ApiConstants.RESERVED_RESOURCE_DETAILS, reservedResourceDetails, false)); + } if (CollectionUtils.isNotEmpty(detailsVOList)) { extensionDetailsDao.saveDetails(detailsVOList); } @@ -665,12 +790,16 @@ public List listExtensions(ListExtensionsCmd cmd) { Long id = cmd.getExtensionId(); String name = cmd.getName(); String keyword = cmd.getKeyword(); + String typeStr = cmd.getType(); + final SearchBuilder sb = extensionDao.createSearchBuilder(); final Filter searchFilter = new Filter(ExtensionVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal()); sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("type", sb.entity().getType(), SearchCriteria.Op.EQ); + sb.done(); final SearchCriteria sc = sb.create(); if (id != null) { @@ -685,6 +814,14 @@ public List listExtensions(ListExtensionsCmd cmd) { sc.setParameters("keyword", "%" + keyword + "%"); } + if (typeStr != null) { + Extension.Type type = EnumUtils.getEnum(Extension.Type.class, typeStr); + if (type == null) { + throw new InvalidParameterValueException("Invalid type: " + typeStr); + } + sc.setParameters("type", type); + } + final Pair, Integer> result = extensionDao.searchAndCount(sc, searchFilter); List responses = new ArrayList<>(); for (ExtensionVO extension : result.first()) { @@ -704,6 +841,7 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { final String stateStr = cmd.getState(); final Map details = cmd.getDetails(); final Boolean cleanupDetails = cmd.isCleanupDetails(); + final String reservedResourceDetails = cmd.getReservedResourceDetails(); final ExtensionVO extensionVO = extensionDao.findById(id); if (extensionVO == null) { throw new InvalidParameterValueException("Failed to find the extension"); @@ -727,12 +865,22 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { } } final boolean updateNeededFinal = updateNeeded; + final Set oldNetworkServices = Extension.Type.NetworkOrchestrator.equals(extensionVO.getType()) ? + resolveExtensionServices(extensionVO) : Collections.emptySet(); ExtensionVO result = Transaction.execute((TransactionCallbackWithException) status -> { if (updateNeededFinal && !extensionDao.update(id, extensionVO)) { throw new CloudRuntimeException(String.format("Failed to updated the extension: %s", extensionVO.getName())); } - updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, id); + updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, reservedResourceDetails, + id); + if (Extension.Type.NetworkOrchestrator.equals(extensionVO.getType())) { + Set newNetworkServices = resolveExtensionServices(extensionVO); + if (!newNetworkServices.equals(oldNetworkServices)) { + updateNetworkExtensionServicesOnPhysicalNetworks(extensionVO, oldNetworkServices, + newNetworkServices); + } + } return extensionVO; }); if (StringUtils.isNotBlank(stateStr)) { @@ -748,9 +896,11 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { return result; } - protected void updateExtensionsDetails(Boolean cleanupDetails, Map details, Boolean orchestratorRequiresPrepareVm, long id) { + protected void updateExtensionsDetails(Boolean cleanupDetails, Map details, + Boolean orchestratorRequiresPrepareVm, String reservedResourceDetails, long id) { final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details); - if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null) { + if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null && + StringUtils.isBlank(reservedResourceDetails)) { return; } if (needToUpdateAllDetails) { @@ -761,6 +911,9 @@ protected void updateExtensionsDetails(Boolean cleanupDetails, Map detailsVOList.add( new ExtensionDetailsVO(id, key, value, false))); @@ -775,15 +928,29 @@ protected void updateExtensionsDetails(Boolean cleanupDetails, Map details = cmd.getDetails(); + final Boolean cleanupDetails = cmd.isCleanupDetails(); + + if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) { + throw new InvalidParameterValueException( + String.format("Currently only [%s] can be used to update an extension registration", + EnumSet.allOf(ExtensionResourceMap.ResourceType.class))); } ExtensionVO extension = extensionDao.findById(extensionId); if (extension == null) { throw new InvalidParameterValueException("Invalid extension specified"); } - ExtensionResourceMap extensionResourceMap = registerExtensionWithCluster(clusterVO, extension, cmd.getDetails()); - return extensionDao.findById(extensionResourceMap.getExtensionId()); + + ExtensionResourceMap.ResourceType resType = EnumUtils.getEnum(ExtensionResourceMap.ResourceType.class, resourceType); + long resolvedResourceId; + if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) { + PhysicalNetworkVO physicalNetwork = physicalNetworkDao.findByUuid(resourceId); + if (physicalNetwork == null) { + throw new InvalidParameterValueException("Invalid physical network ID specified"); + } + resolvedResourceId = physicalNetwork.getId(); + } else { + ClusterVO clusterVO = clusterDao.findByUuid(resourceId); + if (clusterVO == null) { + throw new InvalidParameterValueException("Invalid cluster ID specified"); + } + resolvedResourceId = clusterVO.getId(); + } + + ExtensionResourceMapVO targetMapping = extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType( + extensionId, resolvedResourceId, resType); + if (targetMapping == null) { + throw new InvalidParameterValueException(String.format( + "Extension '%s' is not registered with resource %s (%s)", + extension.getName(), resourceId, resourceType)); + } + + if (Boolean.TRUE.equals(cleanupDetails)) { + extensionResourceMapDetailsDao.removeDetails(targetMapping.getId()); + } else if (MapUtils.isNotEmpty(details)) { + List detailsList = buildExtensionResourceDetailsArray(targetMapping.getId(), details); + appendHiddenExtensionResourceDetails(targetMapping.getId(), detailsList); + detailsList = detailsList.stream() + .filter(detail -> detail.getValue() != null) + .collect(Collectors.toList()); + extensionResourceMapDetailsDao.saveDetails(detailsList); + } + + return extensionDao.findById(extensionId); } @Override @@ -873,8 +1109,9 @@ public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extens List detailsVOList = new ArrayList<>(); if (MapUtils.isNotEmpty(details)) { for (Map.Entry entry : details.entrySet()) { + boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase()); detailsVOList.add(new ExtensionResourceMapDetailsVO(savedExtensionMap.getId(), - entry.getKey(), entry.getValue())); + entry.getKey(), entry.getValue(), display)); } extensionResourceMapDetailsDao.saveDetails(detailsVOList); } @@ -884,6 +1121,227 @@ public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extens return result; } + protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetworkVO physicalNetwork, + Extension extension, Map details) { + // Only NetworkOrchestrator extensions can be registered with physical networks + if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) { + throw new InvalidParameterValueException(String.format( + "Only extensions of type %s can be registered with a physical network. " + + "Extension '%s' is of type %s.", + Extension.Type.NetworkOrchestrator.name(), + extension.getName(), extension.getType().name())); + } + + // Block registering the exact same extension twice on the same physical network + final ExtensionResourceMap.ResourceType resourceType = ExtensionResourceMap.ResourceType.PhysicalNetwork; + ExtensionResourceMapVO existing = extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType( + extension.getId(), physicalNetwork.getId(), resourceType); + if (existing != null) { + throw new CloudRuntimeException(String.format( + "Extension '%s' is already registered with physical network %s", + extension.getName(), physicalNetwork)); + } + + // Resolve which services this extension provides from its network.services detail + Set services = resolveExtensionServices(extension); + + return Transaction.execute((TransactionCallbackWithException) status -> { + // 1. Persist the extension<->physical-network mapping + ExtensionResourceMapVO extensionMap = new ExtensionResourceMapVO(extension.getId(), + physicalNetwork.getId(), resourceType); + ExtensionResourceMapVO savedExtensionMap = extensionResourceMapDao.persist(extensionMap); + + // 2. Persist device credentials / details + List detailsVOList = new ArrayList<>(); + if (MapUtils.isNotEmpty(details)) { + for (Map.Entry entry : details.entrySet()) { + boolean display = !SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase()); + detailsVOList.add(new ExtensionResourceMapDetailsVO(savedExtensionMap.getId(), + entry.getKey(), entry.getValue(), display)); + } + extensionResourceMapDetailsDao.saveDetails(detailsVOList); + } + + // 3. Auto-create the NetworkServiceProvider entry for this extension so that + // the services are visible in the UI and in listSupportedNetworkServices. + // The NSP name equals the extension name; state is Enabled by default. + PhysicalNetworkServiceProviderVO existingNsp = + physicalNetworkServiceProviderDao.findByServiceProvider( + physicalNetwork.getId(), extension.getName()); + if (existingNsp == null) { + PhysicalNetworkServiceProviderVO nsp = + new PhysicalNetworkServiceProviderVO(physicalNetwork.getId(), extension.getName()); + applyServicesToNsp(nsp, services); + nsp.setState(PhysicalNetworkServiceProvider.State.Enabled); + physicalNetworkServiceProviderDao.persist(nsp); + logger.info("Auto-created NetworkServiceProvider '{}' (Enabled) for physical network {} " + + "with services {}", extension, physicalNetwork, services); + } + + return savedExtensionMap; + }); + } + + /** + * Resolves the set of network service names declared in the extension's + * {@code network.services} detail. Falls back to an empty set if not present + */ + private Set resolveExtensionServices(Extension extension) { + Map extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId()); + Set parsed = parseNetworkServicesFromDetailKeys(extDetails); + if (!parsed.isEmpty()) { + return parsed; + } + // Falls back to an empty set if not present + return new HashSet<>(); + } + + /** + * Resolves the set of service names from the extension detail map. + * From {@code network.services} comma-separated key. + */ + private Set parseNetworkServicesFromDetailKeys(Map extDetails) { + if (extDetails == null) { + return Collections.emptySet(); + } + // New format: "network.services" = "SourceNat,StaticNat,..." + if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) { + String value = extDetails.get(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY); + if (StringUtils.isNotBlank(value)) { + Set services = new HashSet<>(); + for (String s : value.split(",")) { + Service service = Network.Service.getService(s.trim()); + if (service != null) { + services.add(service); + } + } + if (!services.isEmpty()) { + return services; + } + } + } + + return Collections.emptySet(); + } + + /** + * Builds a full {@code Map>} from the + * extension detail map. From the split keys + * {@code network.services} + {@code network.service.capabilities}. + */ + private Map> buildCapabilitiesFromDetailKeys( + Map extDetails) { + if (extDetails == null) { + return new HashMap<>(); + } + // New split format + if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) { + Set serviceNames = parseNetworkServicesFromDetailKeys(extDetails); + if (!serviceNames.isEmpty()) { + JsonObject capsObj = null; + if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY)) { + try { + capsObj = JsonParser.parseString( + extDetails.get(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY)) + .getAsJsonObject(); + } catch (Exception e) { + logger.warn("Failed to parse network.service.capabilities JSON: {}", e.getMessage()); + } + } + Map> result = new HashMap<>(); + for (Service service : serviceNames) { + Map capMap = new HashMap<>(); + if (capsObj != null && capsObj.has(service.getName())) { + JsonObject svcCaps = capsObj.getAsJsonObject(service.getName()); + for (Map.Entry entry : svcCaps.entrySet()) { + Capability cap = Capability.getCapability(entry.getKey()); + if (cap != null) { + capMap.put(cap, entry.getValue().getAsString()); + } + } + } + result.put(service, capMap); + } + return result; + } + } + + return new HashMap<>(); + } + + /** + * Sets the boolean service-provided flags on a {@link PhysicalNetworkServiceProviderVO} + * based on a set of service names. + */ + private void applyServicesToNsp(PhysicalNetworkServiceProviderVO nsp, Set services) { + nsp.setSourcenatServiceProvided(services.contains(Service.SourceNat)); + nsp.setStaticnatServiceProvided(services.contains(Service.StaticNat)); + nsp.setPortForwardingServiceProvided(services.contains(Service.PortForwarding)); + nsp.setFirewallServiceProvided(services.contains(Service.Firewall)); + nsp.setGatewayServiceProvided(services.contains(Service.Gateway)); + nsp.setDnsServiceProvided(services.contains(Service.Dns)); + nsp.setDhcpServiceProvided(services.contains(Service.Dhcp)); + nsp.setUserdataServiceProvided(services.contains(Service.UserData)); + nsp.setLbServiceProvided(services.contains(Service.Lb)); + nsp.setVpnServiceProvided(services.contains(Service.Vpn)); + nsp.setSecuritygroupServiceProvided(services.contains(Service.SecurityGroup)); + nsp.setNetworkAclServiceProvided(services.contains(Service.NetworkACL)); + nsp.setCustomActionServiceProvided(services.contains(Service.CustomAction)); + } + + /** + * Propagates a change in the set of network services declared by a NetworkOrchestrator + * extension to every physical network the extension is registered with. A service that is + * still referenced by a network or VPC offering (i.e. the extension is configured as the + * provider for that service) cannot be removed. Otherwise, every physical network's provider + * is updated to add newly declared services and drop services that are no longer declared. + */ + protected void updateNetworkExtensionServicesOnPhysicalNetworks(ExtensionVO extension, + Set oldServices, Set newServices) { + Set removedServices = new HashSet<>(oldServices); + removedServices.removeAll(newServices); + Set addedServices = new HashSet<>(newServices); + addedServices.removeAll(oldServices); + if (removedServices.isEmpty() && addedServices.isEmpty()) { + return; + } + for (Service removedService : removedServices) { + boolean usedByNetworkOffering = CollectionUtils.isNotEmpty( + networkOfferingServiceMapDao.listOfferingIdsByServiceAndProvider(removedService, extension.getName())); + boolean usedByVpcOffering = CollectionUtils.isNotEmpty( + vpcOfferingServiceMapDao.listOfferingIdsByServiceAndProvider(removedService, extension.getName())); + if (usedByNetworkOffering || usedByVpcOffering) { + throw new CloudRuntimeException(String.format( + "Cannot remove service %s from extension '%s' as it is used by a network/VPC offering", + removedService.getName(), extension.getName())); + } + } + List physicalNetworkIds = extensionResourceMapDao.listResourceIdsByExtensionIdAndType( + extension.getId(), ExtensionResourceMap.ResourceType.PhysicalNetwork); + for (Long physicalNetworkId : physicalNetworkIds) { + PhysicalNetworkServiceProviderVO nsp = physicalNetworkServiceProviderDao.findByServiceProvider( + physicalNetworkId, extension.getName()); + if (nsp == null) { + continue; + } + List enabledServices = new ArrayList<>(nsp.getEnabledServices()); + enabledServices.removeAll(removedServices); + for (Service addedService : addedServices) { + if (!enabledServices.contains(addedService)) { + enabledServices.add(addedService); + } + } + nsp.setEnabledServices(enabledServices); + physicalNetworkServiceProviderDao.update(nsp.getId(), nsp); + logger.info("Updated NetworkServiceProvider '{}' on physical network {} with services {}", + extension, physicalNetworkId, enabledServices); + } + } + + /** Keys that are always stored with display=false (sensitive). */ + private static final Set SENSITIVE_DETAIL_KEYS = + Set.of("password", "sshkey"); + @Override @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_UNREGISTER, eventDescription = "unregistering extension resource") public Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd) { @@ -892,10 +1350,15 @@ public Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd) { final String resourceType = cmd.getResourceType(); if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) { throw new InvalidParameterValueException( - String.format("Currently only [%s] can be used to unregister an extension of type Orchestrator", + String.format("Currently only [%s] can be used to unregister an extension", EnumSet.allOf(ExtensionResourceMap.ResourceType.class))); } - unregisterExtensionWithCluster(resourceId, extensionId); + ExtensionResourceMap.ResourceType resType = EnumUtils.getEnum(ExtensionResourceMap.ResourceType.class, resourceType); + if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(resType)) { + unregisterExtensionWithPhysicalNetwork(resourceId, extensionId); + } else { + unregisterExtensionWithCluster(resourceId, extensionId); + } return extensionDao.findById(extensionId); } @@ -915,6 +1378,45 @@ public void unregisterExtensionWithCluster(Cluster cluster, Long extensionId) { } } + protected void unregisterExtensionWithPhysicalNetwork(String resourceId, Long extensionId) { + PhysicalNetworkVO physicalNetwork = physicalNetworkDao.findByUuid(resourceId); + if (physicalNetwork == null) { + throw new InvalidParameterValueException("Invalid physical network ID specified"); + } + // Find the specific map entry for this extension+physical-network combination + ExtensionResourceMapVO existing = extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType( + extensionId, physicalNetwork.getId(), ExtensionResourceMap.ResourceType.PhysicalNetwork); + if (existing == null) { + return; + } + + ExtensionVO ext = extensionDao.findById(existing.getExtensionId()); + if (ext != null) { + List networksUsingProvider = networkDao.listByPhysicalNetworkAndProvider( + physicalNetwork.getId(), ext.getName()); + if (CollectionUtils.isNotEmpty(networksUsingProvider)) { + throw new CloudRuntimeException(String.format( + "Cannot unregister extension '%s' from physical network %s. " + + "Provider is used by %d existing network service(s)", + ext.getName(), physicalNetwork, networksUsingProvider.size())); + } + } + + extensionResourceMapDao.remove(existing.getId()); + extensionResourceMapDetailsDao.removeDetails(existing.getId()); + + // Also remove the auto-created NSP for this extension + if (ext != null) { + PhysicalNetworkServiceProviderVO nsp = + physicalNetworkServiceProviderDao.findByServiceProvider(physicalNetwork.getId(), ext.getName()); + if (nsp != null) { + physicalNetworkServiceProviderDao.remove(nsp.getId()); + logger.info("Removed NetworkServiceProvider '{}' from physical network {} " + + "on extension unregister", ext, physicalNetwork); + } + } + } + @Override public ExtensionResponse createExtensionResponse(Extension extension, EnumSet viewDetails) { @@ -938,6 +1440,12 @@ public ExtensionResponse createExtensionResponse(Extension extension, Cluster cluster = clusterDao.findById(extensionResourceMapVO.getResourceId()); extensionResourceResponse.setId(cluster.getUuid()); extensionResourceResponse.setName(cluster.getName()); + } else if (ExtensionResourceMap.ResourceType.PhysicalNetwork.equals(extensionResourceMapVO.getResourceType())) { + PhysicalNetworkVO pn = physicalNetworkDao.findById(extensionResourceMapVO.getResourceId()); + if (pn != null) { + extensionResourceResponse.setId(pn.getUuid()); + extensionResourceResponse.setName(pn.getName()); + } } Map details = extensionResourceMapDetailsDao.listDetailsKeyPairs( extensionResourceMapVO.getId(), true); @@ -961,12 +1469,16 @@ public ExtensionResponse createExtensionResponse(Extension extension, hiddenDetails = extensionDetails.second(); } else { hiddenDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(), - List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)); + List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + ApiConstants.RESERVED_RESOURCE_DETAILS)); } if (hiddenDetails.containsKey(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)) { response.setOrchestratorRequiresPrepareVm(Boolean.parseBoolean( hiddenDetails.get(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))); } + if (hiddenDetails.containsKey(ApiConstants.RESERVED_RESOURCE_DETAILS)) { + response.setReservedResourceDetails(hiddenDetails.get(ApiConstants.RESERVED_RESOURCE_DETAILS)); + } response.setObjectName(Extension.class.getSimpleName().toLowerCase()); return response; } @@ -1089,7 +1601,7 @@ public List listCustomActions(ListCustomActionCmd } if (extensionId == null && resourceType != null && StringUtils.isNotBlank(resourceId)) { - Extension extension = getExtensionFromResource(resourceType, resourceId); + Extension extension = getExtensionWithCustomActionFromResource(resourceType, resourceId); if (extension == null) { logger.error("No extension found for the specified resource [type: {}, id: {}]", resourceTypeStr, resourceId); throw new InvalidParameterValueException("Internal error listing custom actions with specified resource"); @@ -1281,7 +1793,7 @@ protected void updatedCustomActionDetails(long id, Boolean cleanupDetails, Map cmdParameters = cmd.getParameters(); @@ -1306,8 +1818,6 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { logger.error("Failed to run {} as it is not enabled", customActionVO); throw new InvalidParameterValueException(error); } - final String actionName = customActionVO.getName(); - RunCustomActionCommand runCustomActionCommand = new RunCustomActionCommand(actionName); final long extensionId = customActionVO.getExtensionId(); final ExtensionVO extensionVO = extensionDao.findById(extensionId); if (extensionVO == null) { @@ -1338,39 +1848,47 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { logger.error("Specified resource does not exist for running {}", customActionVO); throw new CloudRuntimeException(error); } - Long clusterId = null; - Long hostId = null; - if (entity instanceof Cluster) { - clusterId = ((Cluster)entity).getId(); - List hosts = hostDao.listByClusterAndHypervisorType(clusterId, Hypervisor.HypervisorType.External); - if (CollectionUtils.isEmpty(hosts)) { - logger.error("No hosts found for {} for running {}", entity, customActionVO); - throw new CloudRuntimeException(error); - } - hostId = hosts.get(0).getId(); - } else if (entity instanceof Host) { - Host host = (Host)entity; - if (!Hypervisor.HypervisorType.External.equals(host.getHypervisorType())) { - logger.error("Invalid {} specified as host resource for running {}", entity, customActionVO); - throw new InvalidParameterValueException(error); - } - hostId = host.getId(); - clusterId = host.getClusterId(); - } else if (entity instanceof VirtualMachine) { - VirtualMachine virtualMachine = (VirtualMachine)entity; + if (entity instanceof VirtualMachine) { + VirtualMachine virtualMachine = (VirtualMachine) entity; accountService.checkAccess(caller, null, true, virtualMachine); - if (!Hypervisor.HypervisorType.External.equals(virtualMachine.getHypervisorType())) { - logger.error("Invalid {} specified as VM resource for running {}", entity, customActionVO); - throw new InvalidParameterValueException(error); - } - VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(virtualMachine); - VirtualMachineTO virtualMachineTO = virtualMachineManager.toVmTO(vmProfile); - runCustomActionCommand.setVmTO(virtualMachineTO); - Pair clusterAndHostId = virtualMachineManager.findClusterAndHostIdForVm(virtualMachine, false); - clusterId = clusterAndHostId.first(); - hostId = clusterAndHostId.second(); + return runVirtualMachineCustomAction(virtualMachine, customActionVO, extensionVO, + actionResourceType, cmdParameters); + } else if (entity instanceof Network) { + // Network custom action: dispatched directly to NetworkCustomActionProvider (no agent) + Network network = (Network) entity; + accountService.checkAccess(caller, null, true, network); + return runNetworkCustomAction(network, customActionVO, extensionVO, actionResourceType, cmdParameters); + } else if (entity instanceof Vpc) { + // VPC custom action: dispatched directly to NetworkCustomActionProvider (no agent) + Vpc vpc = (Vpc) entity; + accountService.checkAccess(caller, null, true, vpc); + return runVpcCustomAction(vpc, customActionVO, extensionVO, actionResourceType, cmdParameters); + } + throw new CloudRuntimeException(String.format("Custom action for resource type %s is not supported", + actionResourceType.name())); + } + + private CustomActionResultResponse runVirtualMachineCustomAction(VirtualMachine virtualMachine, + ExtensionCustomActionVO customActionVO, ExtensionVO extensionVO, + ExtensionCustomAction.ResourceType actionResourceType, + Map cmdParameters) { + + String error = "Internal error running action on virtual machine"; + + if (!Hypervisor.HypervisorType.External.equals(virtualMachine.getHypervisorType())) { + logger.error("Invalid {} specified as VM resource for running {}", virtualMachine, customActionVO); + throw new InvalidParameterValueException(error); } + final String actionName = customActionVO.getName(); + RunCustomActionCommand runCustomActionCommand = new RunCustomActionCommand(actionName); + VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(virtualMachine); + VirtualMachineTO virtualMachineTO = virtualMachineManager.toVmTO(vmProfile); + runCustomActionCommand.setVmTO(virtualMachineTO); + Pair clusterAndHostId = virtualMachineManager.findClusterAndHostIdForVm(virtualMachine, false); + + Long clusterId = clusterAndHostId.first(); + Long hostId = clusterAndHostId.second(); if (clusterId == null || hostId == null) { logger.error( "Unable to find cluster or host with the specified resource - cluster ID: {}, host ID: {}", @@ -1404,7 +1922,7 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { Map result = new HashMap<>(); response.setSuccess(false); result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, - actionResourceType, entity)); + actionResourceType, virtualMachine)); Map> externalDetails = getExternalAccessDetails(allDetails.first(), hostId, extensionResource); Map vmExternalDetails = null; @@ -1419,7 +1937,7 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { runCustomActionCommand.setWait(customActionVO.getTimeout()); try { logger.info("Running custom action: {} with {} parameters", actionName, - (parameters != null ? parameters.keySet().size() : 0)); + (parameters != null ? parameters.size() : 0)); Answer answer = agentMgr.send(hostId, runCustomActionCommand); if (!(answer instanceof RunCustomActionAnswer)) { logger.error("Unexpected answer [{}] received for {}", answer.getClass().getSimpleName(), @@ -1429,7 +1947,7 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { RunCustomActionAnswer customActionAnswer = (RunCustomActionAnswer) answer; response.setSuccess(answer.getResult()); result.put(ApiConstants.MESSAGE, getActionMessage(answer.getResult(), customActionVO, extensionVO, - actionResourceType, entity)); + actionResourceType, virtualMachine)); result.put(ApiConstants.DETAILS, customActionAnswer.getDetails()); } } catch (AgentUnavailableException e) { @@ -1445,6 +1963,183 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { return response; } + /** + * Executes a custom action for a Network resource by delegating to an + * available {@link NetworkCustomActionProvider} (e.g. NetworkExtensionElement). + * This path does NOT go through the agent framework. + */ + protected CustomActionResultResponse runNetworkCustomAction(Network network, + ExtensionCustomActionVO customActionVO, ExtensionVO extensionVO, + ExtensionCustomAction.ResourceType actionResourceType, + Map cmdParameters) { + + final String actionName = customActionVO.getName(); + CustomActionResultResponse response = new CustomActionResultResponse(); + response.setId(customActionVO.getUuid()); + response.setName(actionName); + response.setObjectName("customactionresult"); + Map result = new HashMap<>(); + response.setSuccess(false); + result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, actionResourceType, network)); + + // Resolve action parameters + List actionParameters = null; + Pair, Map> allDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customActionVO.getId()); + if (allDetails.second().containsKey(ApiConstants.PARAMETERS)) { + actionParameters = ExtensionCustomAction.Parameter.toListFromJson( + allDetails.second().get(ApiConstants.PARAMETERS)); + } + Map parameters = null; + if (CollectionUtils.isNotEmpty(actionParameters)) { + parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters); + } + + // Find the provider name for this network + String providerName = networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), Service.CustomAction); + if (StringUtils.isBlank(providerName)) { + logger.error("No network service provider found for network {}", network); + result.put(ApiConstants.DETAILS, "No network service provider found for this network"); + response.setResult(result); + return response; + } + + // Check if provider name matches the extension name + if (!providerName.equals(extensionVO.getName())) { + logger.error("Provider name '{}' for network {} does not match extension name '{}'", + providerName, network, extensionVO); + result.put(ApiConstants.DETAILS, "Network service provider '" + providerName + + "' does not match extension '" + extensionVO.getName() + "'"); + response.setResult(result); + return response; + } + + // Get the network element implementing that provider + NetworkElement element = networkModel.getElementImplementingProvider(providerName); + if (element == null) { + logger.error("No NetworkElement found implementing provider '{}' for network {}", providerName, network); + result.put(ApiConstants.DETAILS, "No network element found for provider: " + providerName); + response.setResult(result); + return response; + } + + // The element must implement NetworkCustomActionProvider + if (!(element instanceof NetworkCustomActionProvider)) { + logger.error("Network element '{}' for provider '{}' does not support custom actions", + element.getClass().getSimpleName(), providerName); + result.put(ApiConstants.DETAILS, "Provider '" + providerName + "' does not support custom actions"); + response.setResult(result); + return response; + } + + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + try { + if (!provider.canHandleCustomAction(network)) { + throw new CloudRuntimeException("Provider '" + providerName + "' cannot handle custom action for this network"); + } + logger.info("Running network custom action '{}' on network {} via {} (provider: {})", + actionName, network, element.getClass().getSimpleName(), providerName); + String output = provider.runCustomAction(network, actionName, parameters); + boolean success = output != null; + response.setSuccess(success); + result.put(ApiConstants.MESSAGE, getActionMessage(success, customActionVO, extensionVO, actionResourceType, network)); + result.put(ApiConstants.DETAILS, success ? output : "Action failed — check management server logs for details"); + } catch (Exception e) { + logger.error("Network custom action '{}' threw exception: {}", actionName, e.getMessage(), e); + result.put(ApiConstants.DETAILS, "Action failed: " + e.getMessage()); + } + response.setResult(result); + return response; + } + + /** + * Executes a custom action for a VPC resource by finding the VPC's + * extension provider and dispatching directly to it (no tier network lookup). + */ + protected CustomActionResultResponse runVpcCustomAction(Vpc vpc, + ExtensionCustomActionVO customActionVO, ExtensionVO extensionVO, + ExtensionCustomAction.ResourceType actionResourceType, + Map cmdParameters) { + + final String actionName = customActionVO.getName(); + CustomActionResultResponse response = new CustomActionResultResponse(); + response.setId(customActionVO.getUuid()); + response.setName(actionName); + response.setObjectName("customactionresult"); + Map result = new HashMap<>(); + response.setSuccess(false); + result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, actionResourceType, vpc)); + + // Resolve action parameters + List actionParameters = null; + Pair, Map> allDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customActionVO.getId()); + if (allDetails.second().containsKey(ApiConstants.PARAMETERS)) { + actionParameters = ExtensionCustomAction.Parameter.toListFromJson( + allDetails.second().get(ApiConstants.PARAMETERS)); + } + Map parameters = null; + if (CollectionUtils.isNotEmpty(actionParameters)) { + parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters); + } + + // Find the provider name for this VPC + String providerName = vpcServiceMapDao.getProviderForServiceInVpc(vpc.getId(), Service.CustomAction); + if (StringUtils.isBlank(providerName)) { + logger.error("No VPC service provider found for VPC {}", vpc.getId()); + result.put(ApiConstants.DETAILS, "No VPC service provider found for this VPC"); + response.setResult(result); + return response; + } + + // Check if provider name matches the extension name + if (!providerName.equals(extensionVO.getName())) { + logger.error("Provider name '{}' for vpc {} does not match extension name '{}'", + providerName, vpc.getId(), extensionVO.getName()); + result.put(ApiConstants.DETAILS, "Network service provider '" + providerName + + "' does not match extension '" + extensionVO.getName() + "'"); + response.setResult(result); + return response; + } + + // Get the network element implementing that provider + NetworkElement element = networkModel.getElementImplementingProvider(providerName); + if (element == null) { + logger.error("No NetworkElement found implementing provider '{}' for VPC {}", providerName, vpc); + result.put(ApiConstants.DETAILS, "No network element found for provider: " + providerName); + response.setResult(result); + return response; + } + + // The element must implement NetworkCustomActionProvider + if (!(element instanceof NetworkCustomActionProvider)) { + logger.error("Network element '{}' for provider '{}' does not support VPC custom actions", + element.getClass().getSimpleName(), providerName); + result.put(ApiConstants.DETAILS, "Provider '" + providerName + "' does not support custom actions"); + response.setResult(result); + return response; + } + + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + try { + if (!provider.canHandleVpcCustomAction(vpc)) { + throw new CloudRuntimeException("Provider '" + providerName + "' cannot handle custom action for this VPC"); + } + logger.info("Running VPC custom action '{}' on VPC {} via {} (provider: {})", + actionName, vpc.getId(), element.getClass().getSimpleName(), providerName); + String output = provider.runCustomAction(vpc, actionName, parameters); + boolean success = output != null; + response.setSuccess(success); + result.put(ApiConstants.MESSAGE, getActionMessage(success, customActionVO, extensionVO, actionResourceType, vpc)); + result.put(ApiConstants.DETAILS, success ? output : "Action failed — check management server logs for details"); + } catch (Exception e) { + logger.error("VPC custom action '{}' threw exception: {}", actionName, e.getMessage(), e); + result.put(ApiConstants.DETAILS, "Action failed: " + e.getMessage()); + } + response.setResult(result); + return response; + } + @Override public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction) { ExtensionCustomActionResponse response = new ExtensionCustomActionResponse(customAction.getUuid(), @@ -1554,11 +2249,8 @@ public void updateExtensionResourceMapDetails(long extensionResourceMapId, Map detailsList = new ArrayList<>(); - for (Map.Entry entry : details.entrySet()) { - detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(), - entry.getValue())); - } + List detailsList = + buildExtensionResourceDetailsArray(extensionResourceMapId, details); extensionResourceMapDetailsDao.saveDetails(detailsList); } @@ -1596,6 +2288,11 @@ public Extension getExtension(long id) { return extensionDao.findById(id); } + @Override + public Extension getExtensionByNameAndType(String name, Extension.Type type) { + return extensionDao.findByNameAndType(name, type); + } + @Override public Extension getExtensionForCluster(long clusterId) { Long extensionId = getExtensionIdForCluster(clusterId); @@ -1605,6 +2302,24 @@ public Extension getExtensionForCluster(long clusterId) { return extensionDao.findById(extensionId); } + @Override + public List getExtensionReservedResourceDetails(long extensionId) { + ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(extensionId, + ApiConstants.RESERVED_RESOURCE_DETAILS); + List reservedDetails = new ArrayList<>(); + if (detailsVO != null && StringUtils.isNotBlank(detailsVO.getValue())) { + String[] parts = detailsVO.getValue().split(","); + for (String part : parts) { + String trimmedPart = part.trim(); + if (StringUtils.isNotBlank(trimmedPart)) { + reservedDetails.add(trimmedPart); + } + } + } + addInbuiltExtensionReservedResourceDetails(extensionId, reservedDetails); + return reservedDetails; + } + @Override public boolean start() { long pathStateCheckInterval = PathStateCheckInterval.value(); @@ -1642,6 +2357,7 @@ public List> getCommands() { cmds.add(UpdateExtensionCmd.class); cmds.add(RegisterExtensionCmd.class); cmds.add(UnregisterExtensionCmd.class); + cmds.add(UpdateRegisteredExtensionCmd.class); return cmds; } @@ -1693,4 +2409,95 @@ protected void runInContext() { } } } + + @Override + public String getExtensionScriptPath(Extension extension) { + if (extension == null) { + return null; + } + return externalProvisioner.getExtensionPath(extension.getRelativePath()); + } + + @Override + public Extension getExtensionForPhysicalNetworkAndProvider(long physicalNetworkId, String providerName) { + if (StringUtils.isBlank(providerName)) { + return null; + } + ExtensionVO ext = extensionDao.findByName(providerName); + if (ext == null) { + return null; + } + + ExtensionResourceMapVO map = extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType( + ext.getId(), physicalNetworkId, ExtensionResourceMap.ResourceType.PhysicalNetwork); + if (map != null) { + return ext; + } + return null; + } + + @Override + public Map getAllResourceMapDetailsForExtensionOnPhysicalNetwork(long physicalNetworkId, long extensionId) { + ExtensionResourceMapVO map = extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType( + extensionId, physicalNetworkId, ExtensionResourceMap.ResourceType.PhysicalNetwork); + if (map == null) { + return new HashMap<>(); + } + + Map details = extensionResourceMapDetailsDao.listDetailsKeyPairs(map.getId()); + return details != null ? details : new HashMap<>(); + } + + @Override + public boolean isNetworkExtensionProvider(String providerName) { + if (StringUtils.isBlank(providerName)) { + return false; + } + return extensionDao.findByNameAndType(providerName, Extension.Type.NetworkOrchestrator) != null; + } + + @Override + public boolean usesNetworkExtensionIsolation(String providerName) { + Extension extension = extensionDao.findByName(providerName); + if (extension == null) { + return false; + } + Map extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId()); + if (MapUtils.isEmpty(extDetails)) { + return false; + } + return NETWORK_EXTENSION_ISOLATION_METHOD.equalsIgnoreCase(extDetails.get(NETWORK_ISOLATION_METHOD_DETAIL_KEY)); + } + + @Override + public List listExtensionsByType(Extension.Type type) { + if (type == null) { + return new ArrayList<>(); + } + List extensions = extensionDao.listByType(type); + if (CollectionUtils.isEmpty(extensions)) { + return new ArrayList<>(); + } + return new ArrayList<>(extensions); + } + + @Override + public Map> getNetworkCapabilitiesForProvider(Long physicalNetworkId, + String providerName) { + if (StringUtils.isBlank(providerName)) { + return new HashMap<>(); + } + Extension extension = null; + if (physicalNetworkId != null) { + extension = getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName); + } else { + // Search across all physical networks + extension = extensionDao.findByNameAndType(providerName, Extension.Type.NetworkOrchestrator); + } + if (extension == null) { + return new HashMap<>(); + } + Map extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId()); + return buildCapabilitiesFromDetailKeys(extDetails); + } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java new file mode 100644 index 000000000000..e98848c01f7d --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java @@ -0,0 +1,2618 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.framework.extensions.network; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import com.cloud.agent.api.to.LoadBalancerTO; +import com.cloud.dc.DataCenter; +import com.cloud.dc.Vlan; +import com.cloud.dc.VlanVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.VlanDao; +import com.cloud.deploy.DeployDestination; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.dao.HostDao; +import com.cloud.network.IpAddressManager; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.Network.Capability; +import com.cloud.network.Network.Provider; +import com.cloud.network.Network.Service; +import com.cloud.network.NetworkModel; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkDetailVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.PhysicalNetworkServiceProvider; +import com.cloud.network.PublicIpAddress; +import com.cloud.network.addr.PublicIp; +import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDetailsDao; +import com.cloud.network.dao.NetworkServiceMapDao; +import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.network.element.AggregatedCommandExecutor; +import com.cloud.network.element.DhcpServiceProvider; +import com.cloud.network.element.DnsServiceProvider; +import com.cloud.network.element.FirewallServiceProvider; +import com.cloud.network.element.IpDeployer; +import com.cloud.network.element.LoadBalancingServiceProvider; +import com.cloud.network.element.NetworkACLServiceProvider; +import com.cloud.network.element.NetworkElement; +import com.cloud.network.element.PortForwardingServiceProvider; +import com.cloud.network.element.SourceNatServiceProvider; +import com.cloud.network.element.StaticNatServiceProvider; +import com.cloud.network.element.UserDataServiceProvider; +import com.cloud.network.element.VpcProvider; +import com.cloud.network.vpc.NetworkACLItem; +import com.cloud.network.vpc.PrivateGateway; +import com.cloud.network.vpc.StaticRouteProfile; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.lb.LoadBalancingRule; +import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.rules.StaticNat; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.dao.GuestOSCategoryDao; +import com.cloud.storage.dao.GuestOSDao; +import com.cloud.user.AccountService; +import com.cloud.uservm.UserVm; +import com.cloud.offering.NetworkOffering; +import com.cloud.user.Account; +import com.cloud.utils.Pair; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.Nic; +import com.cloud.vm.NicProfile; +import com.cloud.vm.NicVO; +import com.cloud.vm.ReservationContext; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionHelper; +import org.apache.cloudstack.extension.NetworkCustomActionProvider; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.stream.Collectors; + + +public class NetworkExtensionElement extends AdapterBase implements + NetworkElement, SourceNatServiceProvider, StaticNatServiceProvider, + PortForwardingServiceProvider, IpDeployer, NetworkCustomActionProvider, + DhcpServiceProvider, DnsServiceProvider, FirewallServiceProvider, + UserDataServiceProvider, LoadBalancingServiceProvider, + VpcProvider, NetworkACLServiceProvider, AggregatedCommandExecutor { + + private static final Map> DEFAULT_CAPABILITIES = new HashMap<>(); + + + /** + * When non-null, restricts all operations to the extension whose name + * matches this provider name. + */ + private String providerName; + + @Inject + private NetworkModel networkModel; + @Inject + private NetworkServiceMapDao ntwkSrvcDao; + @Inject + private ExtensionHelper extensionHelper; + @Inject + private NetworkDetailsDao networkDetailsDao; + @Inject + private IpAddressManager ipAddressManager; + @Inject + private NetworkOrchestrationService networkManager; + @Inject + private AccountService accountService; + @Inject + private PhysicalNetworkDao physicalNetworkDao; + @Inject + private ExtensionDetailsDao extensionDetailsDao; + @Inject + private NetworkDao networkDao; + @Inject + private DataCenterDao dataCenterDao; + @Inject + private VlanDao vlanDao; + @Inject + private GuestOSCategoryDao guestOSCategoryDao; + @Inject + private GuestOSDao guestOSDao; + @Inject + private HostDao hostDao; + @Inject + private VMInstanceDetailsDao vmInstanceDetailsDao; + @Inject + private UserVmDao userVmDao; + @Inject + private NicDao nicDao; + @Inject + private NetworkOfferingDao networkOfferingDao; + @Inject + private ServiceOfferingDao serviceOfferingDao; + @Inject + private FirewallRulesDao firewallRulesDao; + @Inject + private IPAddressDao ipAddressDao; + @Inject + private VpcDao vpcDao; + @Inject + private VpcDetailsDao vpcDetailsDao; + + // ---- Script argument names ---- + + public static final String ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS = "physical-network-extension-details"; + public static final String ARG_NETWORK_EXTENSION_DETAILS = "network-extension-details"; + public static final String ARG_PAYLOAD = "payload"; + public static final String ARG_ACTION_PARAMS = "action-params"; + + public static final int DEFAULT_SCRIPT_TIMEOUT_SECONDS = 60; + + public static final int EXIT_CODE_SUCCESS = 0; + public static final int EXIT_CODE_FAILURE = -1; + + // ---- Script command names ---- + + public static final String CMD_IMPLEMENT_NETWORK = "implement-network"; + public static final String CMD_SHUTDOWN_NETWORK = "shutdown-network"; + public static final String CMD_DESTROY_NETWORK = "destroy-network"; + public static final String CMD_ENSURE_NETWORK_DEVICE = "ensure-network-device"; + public static final String CMD_ASSIGN_IP = "assign-ip"; + public static final String CMD_RELEASE_IP = "release-ip"; + public static final String CMD_ADD_STATIC_NAT = "add-static-nat"; + public static final String CMD_DELETE_STATIC_NAT = "delete-static-nat"; + public static final String CMD_ADD_PORT_FORWARD = "add-port-forward"; + public static final String CMD_DELETE_PORT_FORWARD = "delete-port-forward"; + public static final String CMD_ADD_DHCP_ENTRY = "add-dhcp-entry"; + public static final String CMD_CONFIG_DHCP_SUBNET = "config-dhcp-subnet"; + public static final String CMD_REMOVE_DHCP_SUBNET = "remove-dhcp-subnet"; + public static final String CMD_REMOVE_DHCP_ENTRY = "remove-dhcp-entry"; + public static final String CMD_ADD_DNS_ENTRY = "add-dns-entry"; + public static final String CMD_REMOVE_DNS_ENTRY = "remove-dns-entry"; + public static final String CMD_CONFIG_DNS_SUBNET = "config-dns-subnet"; + public static final String CMD_REMOVE_DNS_SUBNET = "remove-dns-subnet"; + public static final String CMD_SAVE_VM_DATA = "save-vm-data"; + public static final String CMD_SAVE_PASSWORD = "save-password"; + public static final String CMD_SAVE_USERDATA = "save-userdata"; + public static final String CMD_SAVE_SSHKEY = "save-sshkey"; + public static final String CMD_SAVE_HYPERVISOR_HOSTNAME = "save-hypervisor-hostname"; + public static final String CMD_APPLY_LB_RULES = "apply-lb-rules"; + public static final String CMD_APPLY_FW_RULES = "apply-fw-rules"; + public static final String CMD_RESTORE_NETWORK = "restore-network"; + public static final String CMD_IMPLEMENT_VPC = "implement-vpc"; + public static final String CMD_SHUTDOWN_VPC = "shutdown-vpc"; + public static final String CMD_UPDATE_VPC_SOURCE_NAT_IP = "update-vpc-source-nat-ip"; + public static final String CMD_APPLY_NETWORK_ACL = "apply-network-acl"; + public static final String CMD_CUSTOM_ACTION = "custom-action"; + public static final String CMD_PREPARE_NIC = "prepare-nic"; + public static final String CMD_RELEASE_NIC = "release-nic"; + + // ---- Network detail key ---- + + /** + * Key used to persist the per-network JSON blob in {@code network_details}. + * The blob is produced by the network-extension.sh's {@code ensure-network-device} + * command and may contain any fields the script needs (e.g. selected host, + * namespace name, VRF ID, …). + */ + public static final String NETWORK_DETAIL_EXTENSION_DETAILS = "extension.details"; + + /** + * Extension detail key that controls whether {@link #ensureExtensionIp} allocates + * a placeholder NIC/IP. + * + *

Applies to any network whose DHCP/DNS/UserData service is provided by this + * extension without SourceNat or Gateway — i.e. shared networks and + * isolated networks that omit SourceNat. Set the value to {@code "false"} on the + * Extension object (via {@code createExtension} or {@code updateExtension}) when + * the extension handles its own data-plane addressing and does not need CloudStack + * to reserve a real IP from the subnet pool (e.g. OVN, which programs port + * bindings in-fabric). Omitting the detail or setting any value other than + * {@code "false"} preserves the original behaviour (placeholder IP is allocated).

+ */ + public static final String NETWORK_ALLOCATE_EXTENSION_IP = "network.allocate.extension.ip"; + + public String getProviderName() { + return providerName; + } + + /** + * Returns a new {@link NetworkExtensionElement} scoped to {@code providerName}, + * sharing all injected dependencies with this instance. + */ + public NetworkExtensionElement withProviderName(String providerName) { + NetworkExtensionElement copy = new NetworkExtensionElement(); + copy.networkModel = this.networkModel; + copy.ntwkSrvcDao = this.ntwkSrvcDao; + copy.extensionHelper = this.extensionHelper; + copy.networkDetailsDao = this.networkDetailsDao; + copy.ipAddressManager = this.ipAddressManager; + copy.physicalNetworkDao = this.physicalNetworkDao; + copy.extensionDetailsDao = this.extensionDetailsDao; + copy.networkDao = this.networkDao; + copy.dataCenterDao = this.dataCenterDao; + copy.vlanDao = this.vlanDao; + copy.guestOSCategoryDao = this.guestOSCategoryDao; + copy.guestOSDao = this.guestOSDao; + copy.hostDao = this.hostDao; + copy.vmInstanceDetailsDao = this.vmInstanceDetailsDao; + copy.userVmDao = this.userVmDao; + copy.nicDao = this.nicDao; + copy.networkManager = this.networkManager; + copy.networkOfferingDao = this.networkOfferingDao; + copy.serviceOfferingDao = this.serviceOfferingDao; + copy.accountService = this.accountService; + copy.firewallRulesDao = this.firewallRulesDao; + copy.ipAddressDao = this.ipAddressDao; + copy.vpcDao = this.vpcDao; + copy.vpcDetailsDao = this.vpcDetailsDao; + copy.providerName = providerName; + + logger.debug("NetworkExtensionElement initialised with provider name '{}'", providerName); + return copy; + } + + // ---- Capabilities ---- + + @Override + public Map> getCapabilities() { + try { + // If this element is scoped to a provider name, prefer capabilities stored + // in the extension's "network.service.capabilities" detail. The ExtensionHelper + // exposes a helper that loads the Service→Capability map from the DB. + if (StringUtils.isNotBlank(providerName)) { + Map> caps = extensionHelper.getNetworkCapabilitiesForProvider(null, providerName); + if (MapUtils.isNotEmpty(caps)) { + return caps; + } + } + } catch (Exception e) { + logger.warn("Failed to load network service capabilities from extension details for provider '{}': {}", providerName, e.getMessage()); + } + + return DEFAULT_CAPABILITIES; + } + + @Override + public Provider getProvider() { + if (providerName != null) { + return Provider.createTransientProvider(providerName); + } + return Provider.NetworkExtension; + } + + // ---- Extension / provider resolution ---- + + protected Extension resolveExtension(Network network) { + Long physicalNetworkId = network.getPhysicalNetworkId(); + if (physicalNetworkId == null) { + logger.warn("Network {} has no physical network — cannot resolve extension", network); + return null; + } + Extension ext = extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName); + if (ext != null) { + return ext; + } + logger.warn("No extension found for scoped provider '{}' on physical network {}", providerName, physicalNetworkId); + return null; + } + + protected boolean canHandle(Network network, Service service) { + Long physicalNetworkId = network.getPhysicalNetworkId(); + if (physicalNetworkId == null) { + return false; + } + boolean hasExt = extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, providerName) != null; + if (!hasExt) { + return false; + } + if (service == null) { + return true; + } + List sp = ntwkSrvcDao.getProvidersForServiceInNetwork(network.getId(), service); + return sp != null && sp.stream() + .anyMatch(p -> extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, p) != null); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + return true; + } + + // ---- NetworkElement lifecycle ---- + + @Override + public boolean implement(Network network, NetworkOffering offering, DeployDestination dest, + ReservationContext context) throws ConcurrentOperationException, + ResourceUnavailableException, InsufficientCapacityException { + if (!canHandle(network, null)) { + return false; + } + logger.info("Implementing network extension for network {} (VLAN {})", network, network.getBroadcastUri()); + + // Step 1: Ensure a network device is selected and its details stored. + ensureExtensionDetails(network); + + // Step 2: Create the network on the device. + JsonObject implementPayload = new JsonObject(); + addNetworkToPayload(implementPayload, network); + addExtensionIpToPayload(implementPayload, network); + + Pair result = executeScriptAndReturnOutput(network, CMD_IMPLEMENT_NETWORK, implementPayload); + + if (result.first() != EXIT_CODE_SUCCESS) { + return false; + } + + // Update the network properties from the output + applyNetworkUpdateFromScriptOutput(network, result.second()); + + // Step 3: Configure source NAT for both VPC and non-VPC networks for + // compatibility (other network-element providers may also implement VPC tiers). + // When this is a VPC tier, the script's assign-ip does nothing for source-nat + // because VPC source NAT is managed at the VPC level by implementVpc(). + if (canHandle(network, Service.SourceNat)) { + try { + if (network.getVpcId() == null) { + // Isolated network: apply the network's own source NAT IP. + Account owner = accountService.getAccount(network.getAccountId()); + PublicIpAddress existingIp = networkModel.getSourceNatIpAddressForGuestNetwork(owner, network); + if (existingIp != null) { + applyIps(network, List.of(existingIp), Set.of(Service.SourceNat)); + } + } else { + // VPC tier: apply the VPC-level source NAT IP (script is a no-op for SNAT). + final PublicIpAddress vpcSourceNatIp = getVpcSourceNatIp(network.getVpcId()); + if (vpcSourceNatIp != null) { + applyIps(network, List.of(vpcSourceNatIp), Set.of(Service.SourceNat)); + } + } + } catch (Exception e) { + logger.warn("Failed to configure source NAT IP for network {}: {}", network, e.getMessage()); + } + } + + return true; + } + + @Override + public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + + if (!canHandle(network, null)) { + return false; + } + + if (!networkModel.isProviderEnabledInPhysicalNetwork(networkModel.getPhysicalNetworkId(network), getProvider().getName())) { + return false; + } + + // Sync nic with network + applyNicUpdateFromNetwork(network, nic.getId()); + + // Build payload for prepare-nic script command + try { + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + if (vm != null) { + payload.addProperty("hostname", safeStr(vm.getHostName())); + } + addExtensionIpToPayload(payload, network); + + logger.debug("Preparing NIC via extension script: network={} nicMac={} nicIp={}", network, nic.getMacAddress(), nic.getIPv4Address()); + + return executeScript(network, CMD_PREPARE_NIC, payload); + } catch (Exception e) { + logger.warn("prepare: failed to prepare NIC for network {}: {}", network, e.getMessage()); + return false; + } + } + + private void applyNicUpdateFromNetwork(Network network, Long nicId) { + if (nicId == null) { + return; + } + NicVO nicVo = nicDao.findById(nicId); + if (nicVo == null) { + return; + } + if (network.getBroadcastUri() != null) { + nicVo.setBroadcastUri(network.getBroadcastUri()); + nicVo.setIsolationUri(network.getBroadcastUri()); + nicDao.update(nicId, nicVo); + } + } + + @Override + public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm, + ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { + if (!canHandle(network, null)) { + return true; + } + + try { + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + addExtensionIpToPayload(payload, network); + + logger.debug("Releasing NIC via extension script: network={} nicMac={} nicIp={}", network, nic != null ? nic.getMacAddress() : null, nic != null ? nic.getIPv4Address() : null); + + return executeScript(network, CMD_RELEASE_NIC, payload); + } catch (Exception e) { + logger.warn("release: failed to release NIC for network {}: {}", network, e.getMessage()); + return false; + } + } + + @Override + public boolean shutdown(Network network, ReservationContext context, boolean cleanup) + throws ConcurrentOperationException, ResourceUnavailableException { + logger.info("Shutting down network extension for network {}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + boolean result = executeScript(network, CMD_SHUTDOWN_NETWORK, payload); + if (result) { + // Remove stored per-network extension details (e.g. namespace). For VPC-backed networks + // the namespace is named cs-vpc-, stored in the extension details. Removing the + // stored details ensures the namespace is deleted/forgotten on shutdown. + try { + networkDetailsDao.removeDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS); + } catch (Exception e) { + logger.warn("Failed to remove network extension details for network {}: {}", network, e.getMessage()); + } + } + return result; + } + + @Override + public boolean destroy(Network network, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException { + logger.info("Destroying network extension for network {}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + // For both isolated and VPC tier networks, use destroy-network. + // For VPC tiers, the script preserves the shared namespace; + // the VPC namespace is removed only when shutdownVpc() calls shutdown-vpc. + boolean result = executeScript(network, CMD_DESTROY_NETWORK, payload); + if (result) { + cleanupPlaceholderNicIp(network, context); + networkDetailsDao.removeDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS); + } + return result; + } + + /** + * Releases placeholder NIC IPs allocated for DHCP/DNS/UserData extension traffic, + * then removes the placeholder NIC record(s) for this network. + */ + protected void cleanupPlaceholderNicIp(Network network, ReservationContext context) { + List placeholderNics = nicDao.listPlaceholderNicsByNetworkIdAndVmType( + network.getId(), VirtualMachine.Type.DomainRouter); + if (CollectionUtils.isEmpty(placeholderNics)) { + return; + } + + long userId = accountService.getSystemUser().getId(); + Account caller = accountService.getSystemAccount(); + if (context != null && context.getAccount() != null) { + caller = context.getAccount(); + } + + for (NicVO placeholderNic : placeholderNics) { + try { + String ip = placeholderNic.getIPv4Address(); + if (StringUtils.isNotBlank(ip)) { + logger.debug("Cleaning up PlaceHolder IP {} on network {}", ip, network); + IPAddressVO ipAddress = ipAddressDao.findByIpAndSourceNetworkId(network.getId(), ip); + if (ipAddress != null) { + if (Network.GuestType.Shared.equals(network.getGuestType())) { + ipAddressManager.disassociatePublicIpAddress(ipAddress, userId, caller); + } else { + ipAddressManager.markIpAsUnavailable(ipAddress.getId()); + ipAddressDao.unassignIpAddress(ipAddress.getId()); + } + } + } + } catch (Exception e) { + logger.warn("Failed to release placeholder IP for network {} and nic {}: {}", + network, placeholderNic, e.getMessage()); + } + + try { + nicDao.remove(placeholderNic.getId()); + } catch (Exception e) { + logger.warn("Failed to remove placeholder nic {} for network {}: {}", + placeholderNic, network, e.getMessage()); + } + } + } + + @Override + public boolean isReady(PhysicalNetworkServiceProvider provider) { + return true; + } + + @Override + public boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException { + return true; + } + + @Override + public boolean canEnableIndividualServices() { + return true; + } + + @Override + public boolean verifyServicesCombination(Set services) { + return true; + } + + // ---- ensure-network-device ---- + + /** + * Calls the network-extension.sh script with {@code ensure-network-device} before + * the first network operation. The script verifies the previously selected + * device is reachable (using the {@code hosts} list in the physical-network + * extension details) and performs failover if needed. The returned JSON is + * persisted in {@code network_details} and forwarded to all subsequent calls + * via {@value #ARG_NETWORK_EXTENSION_DETAILS}. + * + *

For VPC tier networks the extension details are inherited from the VPC-level + * details (stored in {@code vpc_details}) so all tiers in the same VPC share + * the same host/namespace binding. The script's {@code ensure-network-device} + * is only called at the VPC level (see {@link #ensureExtensionDetails(Vpc)}).

+ */ + protected void ensureExtensionDetails(Network network) { + if (network.getVpcId() != null) { + Vpc vpc = vpcDao.findById(network.getVpcId()); + ensureExtensionDetails(vpc); + return; + } + + // Isolated network: run ensure-network-device to select / validate the host. + Map stored = networkDetailsDao.listDetailsKeyPairs(network.getId()); + String currentDetails = stored != null + ? stored.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + + logger.info("Ensuring network device for network {} (current={})", network, currentDetails); + + Extension extension = resolveExtension(network); + File scriptFile = resolveScriptFile(network, extension); + + JsonObject argsPayload = new JsonObject(); + addNetworkToPayload(argsPayload, network); + argsPayload.addProperty("current_details", currentDetails); + JsonObject payload = buildNetworkScriptPayload(network, argsPayload, extension); + + try { + Pair result = executeScriptWithFilePayload(scriptFile, + CMD_ENSURE_NETWORK_DEVICE, payload, "Network extension"); + String output = result.second() != null ? result.second() : ""; + + if (result.first() != EXIT_CODE_SUCCESS) { + logger.warn("ensure-network-device exited {} for network {} — keeping current details", + -1, network); + if ("{}".equals(currentDetails)) { + networkDetailsDao.addDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); + } + return; + } + if (output.isEmpty()) { + output = "{}".equals(currentDetails) ? "{}" : currentDetails; + } + if (!output.equals(currentDetails)) { + logger.info("Network device updated for network {}: {}", network, output); + networkDetailsDao.addDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, output, false); + } else { + logger.debug("Network device unchanged for network {}: {}", network, output); + } + } catch (Exception e) { + logger.warn("Failed ensure-network-device for network {}: {}", network, e.getMessage()); + if ("{}".equals(currentDetails)) { + networkDetailsDao.addDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); + } + } + } + + /* + * If the network supports DHCP/DNS/UserData but not SourceNat/Gateway, + * an additional IP is needed on the external network to host these services. + * This method ensures that IP is allocated and configured on the external network and returns its address. + */ + protected String ensureExtensionIp(Network network) { + if (networkModel.isAnyServiceSupportedInNetwork(network.getId(), this.getProvider(), + Service.SourceNat, Service.Gateway)) { + // Gateway or Source NAT will be configured on the external network + return network.getGateway(); + } + + if (networkModel.isAnyServiceSupportedInNetwork(network.getId(), this.getProvider(), + Service.Dhcp, Service.Dns, Service.UserData)) { + // Some extensions (e.g. OVN) handle shared-network addressing in the + // data plane and do not need a CloudStack-allocated placeholder IP. + Extension ext = extensionHelper.getExtensionByNameAndType(providerName, Extension.Type.NetworkOrchestrator); + if (ext != null) { + ExtensionDetailsVO allocateDetail = + extensionDetailsDao.findDetail(ext.getId(), NETWORK_ALLOCATE_EXTENSION_IP); + if (allocateDetail != null && "false".equalsIgnoreCase(allocateDetail.getValue())) { + logger.debug("Skipping placeholder IP for network {} — {} detail is false", + network.getId(), NETWORK_ALLOCATE_EXTENSION_IP); + return null; + } + try { + // An extra IP will be allocated and configured on the external network + Nic placeholderNic = networkModel.getPlaceholderNicForRouter(network, null); + if (placeholderNic == null) { + NetworkDetailVO routerIpDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.ROUTER_IP); + String routerIp = routerIpDetail != null ? routerIpDetail.getValue() : null; + Account account = accountService.getAccount(network.getAccountId()); + String extensionIp = Network.GuestType.Shared.equals(network.getGuestType()) ? + ipAddressManager.assignPublicIpAddress(network.getDataCenterId(), null, account, Vlan.VlanType.DirectAttached, network.getId(), routerIp, false, false).getAddress().toString() : + ipAddressManager.acquireGuestIpAddress(network, routerIp); + logger.debug("Saving placeholder nic with ip4 address {} for the network", extensionIp, network); + networkManager.savePlaceholderNic(network, extensionIp, null, VirtualMachine.Type.DomainRouter); + return extensionIp; + } + return placeholderNic.getIPv4Address(); + } catch (Exception e) { + logger.warn("Failed to acquire extension IP for network {}: {}", network, e.getMessage()); + } + } + } + return null; + } + + // ---- IpDeployer ---- + + @Override + public boolean applyIps(Network network, List ipAddress, Set services) + throws ResourceUnavailableException { + if (CollectionUtils.isEmpty(ipAddress)) { + return true; + } + logger.info("Applying {} IPs for network {}", ipAddress.size(), network); + + for (PublicIpAddress ip : ipAddress) { + boolean isSourceNat = ip.isSourceNat(); + boolean isRevoke = ip.getState() == IpAddress.State.Releasing; + String action = isRevoke ? CMD_RELEASE_IP : CMD_ASSIGN_IP; + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addPublicIpToPayload(payload, ip.getId(), isSourceNat); + + boolean result = executeScript(network, action, payload); + if (!result) { + throw new ResourceUnavailableException( + "Failed to " + action + " for IP " + ip.getAddress().addr(), + Network.class, network.getId()); + } + } + return true; + } + + // ---- StaticNatServiceProvider ---- + + @Override + public boolean applyStaticNats(Network config, List rules) + throws ResourceUnavailableException { + if (CollectionUtils.isEmpty(rules)) { + return true; + } + if (!canHandle(config, Service.StaticNat)) { + return false; + } + logger.info("Applying {} static NAT rules for network {}", rules.size(), config); + for (StaticNat rule : rules) { + String action = rule.isForRevoke() ? CMD_DELETE_STATIC_NAT : CMD_ADD_STATIC_NAT; + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, config); + addPublicIpToPayload(payload, rule.getSourceIpAddressId(), false); + payload.addProperty("private_ip", safeStr(rule.getDestIpAddress())); + boolean result = executeScript(config, action, payload); + if (!result) { + throw new ResourceUnavailableException("Failed to " + action + " for static NAT rule", + Network.class, config.getId()); + } + } + return true; + } + + // ---- PortForwardingServiceProvider ---- + + @Override + public boolean applyPFRules(Network network, List rules) + throws ResourceUnavailableException { + if (CollectionUtils.isEmpty(rules)) { + return true; + } + if (!canHandle(network, Service.PortForwarding)) { + return false; + } + logger.info("Applying {} port forwarding rules for network {}", rules.size(), network); + for (PortForwardingRule rule : rules) { + boolean isRevoke = rule.getState() == FirewallRule.State.Revoke; + String action = isRevoke ? CMD_DELETE_PORT_FORWARD : CMD_ADD_PORT_FORWARD; + String publicPort = PortForwardingServiceProvider.getPublicPortRange(rule); + String privatePort = PortForwardingServiceProvider.getPrivatePFPortRange(rule); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addPublicIpToPayload(payload, rule.getSourceIpAddressId(), false); + payload.addProperty("public_port", safeStr(publicPort)); + payload.addProperty("private_ip", safeStr(rule.getDestinationIpAddress() != null + ? rule.getDestinationIpAddress().addr() : null)); + payload.addProperty("private_port", safeStr(privatePort)); + payload.addProperty("protocol", safeStr(rule.getProtocol())); + boolean result = executeScript(network, action, payload); + if (!result) { + throw new ResourceUnavailableException("Failed to " + action + " for port forwarding rule", + Network.class, network.getId()); + } + } + return true; + } + + // ---- Script execution ---- + + protected boolean executeScript(Network network, String command, JsonObject argsPayload) { + return executeScriptAndReturnOutput(network, command, argsPayload).first() == 0; + } + + protected Pair executeScriptAndReturnOutput(Network network, String command, JsonObject argsPayload) { + ensureExtensionDetails(network); + Extension extension = resolveExtension(network); + JsonObject payload = buildNetworkScriptPayload(network, argsPayload, extension); + return executeScriptWithFilePayload(network, command, payload); + } + + private JsonObject parseJsonOutput(String outputStr) { + if (StringUtils.isBlank(outputStr)) { + return null; + } + try { + JsonElement parsed = JsonParser.parseString(outputStr); + if (!parsed.isJsonObject()) { + logger.debug("Ignoring non-object script output: {}", outputStr); + return null; + } + return parsed.getAsJsonObject(); + } catch (Exception e) { + logger.debug("Ignoring non-JSON script output: {}", outputStr); + return null; + } + } + + private String getJsonString(JsonObject jsonObject, String keyPath) { + if (jsonObject == null || StringUtils.isBlank(keyPath)) { + return null; + } + JsonElement value = jsonObject.has(keyPath) ? jsonObject.get(keyPath) : null; + if (value == null) { + JsonElement current = jsonObject; + String[] parts = keyPath.split("\\."); + for (String part : parts) { + if (current == null || !current.isJsonObject()) { + current = null; + break; + } + JsonObject currentObj = current.getAsJsonObject(); + if (!currentObj.has(part)) { + current = null; + break; + } + current = currentObj.get(part); + } + value = current; + } + if (value == null || value.isJsonNull()) { + return null; + } + return value.getAsString(); + } + + private void applyNetworkUpdateFromScriptOutput(Network network, String outputStr) { + JsonObject outputJson = parseJsonOutput(outputStr); + String networkBroadcastUri = getJsonString(outputJson, "network.broadcast_uri"); + String networkBroadcastDomainType = getJsonString(outputJson, "network.broadcast_domain_type"); + + if (ExtensionHelper.NETWORK_EXTENSION_GURU_NAME.equals(network.getGuruName()) && + (networkBroadcastUri == null || networkBroadcastDomainType == null)) { + throw new CloudRuntimeException(String.format( + "Script output is missing required network properties 'network.broadcast_uri' and 'network.broadcast_domain_type'" + + " for network %s: %s", network, outputStr)); + } + + try { + NetworkVO networkVo = networkDao.findById(network.getId()); + if (networkVo == null) { + return; + } + + boolean changed = false; + if (networkBroadcastDomainType != null) { + Networks.BroadcastDomainType domainType = EnumUtils.getEnumIgnoreCase(Networks.BroadcastDomainType.class, networkBroadcastDomainType); + if (domainType != null) { + networkVo.setBroadcastDomainType(domainType); + changed = true; + } else { + logger.warn("Ignoring unknown broadcast domain type '{}' for network {}", + networkBroadcastDomainType, network); + } + } + + if (networkBroadcastUri != null) { + networkVo.setBroadcastUri(URI.create(networkBroadcastUri)); + changed = true; + } + + if (changed) { + networkDao.update(networkVo.getId(), networkVo); + for (NicVO nicVO : nicDao.listByNetworkId(networkVo.getId())) { + applyNicUpdateFromNetwork(networkVo, nicVO.getId()); + } + } + } catch (Exception e) { + logger.warn("Failed to update network {} from script output: {}", network, e.getMessage()); + } + } + + protected Pair executeScriptWithFilePayload(Network network, String command, JsonObject payload) { + Extension extension = resolveExtension(network); + File scriptFile = resolveScriptFile(network, extension); + return executeScriptWithFilePayload(scriptFile, command, payload, "Network extension"); + } + + private Pair executeScriptWithFilePayload(File scriptFile, String command, + JsonObject payload, String logPrefix) { + File payloadFile = null; + try { + payloadFile = File.createTempFile("cs-extnet-" + command + "-", ".payload"); + String payloadJson = payload != null ? new Gson().toJson(payload) : "{}"; + logger.debug("Writing payload to payload file {}", payloadFile); + Files.writeString(payloadFile.toPath(), payloadJson, StandardCharsets.UTF_8); + + List cmdLine = new ArrayList<>(); + cmdLine.add(scriptFile.getAbsolutePath()); + cmdLine.add(command); + cmdLine.add(payloadFile.getAbsolutePath()); + cmdLine.add(String.valueOf(DEFAULT_SCRIPT_TIMEOUT_SECONDS)); + + logger.debug("Executing {} script: {}", logPrefix, String.join(" ", cmdLine)); + + ProcessBuilder processBuilder = new ProcessBuilder(cmdLine); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + byte[] output = process.getInputStream().readAllBytes(); + int exitCode = process.waitFor(); + + String outputStr = new String(output).trim(); + if (!outputStr.isEmpty()) { + logger.debug("Script output: {}", outputStr); + } + + if (exitCode != EXIT_CODE_SUCCESS) { + logger.error("{} script {} failed with exit code {}: {}", logPrefix, command, exitCode, outputStr); + return new Pair<>(exitCode, outputStr); + } + + JsonObject outputJson = parseJsonOutput(outputStr); + String status = outputJson != null ? getJsonString(outputJson, "status") : null; + if (StringUtils.isNotBlank(status) && !"success".equalsIgnoreCase(status)) { + logger.error("{} script {} returned non-success status '{}': {}", logPrefix, command, status, outputStr); + return new Pair<>(EXIT_CODE_FAILURE, outputStr); + } + + return new Pair<>(EXIT_CODE_SUCCESS, outputStr); + } catch (Exception e) { + throw new CloudRuntimeException( + String.format("Failed preparing payload file for command %s", command), e); + } finally { + if (payloadFile != null && payloadFile.exists() && !payloadFile.delete()) { + payloadFile.deleteOnExit(); + } + } + } + + private JsonObject buildNetworkScriptPayload(Network network, JsonObject argsPayload, Extension extension) { + JsonObject payload = new JsonObject(); + payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS, + buildPhysicalNetworkExtensionDetailsPayload(network.getPhysicalNetworkId(), extension)); + payload.add(ARG_NETWORK_EXTENSION_DETAILS, buildNetworkExtensionDetailsPayload(network)); + payload.add(ARG_PAYLOAD, argsPayload != null ? argsPayload : new JsonObject()); + return payload; + } + + /** + * Adds all standard network-level fields to the payload. + * Always-present: network_id, vlan, zone_id. + * Conditional (only when non-blank): gateway, cidr, vpc_id, and the two + * IPv6 network fields (network_ip6_gateway, network_ip6_cidr). + */ + private void addNetworkToPayload(JsonObject payload, Network network) { + if (payload == null || network == null) { + return; + } + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("vlan", safeStr(getVlanId(network))); + payload.addProperty("zone_id", String.valueOf(network.getDataCenterId())); + if (network.getGuestType() != null) { + payload.addProperty("guest_type", network.getGuestType().toString().toLowerCase()); + } + if (network.getState() != null) { + payload.addProperty("network_state", network.getState().toString().toLowerCase()); + } + if (StringUtils.isNotBlank(network.getGateway())) { + payload.addProperty("gateway", safeStr(network.getGateway())); + } + if (StringUtils.isNotBlank(network.getCidr())) { + payload.addProperty("cidr", safeStr(network.getCidr())); + } + if (network.getVpcId() != null) { + payload.addProperty("vpc_id", String.valueOf(network.getVpcId())); + } + if (StringUtils.isNotBlank(network.getIp6Gateway())) { + payload.addProperty("network_ip6_gateway", safeStr(network.getIp6Gateway())); + } + if (StringUtils.isNotBlank(network.getIp6Cidr())) { + payload.addProperty("network_ip6_cidr", safeStr(network.getIp6Cidr())); + } + } + + /** + * Adds all standard VPC-level fields to the payload. + * Always-present: vpc_id, zone_id. + * Conditional (only when non-null / non-blank): vpc_state, vpc_cidr. + */ + private void addVpcToPayload(JsonObject payload, Vpc vpc) { + if (payload == null || vpc == null) { + return; + } + payload.addProperty("vpc_id", String.valueOf(vpc.getId())); + payload.addProperty("zone_id", String.valueOf(vpc.getZoneId())); + if (vpc.getState() != null) { + payload.addProperty("vpc_state", vpc.getState().toString().toLowerCase()); + } + if (StringUtils.isNotBlank(vpc.getCidr())) { + payload.addProperty("vpc_cidr", safeStr(vpc.getCidr())); + } + } + + /** + * Adds all available NIC fields to the payload. + * Fields are added only when non-blank / non-null so the JSON stays clean. + * Covers: nic_id, nic_uuid, mac, ip, gateway (IPv4), netmask, default_nic, + * device_id, and the three IPv6 NIC fields. + */ + private void addNicToPayload(JsonObject payload, NicProfile nic) { + if (payload == null || nic == null) { + return; + } + payload.addProperty("nic_id", String.valueOf(nic.getId())); + if (StringUtils.isNotBlank(nic.getUuid())) { + payload.addProperty("nic_uuid", nic.getUuid()); + } + payload.addProperty("mac", safeStr(nic.getMacAddress())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + if (StringUtils.isNotBlank(nic.getIPv4Gateway())) { + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + } + if (StringUtils.isNotBlank(nic.getIPv4Netmask())) { + payload.addProperty("netmask", safeStr(nic.getIPv4Netmask())); + } + payload.addProperty("default_nic", String.valueOf(nic.isDefaultNic())); + if (nic.getDeviceId() != null) { + payload.addProperty("device_id", String.valueOf(nic.getDeviceId())); + } + if (StringUtils.isNotBlank(nic.getIPv6Address())) { + payload.addProperty("ip6_address", safeStr(nic.getIPv6Address())); + } + if (StringUtils.isNotBlank(nic.getIPv6Gateway())) { + payload.addProperty("ip6_gateway", safeStr(nic.getIPv6Gateway())); + } + if (StringUtils.isNotBlank(nic.getIPv6Cidr())) { + payload.addProperty("ip6_cidr", safeStr(nic.getIPv6Cidr())); + } + } + + /** + * Adds the network DNS fields to the payload: {@code dns} (comma-separated + * resolver list) and {@code domain} (network domain suffix). + */ + private void addNetworkDnsToPayload(JsonObject payload, Network network) { + if (payload == null || network == null) { + return; + } + payload.addProperty("dns", safeStr(getNetworkDns(network))); + payload.addProperty("domain", safeStr(network.getNetworkDomain())); + } + + /** + * Resolves the extension IP for {@code network} via {@link #ensureExtensionIp} + * and adds it to the payload as {@code extension_ip}. + */ + private void addExtensionIpToPayload(JsonObject payload, Network network) { + if (payload == null || network == null) { + return; + } + String extensionIp = ensureExtensionIp(network); + if (StringUtils.isNotBlank(extensionIp)) { + payload.addProperty("extension_ip", extensionIp); + } + } + + // ---- Detail helpers ---- + + /** + * Returns all {@code extension_resource_map_details} for the given extension + * on the physical network as a plain map, enriched with physical-network + * metadata (name, kvmnetworklabel, vmwarenetworklabel, xennetworklabel, + * public_kvmnetworklabel) so the wrapper script can derive bridge names and + * interface names without extra lookups. + */ + private Map buildPhysicalNetworkDetailsMap(Long physicalNetworkId, Extension extension) { + Map details = new HashMap<>(); + if (physicalNetworkId == null || extension == null) { + return details; + } + // Start with registered extension_resource_map_details + Map mapDetails = extensionHelper.getAllResourceMapDetailsForExtensionOnPhysicalNetwork( + physicalNetworkId, extension.getId()); + if (mapDetails != null) { + details.putAll(mapDetails); + } + + // Enrich with physical-network record fields + PhysicalNetworkVO pn = physicalNetworkDao.findById(physicalNetworkId); + if (pn != null && pn.getName() != null) { + details.put("physicalnetworkname", pn.getName()); + } + + return details; + } + + /** + * Builds the physical-network extension details as a {@link JsonObject}. + * Includes all {@code extension_resource_map_details} for the extension on the + * physical network, enriched with physical-network metadata fields. + */ + private JsonObject buildPhysicalNetworkExtensionDetailsPayload(Long physicalNetworkId, Extension extension) { + Map map = buildPhysicalNetworkDetailsMap(physicalNetworkId, extension); + JsonObject obj = new JsonObject(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() != null) { + obj.addProperty(entry.getKey(), entry.getValue()); + } + } + return obj; + } + + /** + * Returns the per-network extension-details JSON blob (the value stored under + * {@code NETWORK_DETAIL_EXTENSION_DETAILS} in {@code network_details} or + * {@code vpc_details}) as a {@link JsonObject}. + * Returns an empty object when no blob has been stored yet. + */ + private JsonObject buildNetworkExtensionDetailsPayload(Network network) { + String json; + if (network.getVpcId() != null) { + Map vpcDetails = vpcDetailsDao.listDetailsKeyPairs(network.getVpcId()); + json = vpcDetails != null ? vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + } else { + Map networkDetails = networkDetailsDao.listDetailsKeyPairs(network.getId()); + json = networkDetails != null ? networkDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + } + return parseJsonObjectOrEmpty(json); + } + + /** + * Returns the VPC-level extension-details JSON blob (stored under + * {@code NETWORK_DETAIL_EXTENSION_DETAILS} in {@code vpc_details}) as a + * {@link JsonObject}. Returns an empty object when no blob has been stored. + */ + private JsonObject buildVpcExtensionDetailsPayload(long vpcId) { + Map vpcDetails = vpcDetailsDao.listDetailsKeyPairs(vpcId); + String json = vpcDetails != null ? vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + return parseJsonObjectOrEmpty(json); + } + + /** + * Builds the custom-action parameters as a {@link JsonObject}. + * Returns an empty object for {@code null} or empty parameter maps. + */ + private JsonObject buildActionParamsPayload(Map parameters) { + JsonObject obj = new JsonObject(); + if (MapUtils.isEmpty(parameters)) { + return obj; + } + for (Map.Entry entry : parameters.entrySet()) { + obj.addProperty(entry.getKey(), + entry.getValue() != null ? entry.getValue().toString() : ""); + } + return obj; + } + + /** + * Parses a JSON string into a {@link JsonObject}. + * Returns an empty {@link JsonObject} when the input is {@code null}, blank, + * or not a valid JSON object. + */ + private JsonObject parseJsonObjectOrEmpty(String json) { + if (StringUtils.isBlank(json)) { + return new JsonObject(); + } + try { + JsonElement element = JsonParser.parseString(json); + return element.isJsonObject() ? element.getAsJsonObject() : new JsonObject(); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to parse JSON: " + json, e); + } + } + + // ---- Custom action ---- + + @Override + public boolean canHandleCustomAction(Network network) { + return canHandle(network, Service.CustomAction); + } + + /** + * Runs a custom action on the external network device. + * The custom action payload is written to a temporary file and passed to the + * extension script via {@link #executeScriptWithFilePayload(File, String, JsonObject, String)}. + */ + @Override + public String runCustomAction(Network network, String actionName, Map parameters) { + Extension extension = resolveExtension(network); + File scriptFile = resolveScriptFile(network, extension); + + JsonObject payload = buildCustomActionPayload(network, extension, actionName, parameters); + + logger.info("Running custom action '{}' on network {} (extension: {}, params: {} key(s))", + actionName, network, extension != null ? extension.getName() : "unknown", + parameters != null ? parameters.size() : 0); + + try { + Pair result = executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload, + "Network extension"); + String outputStr = result.second() != null ? result.second().trim() : ""; + + if (result.first() != EXIT_CODE_SUCCESS) { + logger.error("Custom action '{}' on network {} failed: {}", actionName, network, outputStr); + return null; + } + logger.info("Custom action '{}' on network {} completed successfully", actionName, network); + return outputStr.isEmpty() ? "OK" : outputStr; + } catch (Exception e) { + logger.error("Failed to execute custom action '{}' on network {}: {}", actionName, network, e.getMessage()); + throw new CloudRuntimeException(String.format("Failed to execute custom action %s on network %s", actionName, network.getUuid()), e); + } + } + + @Override + public boolean canHandleVpcCustomAction(Vpc vpc) { + return resolveExtensionForVpc(vpc) != null; + } + + /** + * Runs a custom action on the external network device for a VPC. + * The custom action payload is written to a temporary file and passed to the + * extension script via {@link #executeScriptWithFilePayload(File, String, JsonObject, String)}. + */ + @Override + public String runCustomAction(Vpc vpc, String actionName, Map parameters) { + Pair physNetAndExt = resolveExtensionForVpc(vpc); + if (physNetAndExt == null) { + throw new CloudRuntimeException("No extension found for VPC " + vpc.getUuid()); + } + Long physicalNetworkId = physNetAndExt.first(); + Extension extension = physNetAndExt.second(); + File scriptFile = resolveScriptFileForVpc(physicalNetworkId, extension); + + JsonObject payload = buildCustomActionPayload(vpc, physicalNetworkId, extension, actionName, parameters); + + logger.info("Running custom action '{}' on VPC {} (extension: {}, params: {} key(s))", + actionName, vpc, extension != null ? extension.getName() : "unknown", + parameters != null ? parameters.size() : 0); + + try { + Pair result = executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload, + "VPC extension"); + String outputStr = result.second() != null ? result.second().trim() : ""; + + if (result.first() != EXIT_CODE_SUCCESS) { + logger.error("VPC custom action '{}' on VPC {} failed: {}", actionName, vpc, outputStr); + return null; + } + logger.info("VPC custom action '{}' on VPC {} completed successfully", actionName, vpc); + return outputStr.isEmpty() ? "OK" : outputStr; + } catch (Exception e) { + logger.error("Failed to execute VPC custom action '{}' on VPC {}: {}", actionName, vpc, e.getMessage()); + throw new CloudRuntimeException(String.format("Failed to execute VPC custom action %s on VPC %s ", actionName, vpc.getUuid()), e); + } + } + + private JsonObject buildCustomActionPayload(Network network, Extension extension, String actionName, + Map parameters) { + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + payload.addProperty("action", actionName); + payload.add(ARG_ACTION_PARAMS, buildActionParamsPayload(parameters)); + payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS, + buildPhysicalNetworkExtensionDetailsPayload(network.getPhysicalNetworkId(), extension)); + payload.add(ARG_NETWORK_EXTENSION_DETAILS, + buildNetworkExtensionDetailsPayload(network)); + return payload; + } + + private JsonObject buildCustomActionPayload(Vpc vpc, Long physicalNetworkId, Extension extension, + String actionName, Map parameters) { + JsonObject payload = new JsonObject(); + addVpcToPayload(payload, vpc); + payload.addProperty("action", actionName); + payload.add(ARG_ACTION_PARAMS, buildActionParamsPayload(parameters)); + payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS, + buildPhysicalNetworkExtensionDetailsPayload(physicalNetworkId, extension)); + payload.add(ARG_NETWORK_EXTENSION_DETAILS, + buildVpcExtensionDetailsPayload(vpc.getId())); + return payload; + } + + // ---- Script file resolution ---- + + /** + * Resolves the executable script file from the given extension. + * + *

Lookup order (first match wins):

+ *
    + *
  1. {@code /.sh} — preferred convention, + * e.g. for an extension named {@code network-extension} the script is + * {@code network-extension.sh}.
  2. + *
  3. {@code } itself, if it is a file and is executable.
  4. + *
+ */ + protected File resolveScriptFile(Network network, Extension extension) { + Long physicalNetworkId = network.getPhysicalNetworkId(); + if (physicalNetworkId == null) { + throw new CloudRuntimeException("Network " + network.getUuid() + " has no physical network"); + } + if (extension == null) { + throw new CloudRuntimeException( + "No NetworkOrchestrator extension found for network " + network.getUuid() + + " on physical network " + physicalNetworkId); + } + if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) { + throw new CloudRuntimeException("Extension " + extension.getName() + " is not of type NetworkOrchestrator"); + } + if (!Extension.State.Enabled.equals(extension.getState())) { + throw new CloudRuntimeException("Extension " + extension.getName() + " is not enabled"); + } + if (!extension.isPathReady()) { + throw new CloudRuntimeException("Extension " + extension.getName() + " path is not ready"); + } + + String extensionPath = extensionHelper.getExtensionScriptPath(extension); + if (extensionPath == null) { + throw new CloudRuntimeException("Could not resolve path for extension " + extension.getName()); + } + + File extensionFile = new File(extensionPath); + if (extensionFile.isFile() && extensionFile.canExecute()) { + return extensionFile; + } + + throw new CloudRuntimeException( + "No executable script found in extension path " + extensionPath + " inside the extension directory."); + } + + // ---- Helpers ---- + + private String getVlanId(Network network) { + return network.getBroadcastUri() != null + ? Networks.BroadcastDomainType.getValue(network.getBroadcastUri()) : null; + } + + private String getIpAddress(Long ipAddressId) { + if (ipAddressId == null) { + return ""; + } + IpAddress ip = networkModel.getIp(ipAddressId); + return ip != null ? ip.getAddress().addr() : ""; + } + + /** + * Adds all standard public-IP fields to the payload. + * Makes exactly two DB calls: one for the {@link IpAddress} and one for + * its {@link VlanVO}. All five fields are then derived from those two + * objects in memory — no further DB calls are made. + * Fields: {@code public_ip}, {@code public_vlan}, {@code public_gateway}, + * {@code public_cidr}, {@code source_nat}. + */ + private void addPublicIpToPayload(JsonObject payload, Long ipAddressId, boolean sourceNat) { + if (payload == null || ipAddressId == null) { + return; + } + IpAddress ip = networkModel.getIp(ipAddressId); + if (ip == null || ip.getAddress() == null) { + return; + } + VlanVO vlan = vlanDao.findById(ip.getVlanId()); + payload.addProperty("public_ip", safeStr(ip.getAddress().addr())); + payload.addProperty("public_vlan", vlan != null ? safeStr(vlan.getVlanTag()) : ""); + payload.addProperty("public_gateway", vlan != null ? safeStr(vlan.getVlanGateway()) : ""); + payload.addProperty("public_cidr", vlan != null + ? StringUtils.defaultString(NetUtils.ipAndNetMaskToCidr(ip.getAddress().addr(), vlan.getVlanNetmask())) : ""); + payload.addProperty("source_nat", String.valueOf(sourceNat)); + } + + private String safeStr(String value) { + return value != null ? value : ""; + } + + // ---- DhcpServiceProvider ---- + + private String getNetworkDns(final Network network) { + final DataCenter dc = dataCenterDao.findById(network.getDataCenterId()); + Pair dnsList = networkModel.getNetworkIp4Dns(network, dc); + return dnsList.first() + (dnsList.second() != null ? "," + dnsList.second() : ""); + } + + @Override + public boolean addDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (!canHandle(network, Service.Dhcp)) { + return false; + } + logger.debug("addDhcpEntry: network={} mac={} ip={} ipv6={}", network, + nic.getMacAddress(), nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("hostname", safeStr(vm.getHostName())); + addNetworkDnsToPayload(payload, network); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_ADD_DHCP_ENTRY, payload); + } + + @Override + public boolean configDhcpSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (!canHandle(network, Service.Dhcp)) { + return false; + } + logger.debug("configDhcpSupportForSubnet: network={}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + addExtensionIpToPayload(payload, network); + addNicToPayload(payload, nic); + return executeScript(network, CMD_CONFIG_DHCP_SUBNET, payload); + } + + @Override + public boolean removeDhcpSupportForSubnet(Network network) throws ResourceUnavailableException { + if (!canHandle(network, Service.Dhcp)) { + return false; + } + logger.debug("removeDhcpSupportForSubnet: network={}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_REMOVE_DHCP_SUBNET, payload); + } + + @Override + public boolean setExtraDhcpOptions(Network network, long nicId, Map dhcpOptions) { + return false; + } + + @Override + public boolean removeDhcpEntry(Network network, NicProfile nic, VirtualMachineProfile vmProfile) + throws ResourceUnavailableException { + if (!canHandle(network, Service.Dhcp)) { + return false; + } + logger.debug("removeDhcpEntry: network={} mac={} ip={} ipv6={}", network, + nic.getMacAddress(), nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_REMOVE_DHCP_ENTRY, payload); + } + + // ---- DnsServiceProvider ---- + + @Override + public boolean addDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (!canHandle(network, Service.Dns)) { + return false; + } + String hostname = vm.getHostName(); + logger.debug("addDnsEntry: network={} hostname={} ip={} ipv6={}", network, + hostname, nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("hostname", safeStr(hostname)); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_ADD_DNS_ENTRY, payload); + } + + @Override + public boolean removeDnsEntry(Network network, NicProfile nic, VirtualMachineProfile vmProfile) throws ResourceUnavailableException { + if (!canHandle(network, Service.Dns)) { + return false; + } + logger.debug("removeDnsEntry: network={} mac={} ip={} ipv6={}", network, + nic.getMacAddress(), nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + addNicToPayload(payload, nic); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_REMOVE_DNS_ENTRY, payload); + } + + @Override + public boolean configDnsSupportForSubnet(Network network, NicProfile nic, VirtualMachineProfile vm, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (!canHandle(network, Service.Dns)) { + return false; + } + logger.debug("configDnsSupportForSubnet: network={}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + addExtensionIpToPayload(payload, network); + addNicToPayload(payload, nic); + return executeScript(network, CMD_CONFIG_DNS_SUBNET, payload); + } + + @Override + public boolean removeDnsSupportForSubnet(Network network) throws ResourceUnavailableException { + if (!canHandle(network, Service.Dns)) { + return false; + } + logger.debug("removeDnsSupportForSubnet: network={}", network); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_REMOVE_DNS_SUBNET, payload); + } + + // ---- UserDataServiceProvider ---- + + @Override + public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile profile, + DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (!canHandle(network, Service.UserData)) { + return false; + } + + VirtualMachine vm = profile.getVirtualMachine(); + + // SSH public key from VM instance details + String sshPublicKey = null; + try { + VMInstanceDetailVO sshKeyDetail = vmInstanceDetailsDao.findDetail(profile.getId(), VmDetailConstants.SSH_PUBLIC_KEY); + if (sshKeyDetail != null) { + sshPublicKey = sshKeyDetail.getValue(); + } + } catch (Exception e) { + logger.debug("Could not fetch SSH public key for VM {}: {}", profile, e.getMessage()); + } + + // Service offering display name + String serviceOfferingName = ""; + try { + serviceOfferingName = profile.getServiceOffering().getDisplayText(); + } catch (Exception e) { + logger.debug("Could not fetch service offering for VM {}: {}", profile, e.getMessage()); + } + + // Is Windows guest? + boolean isWindows = false; + try { + isWindows = guestOSCategoryDao + .findById(guestOSDao.findById(vm.getGuestOSId()).getCategoryId()) + .getName().equalsIgnoreCase("Windows"); + } catch (Exception e) { + logger.debug("Could not determine OS type for VM {}: {}", profile, e.getMessage()); + } + + // Hypervisor hostname – prefer dest host, fall back to current host + String destHostname = null; + try { + if (dest != null && dest.getHost() != null) { + destHostname = VirtualMachineManager.getHypervisorHostname(dest.getHost().getName()); + } else if (vm.getHostId() != null) { + destHostname = VirtualMachineManager.getHypervisorHostname( + hostDao.findById(vm.getHostId()).getName()); + } + } catch (Exception e) { + logger.debug("Could not resolve hypervisor hostname for VM {}: {}", profile, e.getMessage()); + } + + // Password from the VM profile parameter (set by UserVmManager before deployment) + String password = (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword); + + // Use this NIC's IP — the metadata server in each namespace identifies requesters + // by REMOTE_ADDR, which will be the VM's IP on THIS network (not necessarily the + // default NIC IP), so we always key metadata by the NIC's IP on this network. + String nicIpAddress = nic.getIPv4Address(); + + logger.debug("addPasswordAndUserdata: network={} ip={} ipv6={} hasPassword={} hasSshKey={}", + network.getId(), nicIpAddress, nic.getIPv6Address(), + StringUtils.isNotEmpty(password), + StringUtils.isNotEmpty(sshPublicKey)); + + final UserVmVO userVm = userVmDao.findById(vm.getId()); + if (userVm == null) { + throw new CloudRuntimeException("Could not find UserVmVO for VM " + vm.getUuid()); + } + + // Generate the full metadata set (userdata, meta-data/*, password) in one go + List vmData = networkModel.generateVmData( + userVm.getUserData(), + userVm.getUserDataDetails(), + serviceOfferingName, + vm.getDataCenterId(), + profile.getInstanceName(), + profile.getHostName(), + profile.getId(), + profile.getUuid(), + nicIpAddress, + sshPublicKey, + password, + isWindows, + destHostname); + + if (CollectionUtils.isEmpty(vmData)) { + logger.debug("addPasswordAndUserdata: no VM data generated for network={} ip={} ipv6={}", network, nicIpAddress, nic.getIPv6Address()); + return true; + } + + JsonArray vmDataArray = new JsonArray(); + for (String[] entry : vmData) { + String dir = entry[NetworkModel.CONFIGDATA_DIR]; + String file = entry[NetworkModel.CONFIGDATA_FILE]; + String content = entry.length > NetworkModel.CONFIGDATA_CONTENT + ? entry[NetworkModel.CONFIGDATA_CONTENT] : null; + if (content == null) content = ""; + + String contentStr; + if (NetworkModel.USERDATA_DIR.equals(dir) && NetworkModel.USERDATA_FILE.equals(file)) { + try { + contentStr = new String(Base64.getDecoder().decode(content), StandardCharsets.UTF_8); + } catch (Exception e) { + contentStr = content; + } + } else { + contentStr = content; + } + + JsonObject entryObj = new JsonObject(); + entryObj.addProperty("dir", dir); + entryObj.addProperty("file", file); + entryObj.addProperty("content", contentStr); + vmDataArray.add(entryObj); + } + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("ip", safeStr(nicIpAddress)); + addExtensionIpToPayload(payload, network); + payload.add("vm_data", vmDataArray); + + return executeScript(network, CMD_SAVE_VM_DATA, payload); + } + + @Override + public boolean savePassword(Network network, NicProfile nic, VirtualMachineProfile vm) + throws ResourceUnavailableException { + if (!canHandle(network, Service.UserData)) { + return false; + } + String password = (String) vm.getParameter(VirtualMachineProfile.Param.VmPassword); + if (StringUtils.isEmpty(password)) { + return true; + } + logger.debug("savePassword: network={} ip={} ipv6={}", network, nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("password", password); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_SAVE_PASSWORD, payload); + } + + @Override + public boolean saveUserData(Network network, NicProfile nic, VirtualMachineProfile vm) + throws ResourceUnavailableException { + if (!canHandle(network, Service.UserData)) { + return false; + } + String userData = null; + if (vm.getVirtualMachine() instanceof UserVm) { + userData = ((UserVm) vm.getVirtualMachine()).getUserData(); + } + if (StringUtils.isEmpty(userData)) { + return true; + } + logger.debug("saveUserData: network={} ip={} ipv6={}", network, nic.getIPv4Address(), nic.getIPv6Address()); + String userDataDecoded; + try { + userDataDecoded = new String(Base64.getDecoder().decode(userData), StandardCharsets.UTF_8); + } catch (Exception e) { + userDataDecoded = userData; + } + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("userdata", userDataDecoded); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_SAVE_USERDATA, payload); + } + + @Override + public boolean saveSSHKey(Network network, NicProfile nic, VirtualMachineProfile vm, + String sshPublicKey) throws ResourceUnavailableException { + if (!canHandle(network, Service.UserData)) { + return false; + } + if (StringUtils.isEmpty(sshPublicKey)) { + return true; + } + logger.debug("saveSSHKey: network={} ip={} ipv6={}", network, nic.getIPv4Address(), nic.getIPv6Address()); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("sshkey", sshPublicKey); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_SAVE_SSHKEY, payload); + } + + @Override + public boolean saveHypervisorHostname(NicProfile nic, Network network, VirtualMachineProfile vm, + DeployDestination dest) throws ResourceUnavailableException { + if (!canHandle(network, Service.UserData)) { + return false; + } + String hostname = dest != null && dest.getHost() != null ? dest.getHost().getName() : null; + if (StringUtils.isBlank(hostname)) { + return true; + } + logger.debug("saveHypervisorHostname: network={} ip={} ipv6={} host={}", network, + nic.getIPv4Address(), nic.getIPv6Address(), hostname); + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addNicToPayload(payload, nic); + payload.addProperty("hypervisor_hostname", hostname); + addExtensionIpToPayload(payload, network); + return executeScript(network, CMD_SAVE_HYPERVISOR_HOSTNAME, payload); + } + + // ---- LoadBalancingServiceProvider ---- + + @Override + public boolean applyLBRules(Network network, List rules) + throws ResourceUnavailableException { + if (CollectionUtils.isEmpty(rules)) { + return true; + } + if (!canHandle(network, Service.Lb)) { + return false; + } + logger.info("Applying {} LB rules for network {}", rules.size(), network); + + JsonArray lbRulesArray = new JsonArray(); + for (LoadBalancingRule rule : rules) { + boolean revoke = rule.getState() == FirewallRule.State.Revoke; + JsonObject ruleObj = new JsonObject(); + ruleObj.addProperty("id", rule.getId()); + ruleObj.addProperty("name", rule.getName()); + ruleObj.addProperty("publicIp", rule.getSourceIp() != null ? rule.getSourceIp().addr() : ""); + ruleObj.addProperty("publicPort", rule.getSourcePortStart()); + ruleObj.addProperty("privatePort", rule.getDefaultPortStart()); + ruleObj.addProperty("protocol", safeStr(rule.getProtocol())); + ruleObj.addProperty("algorithm", safeStr(rule.getAlgorithm())); + ruleObj.addProperty("revoke", revoke); + JsonArray backendsArray = buildLBRuleBackendArray(rule); + ruleObj.add("backends", backendsArray); + lbRulesArray.add(ruleObj); + } + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + payload.add("lb_rules", lbRulesArray); + boolean result = executeScript(network, CMD_APPLY_LB_RULES, payload); + if (!result) { + throw new ResourceUnavailableException("Failed to apply LB rules for network " + network.getUuid(), + Network.class, network.getId()); + } + return true; + } + + private static JsonArray buildLBRuleBackendArray(LoadBalancingRule rule) { + JsonArray backendsArray = new JsonArray(); + if (rule.getDestinations() != null) { + for (LoadBalancingRule.LbDestination dest : rule.getDestinations()) { + JsonObject destObj = new JsonObject(); + destObj.addProperty("ip", dest.getIpAddress()); + destObj.addProperty("port", dest.getDestinationPortStart()); + destObj.addProperty("revoked", dest.isRevoked()); + backendsArray.add(destObj); + } + } + return backendsArray; + } + + @Override + public boolean validateLBRule(Network network, LoadBalancingRule rule) { + // Delegate validation to the external script; accept by default + return true; + } + + @Override + public List updateHealthChecks(Network network, + List lbrules) { + // Health-check state updates are not implemented via this path + return new ArrayList<>(); + } + + @Override + public boolean handlesOnlyRulesInTransitionState() { + return false; + } + + @Override + public IpDeployer getIpDeployer(Network network) { + // This element itself implements IpDeployer; return this instance. + return this; + } + + @Override + public boolean rollingRestartSupported() { + return false; + } + + /** + * Applies all active firewall rules for a network to the external network device. + * + *

Three categories of rules are handled:

+ *
    + *
  1. Egress rules ({@link FirewallRule.TrafficType#Egress}) — control outbound + * traffic from guest VMs. The network offering's {@code egressDefaultPolicy} flag + * is consulted: + *
      + *
    • {@code true} (ALLOW by default) — each egress rule becomes a DROP rule; + * a catch-all ACCEPT is appended at the end.
    • + *
    • {@code false} (DENY by default) — each egress rule becomes an ACCEPT rule; + * a catch-all DROP is appended at the end.
    • + *
    + *
  2. + *
  3. Ingress rules ({@link FirewallRule.TrafficType#Ingress}) on public IPs + * (static NAT, port-forwarding, LB, …) — control inbound access to a specific + * public IP. The wrapper script uses {@code conntrack --ctorigdst} to match the + * original pre-DNAT destination, so no private-IP lookup is required and all + * DNAT-based services (static-NAT, port-forwarding, LB) are handled uniformly.
  4. + *
  5. Default egress policy — always conveyed via the JSON payload so the + * script can enforce it even when the explicit rule list is empty.
  6. + *
+ * + *

Full-state rebuild semantics: + * {@code applyFWRules} is called with a narrow scope — the firewall manager + * passes only the rules for one public IP ({@code applyIngressFirewallRules}) or only + * the egress rules ({@code applyEgressFirewallRules}) per call. The script, however, + * rebuilds the entire firewall chain from scratch each time it runs. To avoid wiping + * the rules for other IPs on every call, this method ignores the {@code rules} parameter + * and instead queries the database for all active (non-revoked, non-System) + * {@link FirewallRule.Purpose#Firewall} rules for the network.

+ * + *

Script command: {@code apply-fw-rules}

+ */ + @Override + public boolean applyFWRules(Network network, List rules) + throws ResourceUnavailableException { + if (!canHandle(network, Service.Firewall)) { + return false; + } + + // Determine default egress policy from the network offering. + // true = ALLOW (default permissive; explicit rules are deny-rules) + // false = DENY (default restrictive; explicit rules are allow-rules) + NetworkOfferingVO offering = networkOfferingDao.findById(network.getNetworkOfferingId()); + boolean defaultEgressAllow = offering == null || offering.isEgressDefaultPolicy(); + + // Load ALL active (non-revoked) firewall rules for this network from the DB. + // applyFWRules is called in a narrow scope (only one public IP's ingress rules, or + // only egress rules per call), but the script does a full rebuild of the firewall + // chain. Querying the DB ensures every call produces a complete, correct chain. + List allRules = firewallRulesDao.listByNetworkAndPurposeAndNotRevoked( + network.getId(), FirewallRule.Purpose.Firewall); + // Skip System-type rules — the default egress policy is already conveyed by + // "default_egress_allow". System rules are transient (not stored in DB), but + // guard here anyway in case of future changes. + allRules = allRules.stream() + .filter(r -> !FirewallRule.FirewallRuleType.System.equals(r.getType())) + .collect(Collectors.toList()); + + for (FirewallRuleVO r : allRules) { + firewallRulesDao.loadSourceCidrs(r); + firewallRulesDao.loadDestinationCidrs(r); + } + + logger.info("applyFWRules: network={} activeRules={} defaultEgressAllow={}", + network, allRules.size(), defaultEgressAllow); + + JsonObject fwRules = new JsonObject(); + fwRules.addProperty("default_egress_allow", defaultEgressAllow); + fwRules.addProperty("cidr", safeStr(network.getCidr())); + JsonArray rulesArray = new JsonArray(); + for (FirewallRuleVO rule : allRules) { + boolean isEgress = FirewallRule.TrafficType.Egress.equals(rule.getTrafficType()); + JsonObject ruleObj = new JsonObject(); + ruleObj.addProperty("id", rule.getId()); + ruleObj.addProperty("type", isEgress ? "egress" : "ingress"); + ruleObj.addProperty("protocol", safeStr(rule.getProtocol())); + if (rule.getSourcePortStart() != null) ruleObj.addProperty("portStart", rule.getSourcePortStart()); + if (rule.getSourcePortEnd() != null) ruleObj.addProperty("portEnd", rule.getSourcePortEnd()); + if (rule.getIcmpType() != null) ruleObj.addProperty("icmpType", rule.getIcmpType()); + if (rule.getIcmpCode() != null) ruleObj.addProperty("icmpCode", rule.getIcmpCode()); + // For ingress rules include the public IP the rule is associated with. + if (!isEgress) ruleObj.addProperty("publicIp", getIpAddress(rule.getSourceIpAddressId())); + // sourceCidrs: for ingress = allowed external source IPs; + // for egress = allowed VM source IP ranges + JsonArray sourceCidrsArray = new JsonArray(); + List sourceCidrs = rule.getSourceCidrList(); + if (CollectionUtils.isNotEmpty(sourceCidrs)) { + for (String cidr : sourceCidrs) sourceCidrsArray.add(cidr); + } + ruleObj.add("sourceCidrs", sourceCidrsArray); + // destCidrs: optional destination CIDR filter (meaningful for egress rules) + JsonArray destCidrsArray = new JsonArray(); + List destCidrs = rule.getDestinationCidrList(); + if (CollectionUtils.isNotEmpty(destCidrs)) { + for (String cidr : destCidrs) destCidrsArray.add(cidr); + } + ruleObj.add("destCidrs", destCidrsArray); + rulesArray.add(ruleObj); + } + fwRules.add("rules", rulesArray); + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + payload.add("fw_rules", fwRules); + + boolean result = executeScript(network, CMD_APPLY_FW_RULES, payload); + if (!result) { + throw new ResourceUnavailableException( + "Failed to apply firewall rules for network " + network, + Network.class, network.getId()); + } + return true; + } + + // ---- AggregatedCommandExecutor ---- + + /** + * Called at the start of a network-restart cycle (before rules are re-programmed). + * We have nothing to "start" here — the batch restore is driven by + * {@link #completeAggregatedExecution}. + */ + @Override + public boolean prepareAggregatedExecution(Network network, DeployDestination dest) + throws ResourceUnavailableException { + if (!canHandle(network, null)) { + return true; + } + logger.debug("prepareAggregatedExecution: network={} dest={}", network, dest); + return true; + } + + /** + * Called after all firewall/NAT/LB rules have been re-applied during a network restart. + * + *

Queries all active User-VM NICs on this network from the database, builds a single + * batch JSON payload containing DHCP/DNS/metadata entries for every VM, and sends it to + * the wrapper script as a single {@code restore-network} call. This avoids N script + * invocations (one per VM) and instead performs the full restore in one shot.

+ */ + @Override + public boolean completeAggregatedExecution(Network network, DeployDestination dest) + throws ResourceUnavailableException { + if (!canHandle(network, null)) { + return true; + } + + logger.debug("completeAggregatedExecution: restoring all VM network data for network={} dest={}", network, dest); + + boolean dhcpEnabled = networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dhcp) + && networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dhcp, getProvider()); + boolean dnsEnabled = networkModel.areServicesSupportedInNetwork(network.getId(), Service.Dns) + && networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.Dns, getProvider()); + boolean userdataEnabled = networkModel.areServicesSupportedInNetwork(network.getId(), Service.UserData) + && networkModel.isProviderSupportServiceInNetwork(network.getId(), Service.UserData, getProvider()); + + if (!dhcpEnabled && !dnsEnabled && !userdataEnabled) { + logger.debug("completeAggregatedExecution: no DHCP/DNS/UserData service for network={}, skipping", network); + return true; + } + + // Query all active User-VM NICs on this network + List nics = nicDao.listByNetworkIdAndType(network.getId(), VirtualMachine.Type.User); + if (CollectionUtils.isEmpty(nics)) { + logger.debug("completeAggregatedExecution: no user VM NICs on network={}, skipping", network); + return true; + } + + logger.info("completeAggregatedExecution: building batch restore for {} VMs on network={}", + nics.size(), network); + + JsonObject restoreData = buildRestoreNetworkData(network, nics, dhcpEnabled, dnsEnabled, userdataEnabled); + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + addExtensionIpToPayload(payload, network); + addNetworkDnsToPayload(payload, network); + payload.add("restore_data", restoreData); + + return executeScript(network, CMD_RESTORE_NETWORK, payload); + } + + /** + * Called in the {@code finally} block of the network-restart cycle to clean up any + * temporary state created by {@link #prepareAggregatedExecution}. + * Nothing to clean up here. + */ + @Override + public boolean cleanupAggregatedExecution(Network network, DeployDestination dest) + throws ResourceUnavailableException { + return true; + } + + /** + * Builds the base64-encoded JSON payload for {@code restore-network}. + * + *

The JSON structure is:

+ *
+     * {
+     *   "dhcp_enabled": true,
+     *   "dns_enabled":  true,
+     *   "userdata_enabled": true,
+     *   "vms": [
+     *     {
+     *       "ip":          "10.0.0.10",
+     *       "mac":         "02:00:00:00:00:01",
+     *       "hostname":    "vm-1",
+     *       "default_nic": true,
+     *       "vm_data": [
+     *         { "dir": "userdata", "file": "user-data", "content": "" },
+     *         { "dir": "meta-data", "file": "instance-id", "content": "" },
+     *         ...
+     *       ]
+     *     },
+     *     ...
+     *   ]
+     * }
+     * 
+ * + *

Each {@code vm_data} entry has its {@code content} as a plain UTF-8 string, + * matching the encoding used by the per-VM {@code save-vm-data} command.

+ */ + private JsonObject buildRestoreNetworkData(Network network, List nics, + boolean dhcpEnabled, boolean dnsEnabled, boolean userdataEnabled) { + + Map offeringNameCache = new HashMap<>(); + + JsonObject root = new JsonObject(); + root.addProperty("dhcp_enabled", dhcpEnabled); + root.addProperty("dns_enabled", dnsEnabled); + root.addProperty("userdata_enabled", userdataEnabled); + JsonArray vmsArray = new JsonArray(); + + for (NicVO nic : nics) { + if (nic.getState() != Nic.State.Reserved && nic.getState() != Nic.State.Allocated) { + continue; + } + if (nic.getIPv4Address() == null || nic.getMacAddress() == null) { + continue; + } + + long instanceId = nic.getInstanceId(); + + UserVmVO userVm = userVmDao.findById(instanceId); + if (userVm == null) { + continue; + } + + // Per-VM data array (only if UserData service is enabled) + List vmData = null; + if (userdataEnabled) { + try { + // Service offering display text + String offeringName = offeringNameCache.computeIfAbsent(userVm.getServiceOfferingId(), id -> { + try { + ServiceOfferingVO so = serviceOfferingDao.findById(id); + return so != null ? so.getDisplayText() : ""; + } catch (Exception e) { + return ""; + } + }); + + // SSH public key + String sshPublicKey = null; + try { + VMInstanceDetailVO sshKeyDetail = vmInstanceDetailsDao.findDetail(instanceId, VmDetailConstants.SSH_PUBLIC_KEY); + if (sshKeyDetail != null) { + sshPublicKey = sshKeyDetail.getValue(); + } + } catch (Exception e) { + logger.debug("Could not fetch SSH key for VM {}: {}", userVm, e.getMessage()); + } + + // Is Windows? + boolean isWindows = false; + try { + isWindows = guestOSCategoryDao + .findByIdIncludingRemoved(guestOSDao.findByIdIncludingRemoved(userVm.getGuestOSId()).getCategoryId()) + .getName().equalsIgnoreCase("Windows"); + } catch (Exception ignored) { } + + // Hypervisor hostname from current host + String destHostname = null; + try { + if (userVm.getHostId() != null) { + destHostname = VirtualMachineManager.getHypervisorHostname( + hostDao.findById(userVm.getHostId()).getName()); + } + } catch (Exception ignored) { } + + vmData = networkModel.generateVmData( + userVm.getUserData(), + userVm.getUserDataDetails(), + offeringName, + userVm.getDataCenterId(), + userVm.getInstanceName(), + userVm.getHostName(), + userVm.getId(), + userVm.getUuid(), + nic.getIPv4Address(), + sshPublicKey, + null, // password — not re-issued on restore + isWindows, + destHostname); + } catch (Exception e) { + logger.warn("Could not generate vmData for VM {} on network {}: {}", userVm, network, e.getMessage()); + } + } + + JsonObject vmObj = new JsonObject(); + addNetworkDnsToPayload(vmObj, network); + vmObj.addProperty("ip", nic.getIPv4Address()); + vmObj.addProperty("ip6_address", safeStr(nic.getIPv6Address())); + vmObj.addProperty("mac", nic.getMacAddress()); + vmObj.addProperty("nic_uuid", safeStr(nic.getUuid())); + vmObj.addProperty("hostname", safeStr(userVm.getHostName())); + vmObj.addProperty("device_id", String.valueOf(nic.getDeviceId())); + vmObj.addProperty("default_nic", nic.isDefaultNic()); + + JsonArray vmDataArray = new JsonArray(); + if (CollectionUtils.isNotEmpty(vmData)) { + for (String[] entry : vmData) { + String dir = entry[NetworkModel.CONFIGDATA_DIR]; + String file = entry[NetworkModel.CONFIGDATA_FILE]; + String content = entry.length > NetworkModel.CONFIGDATA_CONTENT + ? entry[NetworkModel.CONFIGDATA_CONTENT] : null; + if (content == null) content = ""; + + String contentStr; + if (NetworkModel.USERDATA_DIR.equals(dir) && NetworkModel.USERDATA_FILE.equals(file)) { + try { + contentStr = new String(Base64.getDecoder().decode(content), StandardCharsets.UTF_8); + } catch (Exception e) { + contentStr = content; + } + } else { + contentStr = content; + } + + JsonObject entryObj = new JsonObject(); + entryObj.addProperty("dir", dir); + entryObj.addProperty("file", file); + entryObj.addProperty("content", contentStr); + vmDataArray.add(entryObj); + } + } + vmObj.add("vm_data", vmDataArray); + vmsArray.add(vmObj); + } + + root.add("vms", vmsArray); + return root; + } + + // ---- VpcProvider ---- + + /** + * Finds the extension + physical-network pair for the given VPC by scanning the + * physical networks in the VPC's zone for a registered NetworkOrchestrator extension. + * Returns {@code null} when no suitable extension is found. + */ + protected Pair resolveExtensionForVpc(Vpc vpc) { + List physNetworks; + List networks = networkDao.listByVpc(vpc.getId()); + if (CollectionUtils.isNotEmpty(networks)) { + physNetworks = new ArrayList<>(); + for (NetworkVO network : networks) { + PhysicalNetworkVO pn = physicalNetworkDao.findById(network.getPhysicalNetworkId()); + if (pn != null && !physNetworks.contains(pn)) { + physNetworks.add(pn); + } + } + } else { + physNetworks = physicalNetworkDao.listByZoneAndTrafficType(vpc.getZoneId(), Networks.TrafficType.Guest); + if (CollectionUtils.isEmpty(physNetworks)) { + return null; + } + } + for (PhysicalNetworkVO pn : physNetworks) { + Extension ext = extensionHelper.getExtensionForPhysicalNetworkAndProvider(pn.getId(), providerName); + if (ext != null) { + return new Pair<>(pn.getId(), ext); + } + } + return null; + } + + /** + * Resolves the script file for a VPC-level operation (no network object required). + */ + protected File resolveScriptFileForVpc(Long physicalNetworkId, Extension extension) { + if (physicalNetworkId == null) { + throw new CloudRuntimeException("No physical network ID for VPC extension"); + } + if (extension == null) { + throw new CloudRuntimeException("No extension found for physical network " + physicalNetworkId); + } + if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) { + throw new CloudRuntimeException("Extension " + extension.getName() + " is not of type NetworkOrchestrator"); + } + if (!Extension.State.Enabled.equals(extension.getState())) { + throw new CloudRuntimeException("Extension " + extension.getName() + " is not enabled"); + } + if (!extension.isPathReady()) { + throw new CloudRuntimeException("Extension " + extension.getName() + " path is not ready"); + } + String extensionPath = extensionHelper.getExtensionScriptPath(extension); + if (extensionPath == null) { + throw new CloudRuntimeException("Could not resolve path for extension " + extension.getName()); + } + File extensionFile = new File(extensionPath); + if (extensionFile.isFile() && extensionFile.canExecute()) { + return extensionFile; + } + throw new CloudRuntimeException( + "No executable script found in extension path " + extensionPath + + ". Expected '" + extension.getName() + ".sh'."); + } + + /** + * Calls {@code ensure-network-device} with VPC-level args (no {@code --network-id}). + * The returned JSON is persisted in {@code vpc_details} under key + * {@value #NETWORK_DETAIL_EXTENSION_DETAILS}. VPC tier networks then inherit + * these details via {@link #ensureExtensionDetails(Network)}. + */ + protected void ensureExtensionDetails(Vpc vpc) { + Map stored = vpcDetailsDao.listDetailsKeyPairs(vpc.getId()); + String currentDetails = stored != null + ? stored.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + + logger.info("Ensuring extension device for VPC {} (current={})", vpc, currentDetails); + + Pair physNetAndExt = resolveExtensionForVpc(vpc); + if (physNetAndExt == null) { + logger.warn("ensureExtensionDetails(vpc): no extension found for VPC {} zone {}", + vpc, vpc.getZoneId()); + return; + } + JsonObject argsPayload = new JsonObject(); + addVpcToPayload(argsPayload, vpc); + argsPayload.addProperty("current_details", currentDetails); + + try { + Pair result = executeVpcScriptAndReturnOutput(vpc, CMD_ENSURE_NETWORK_DEVICE, argsPayload); + String output = result.second() != null ? result.second() : ""; + + if (result.first() != EXIT_CODE_SUCCESS) { + logger.warn("ensure-network-device exited {} for VPC {} — keeping current details", + -1, vpc); + if ("{}".equals(currentDetails)) { + vpcDetailsDao.addDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); + } + return; + } + if (output.isEmpty()) { + output = "{}".equals(currentDetails) ? "{}" : currentDetails; + } + if (!output.equals(currentDetails)) { + logger.info("VPC extension device updated for VPC {}: {}", vpc, output); + vpcDetailsDao.addDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, output, false); + } else { + logger.debug("VPC extension device unchanged for VPC {}: {}", vpc, output); + } + } catch (Exception e) { + logger.warn("Failed ensure-network-device for VPC {}: {}", vpc, e.getMessage()); + if ("{}".equals(currentDetails)) { + vpcDetailsDao.addDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); + } + } + } + + /** + * Executes the extension script for a VPC-level command (no tier network required). + * Uses VPC-level details from {@code vpc_details}. + */ + protected boolean executeVpcScript(Vpc vpc, String command, JsonObject argsPayload) { + return executeVpcScriptAndReturnOutput(vpc, command, argsPayload).first() == EXIT_CODE_SUCCESS; + } + + protected Pair executeVpcScriptAndReturnOutput(Vpc vpc, String command, JsonObject argsPayload) { + Pair physNetAndExt = resolveExtensionForVpc(vpc); + if (physNetAndExt == null) { + logger.warn("executeVpcScript: no extension found for VPC {} zone {}", vpc, vpc.getZoneId()); + return new Pair<>(EXIT_CODE_FAILURE, "No extension found for VPC " + vpc.getUuid()); + } + Long physicalNetworkId = physNetAndExt.first(); + Extension extension = physNetAndExt.second(); + File scriptFile = resolveScriptFileForVpc(physicalNetworkId, extension); + try { + JsonObject payload = buildVpcScriptPayload(vpc, argsPayload, physicalNetworkId, extension); + return executeScriptWithFilePayload(scriptFile, command, payload, "VPC extension"); + } catch (Exception e) { + logger.error("Failed to execute VPC extension script {}: {}", command, e.getMessage(), e); + throw new CloudRuntimeException("Failed to execute VPC extension script: " + command, e); + } + } + + private JsonObject buildVpcScriptPayload(Vpc vpc, JsonObject argsPayload, Long physicalNetworkId, Extension extension) { + JsonObject payload = new JsonObject(); + payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS, + buildPhysicalNetworkExtensionDetailsPayload(physicalNetworkId, extension)); + payload.add(ARG_NETWORK_EXTENSION_DETAILS, buildVpcExtensionDetailsPayload(vpc.getId())); + payload.add(ARG_PAYLOAD, argsPayload != null ? argsPayload : new JsonObject()); + return payload; + } + + protected PublicIpAddress getVpcSourceNatIp(long vpcId) { + final List ips = ipAddressDao.listByAssociatedVpc(vpcId, true); + if (CollectionUtils.isEmpty(ips)) { + return null; + } + IPAddressVO selected = null; + for (final IPAddressVO ip : ips) { + if (ip.getState() != IpAddress.State.Releasing) { + selected = ip; + break; + } + } + if (selected == null) { + selected = ips.get(0); + } + + final VlanVO vlan = vlanDao.findById(selected.getVlanId()); + if (vlan == null) { + logger.warn("No VLAN found for VPC source NAT IP {} (vpc={})", selected.getAddress(), vpcId); + return null; + } + return PublicIp.createFromAddrAndVlan(selected, vlan); + } + + /** + * Implements the VPC by: + *
    + *
  1. Calling {@link #ensureExtensionDetails(Vpc)} to select a host and + * save the VPC-level details (does not use any anchor tier network).
  2. + *
  3. Calling the script's {@code implement-vpc} command to create the VPC + * namespace and VPC-level networking state.
  4. + *
  5. Applying VPC source NAT if a source-NAT IP already exists (the script's + * {@code assign-ip} sets up the public veth + SNAT rule for the VPC CIDR).
  6. + *
+ */ + @Override + public boolean implementVpc(Vpc vpc, DeployDestination dest, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + + // Step 1: Ensure a VPC extension device is selected and details saved at VPC level. + ensureExtensionDetails(vpc); + + // Step 2: Create the VPC namespace (no anchor tier network needed). + JsonObject implPayload = new JsonObject(); + addVpcToPayload(implPayload, vpc); + + // Include source NAT IP if already allocated, so the script can set up the + // VPC-level SNAT rule for the entire VPC CIDR. + final PublicIpAddress sourceNatIp = getVpcSourceNatIp(vpc.getId()); + if (sourceNatIp != null) { + addPublicIpToPayload(implPayload, sourceNatIp.getId(), true); + } + + return executeVpcScript(vpc, CMD_IMPLEMENT_VPC, implPayload); + } + + /** + * Shuts down the VPC by: + *
    + *
  1. Calling {@code destroy-network} for each extension-backed VPC tier (removes + * tier resources but preserves the shared VPC namespace).
  2. + *
  3. Calling {@code shutdown-vpc} to remove the VPC namespace and state after + * all tiers have been cleaned up.
  4. + *
+ */ + @Override + public boolean shutdownVpc(Vpc vpc, ReservationContext context) + throws ConcurrentOperationException, ResourceUnavailableException { + final List networks = networkModel.listNetworksByVpc(vpc.getId()); + + boolean result = true; + if (networks != null) { + for (final Network network : networks) { + if (!canHandle(network, null)) { + continue; + } + + final JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + + final boolean tierResult = executeScript(network, CMD_DESTROY_NETWORK, payload); + result = result && tierResult; + } + } + + // Remove the VPC namespace and VPC-level details regardless of tier result. + JsonObject vpcPayload = new JsonObject(); + addVpcToPayload(vpcPayload, vpc); + boolean vpcResult = executeVpcScript(vpc, CMD_SHUTDOWN_VPC, vpcPayload); + if (vpcResult) { + try { + vpcDetailsDao.removeDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS); + } catch (Exception e) { + logger.warn("Failed to remove VPC extension details for VPC {}: {}", vpc, e.getMessage()); + } + } + result = result && vpcResult; + + return result; + } + + @Override + public boolean createPrivateGateway(PrivateGateway gateway) + throws ConcurrentOperationException, ResourceUnavailableException { + throw new UnsupportedOperationException("Private gateways are not supported by the network extension element."); + } + + /** Private gateways are not supported by the network extension element. */ + @Override + public boolean deletePrivateGateway(PrivateGateway gateway) + throws ConcurrentOperationException, ResourceUnavailableException { + throw new UnsupportedOperationException("Private gateways are not supported by the network extension element."); + } + + /** Static routes are not supported by the network extension element. */ + @Override + public boolean applyStaticRoutes(Vpc vpc, List routes) + throws ResourceUnavailableException { + throw new UnsupportedOperationException("Static routes are not supported by the network extension element."); + } + + /** ACL items on private gateways are not supported by the network extension element. */ + @Override + public boolean applyACLItemsToPrivateGw(PrivateGateway gateway, List rules) + throws ResourceUnavailableException { + throw new UnsupportedOperationException("ACL items on private gateways are not supported by the network extension element."); + } + + @Override + public boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address) { + if (vpc == null || address == null || address.getAddress() == null) { + logger.warn("updateVpcSourceNatIp: invalid input (vpc={}, address={})", vpc, address); + return false; + } + + final JsonObject payload = new JsonObject(); + addVpcToPayload(payload, vpc); + addPublicIpToPayload(payload, address.getId(), true); + + final boolean result = executeVpcScript(vpc, CMD_UPDATE_VPC_SOURCE_NAT_IP, payload); + if (!result) { + logger.warn("updateVpcSourceNatIp: failed to update source NAT IP for VPC {} to {}", + vpc, address.getAddress().addr()); + } + return result; + } + + /** + * Applies VPC network ACL rules for a VPC tier network via the script's + * {@code apply-network-acl} command. Rules are serialised as a JSON array + * and passed via a temporary payload file. + * + *

Script command: {@code apply-network-acl}

+ */ + @Override + public boolean applyNetworkACLs(Network config, List rules) + throws ResourceUnavailableException { + if (!canHandle(config, Service.NetworkACL)) { + return true; + } + + // Rebuild the ACL chain from all non-revoked rules. + List activeRules = rules == null ? List.of() : + rules.stream() + .filter(r -> r.getState() != NetworkACLItem.State.Revoke) + .collect(Collectors.toList()); + + logger.info("applyNetworkACLs: network={} activeRules={}", config, activeRules.size()); + + JsonArray aclRules = buildAclRulesArray(activeRules); + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, config); + payload.add("acl_rules", aclRules); + + boolean result = executeScript(config, CMD_APPLY_NETWORK_ACL, payload); + if (!result) { + throw new ResourceUnavailableException( + "Failed to apply network ACL rules for network " + config.getUuid(), + Network.class, config.getId()); + } + return true; + } + + /** + * Re-applies ACL rules for all extension-backed networks in a VPC after a rule reorder. + * Calls {@code apply-network-acl} for each affected network with the full ACL item list. + */ + @Override + public boolean reorderAclRules(Vpc vpc, List networks, + List networkACLItems) { + if (CollectionUtils.isEmpty(networks)) { + return true; + } + + List activeRules = networkACLItems == null ? List.of() : + networkACLItems.stream() + .filter(r -> r.getState() != NetworkACLItem.State.Revoke) + .collect(Collectors.toList()); + + boolean result = true; + for (Network network : networks) { + if (!canHandle(network, Service.NetworkACL)) { + continue; + } + try { + JsonArray aclRules = buildAclRulesArray(activeRules); + + JsonObject payload = new JsonObject(); + addNetworkToPayload(payload, network); + payload.add("acl_rules", aclRules); + + boolean r = executeScript(network, CMD_APPLY_NETWORK_ACL, payload); + result = result && r; + } catch (Exception e) { + logger.warn("reorderAclRules: failed for network {}: {}", network, e.getMessage()); + result = false; + } + } + return result; + } + + /** + * Serialises a list of {@link NetworkACLItem}s to a {@link JsonArray} + * suitable for passing to the {@code apply-network-acl} script command. + * Rules are sorted by their number (priority order). + */ + private JsonArray buildAclRulesArray(List rules) { + JsonArray array = new JsonArray(); + List sorted = rules.stream() + .sorted(java.util.Comparator.comparingInt(NetworkACLItem::getNumber)) + .collect(Collectors.toList()); + for (NetworkACLItem rule : sorted) { + JsonObject ruleObj = buildAclRuleObject(rule); + JsonArray sourceCidrsArray = new JsonArray(); + List sourceCidrs = rule.getSourceCidrList(); + if (CollectionUtils.isNotEmpty(sourceCidrs)) { + for (String cidr : sourceCidrs) sourceCidrsArray.add(cidr); + } + ruleObj.add("sourceCidrs", sourceCidrsArray); + array.add(ruleObj); + } + return array; + } + + private JsonObject buildAclRuleObject(NetworkACLItem rule) { + JsonObject ruleObj = new JsonObject(); + ruleObj.addProperty("number", rule.getNumber()); + ruleObj.addProperty("action", rule.getAction().name().toLowerCase()); + ruleObj.addProperty("trafficType", rule.getTrafficType().name().toLowerCase()); + ruleObj.addProperty("protocol", safeStr(rule.getProtocol())); + if (rule.getSourcePortStart() != null) ruleObj.addProperty("portStart", rule.getSourcePortStart()); + if (rule.getSourcePortEnd() != null) ruleObj.addProperty("portEnd", rule.getSourcePortEnd()); + if (rule.getIcmpType() != null) ruleObj.addProperty("icmpType", rule.getIcmpType()); + if (rule.getIcmpCode() != null) ruleObj.addProperty("icmpCode", rule.getIcmpCode()); + return ruleObj; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md new file mode 100644 index 000000000000..56197e5fc8cd --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md @@ -0,0 +1,1493 @@ + + +# Network Extension Script Protocol + +This document describes the complete interface between Apache CloudStack's +`NetworkExtensionElement` and the external script (Bash, Python, or any +executable) that implements network services for a custom device. + +Any executable that handles the commands listed below can be registered as a +**NetworkOrchestrator extension** and used as the provider for one or more +CloudStack network services (DHCP, DNS, UserData, SourceNat, StaticNat, +PortForwarding, Firewall, Lb, NetworkACL, Gateway). + +The reference implementation is the `network-namespace` extension at +`extensions/network-namespace/`, which uses Linux network namespaces on KVM +hosts. Use it as a working example. + +--- + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Script Placement Convention](#script-placement-convention) +3. [CloudStack Setup Steps](#cloudstack-setup-steps) +4. [Always-present payload fields](#always-present-payload-fields) +5. [Shared payload fields](#shared-payload-fields) +6. [Command Reference](#command-reference) + - [ensure-network-device](#ensure-network-device) + - [implement-network](#implement-network) + - [shutdown-network](#shutdown-network) + - [destroy-network](#destroy-network) + - [implement-vpc](#implement-vpc) + - [shutdown-vpc](#shutdown-vpc) + - [update-vpc-source-nat-ip](#update-vpc-source-nat-ip) + - [assign-ip / release-ip](#assign-ip--release-ip) + - [add-static-nat / delete-static-nat](#add-static-nat--delete-static-nat) + - [add-port-forward / delete-port-forward](#add-port-forward--delete-port-forward) + - [apply-fw-rules](#apply-fw-rules) + - [apply-network-acl](#apply-network-acl) + - [prepare-nic / release-nic](#prepare-nic--release-nic) + - [add-dhcp-entry / remove-dhcp-entry](#add-dhcp-entry--remove-dhcp-entry) + - [config-dhcp-subnet / remove-dhcp-subnet](#config-dhcp-subnet--remove-dhcp-subnet) + - [set-dhcp-options](#set-dhcp-options) + - [add-dns-entry](#add-dns-entry) + - [config-dns-subnet / remove-dns-subnet](#config-dns-subnet--remove-dns-subnet) + - [save-vm-data](#save-vm-data) + - [save-password](#save-password) + - [save-userdata](#save-userdata) + - [save-sshkey](#save-sshkey) + - [save-hypervisor-hostname](#save-hypervisor-hostname) + - [apply-lb-rules](#apply-lb-rules) + - [restore-network](#restore-network) + - [custom-action](#custom-action) +7. [Service-to-Command Mapping](#service-to-command-mapping) +8. [Capabilities Configuration](#capabilities-configuration) +9. [VPC Networks](#vpc-networks) +10. [Extension IP](#extension-ip) +11. [Exit Codes](#exit-codes) +12. [Minimal Script Skeleton](#minimal-script-skeleton) + +--- + +## Architecture Overview + +``` +CloudStack Management Server + │ + │ exec /.sh + │ payload-file contains JSON for the command invocation + ▼ + Your Script (Bash / Python / Go / …) + │ + │ configures / queries your device: + │ • KVM host over SSH + │ • SDN controller REST API + │ • Hardware appliance CLI + │ • Cloud provider API + ▼ + External Network Device +``` + +CloudStack calls the script synchronously (blocking process execution) on the +**management server** for every network event. The script is responsible for +translating those events into configuration changes on the actual device. + +The script must: + +- **Exit 0** on success. +- **Exit non-zero** on failure (CloudStack will log the error and may retry). +- For `ensure-network-device` only, **print a single-line JSON object** to + stdout (see [ensure-network-device](#ensure-network-device)). + +All other commands must produce no output on stdout (any output is logged at +DEBUG level and ignored). + +--- + +## Script Placement Convention + +CloudStack resolves the executable in this order (first match wins): + +1. **`/.sh`** — preferred convention. + Example: extension named `my-sdn` → script at + `.../my-sdn/my-sdn.sh`. +2. **`` itself**, if it is a regular file and is executable. + +The `` is the `path` field returned by `listExtensions` after +the extension is created. CloudStack sets it to: + +``` +/usr/share/cloudstack-management/extensions// +``` + +> **Tip:** Your script does not have to live on the management server — it can +> be a thin proxy that SSHes into a remote appliance. The +> `network-namespace.sh` entry-point is exactly that: it SSHes into the target +> KVM host and calls the wrapper script there. + +--- + +## CloudStack Setup Steps + +### Step 1 – Create the Extension + +```bash +cmk createExtension \ + name=my-sdn \ + type=NetworkOrchestrator \ + details[0].network.services="SourceNat,StaticNat,PortForwarding,Firewall,Lb,Dhcp,Dns,UserData,CustomAction" \ + details[1].network.service.capabilities="$(cat my-sdn-capabilities.json)" \ + details[2].network.isolation.method="NetworkExtension" +``` + +`network.service.capabilities` is a JSON object — see +[Capabilities Configuration](#capabilities-configuration). + +### Step 2 – Deploy the Script + +Copy your executable to the path reported by `listExtensions`: + +```bash +SCRIPT_PATH=$(cmk listExtensions name=my-sdn | jq -r '.[0].path') +# e.g. /usr/share/cloudstack-management/extensions/my-sdn/ +mkdir -p "${SCRIPT_PATH}" +cp my-sdn.sh "${SCRIPT_PATH}/my-sdn.sh" +chmod 755 "${SCRIPT_PATH}/my-sdn.sh" +``` + +If you have multiple management servers, deploy the script to **every** one. + +### Step 3 – Register the Extension to a Physical Network + +```bash +PHYS_ID=$(cmk listPhysicalNetworks | jq -r '.[0].id') +EXT_ID=$(cmk listExtensions name=my-sdn | jq -r '.[0].id') + +cmk registerExtension \ + id=${EXT_ID} \ + resourcetype=PhysicalNetwork \ + resourceid=${PHYS_ID} \ + details[0].hosts="192.168.1.10,192.168.1.11" \ + details[1].username="root" \ + details[2].password="s3cr3t" +``` + +To update the registration details later, use `updateRegisteredExtension`: + +```bash +cmk updateRegisteredExtension \ + extensionid=${EXT_ID} \ + resourcetype=PhysicalNetwork \ + resourceid=${PHYS_ID} \ + details[0].hosts="192.168.1.10,192.168.1.11" \ + details[1].username="root" \ + details[2].password="s3cr3t" +``` + +Any key/value pairs you pass here are stored with the physical-network +registration as extension metadata. The `custom-action` path embeds them +directly into the payload file under `physical-network-extension-details`. +The schema is entirely yours — CloudStack treats it as opaque. + +> **`network.isolation.method=NetworkExtension`** must be set as an Extension +> detail (via `createExtension` or `updateExtension`). +> This is required whenever your `implement-network` handler outputs JSON that +> updates the network's broadcast domain type. +> Without it the script output is accepted but silently ignored — the network +> record in the CloudStack database is not updated. + +### Step 4 – Enable the Network Service Provider + +```bash +NSP_ID=$(cmk listNetworkServiceProviders physicalnetworkid=${PHYS_ID} \ + name=my-sdn | jq -r '.[0].id') +cmk updateNetworkServiceProvider id=${NSP_ID} state=Enabled +``` + +### Step 5 – Create a Network Offering + +```bash +cmk createNetworkOffering \ + name="My-SDN-Offering" \ + displaytext="My SDN network offering" \ + guestiptype=Isolated \ + traffictype=GUEST \ + supportedservices="Dhcp,Dns,UserData,SourceNat,StaticNat,PortForwarding,Firewall,Lb" \ + serviceProviderList[0].service=Dhcp serviceProviderList[0].provider=my-sdn \ + serviceProviderList[1].service=Dns serviceProviderList[1].provider=my-sdn \ + serviceProviderList[2].service=UserData serviceProviderList[2].provider=my-sdn \ + serviceProviderList[3].service=SourceNat serviceProviderList[3].provider=my-sdn \ + serviceProviderList[4].service=StaticNat serviceProviderList[4].provider=my-sdn \ + serviceProviderList[5].service=PortForwarding serviceProviderList[5].provider=my-sdn \ + serviceProviderList[6].service=Firewall serviceProviderList[6].provider=my-sdn \ + serviceProviderList[7].service=Lb serviceProviderList[7].provider=my-sdn \ + serviceCapabilityList[0].service=SourceNat \ + serviceCapabilityList[0].capabilitytype=SupportedSourceNatTypes \ + serviceCapabilityList[0].capabilityvalue=peraccount +cmk updateNetworkOffering id= state=Enabled +``` + +--- + +## Always-present payload fields + +For all standard network / VPC commands, CloudStack now executes the script as: + +```text +/.sh +``` + +`payload-file` contains a JSON object with this envelope: + +```json +{ + "physical-network-extension-details": {}, + "network-extension-details": {}, + "payload": {} +} +``` + +| Field | Value | +| --- | --- | +| `physical-network-extension-details` | Physical-network extension metadata registered on the physical network, enriched with `physicalnetworkname`. | +| `network-extension-details` | Additional network or VPC details stored in CloudStack and forwarded as a JSON object. | +| `payload` | Command-specific JSON object for the command being executed. | + +`timeout-seconds` is currently `60`. + +> **Important:** `custom-action` is still the exception in shape. It uses its +> own top-level payload structure and does **not** wrap command-specific fields +> under a nested `payload` object. Use top-level `action-params` for +> command-specific parameters. + +--- + +## Shared payload fields + +The following names appear repeatedly inside the nested `payload` object. + +### Network-level fields (added by `addNetworkToPayload`) + +| Field | Description | +| --- | --- | +| `network_id` | CloudStack numeric network ID. | +| `vlan` | Guest VLAN tag (for example `100`). Extracted from the broadcast URI. May be empty for flat networks. | +| `zone_id` | CloudStack zone ID. | +| `guest_type` | Guest network type: `"isolated"`, `"shared"`, or `"l2"`. Scripts should use this to skip NAT/firewall operations that are not applicable to Shared or L2 networks. | +| `network_state` | Guest network state: `"allocated"`, `"setup"`, `"implementing"`, `"implemented"`, `"shutdown"` or `"destroy"`. | +| `gateway` | Guest network gateway (for example `10.0.0.1`). Omitted when blank. | +| `cidr` | Guest network CIDR (for example `10.0.0.0/24`). Omitted when blank. | +| `vpc_id` | CloudStack numeric VPC ID. Present for VPC tier networks and VPC-scoped commands. | +| `network_ip6_gateway` | Guest network IPv6 gateway, when the network has IPv6 configured. | +| `network_ip6_cidr` | Guest network IPv6 CIDR, when the network has IPv6 configured. | + +### NIC-level fields (added by `addNicToPayload`) + +| Field | Description | +| --- | --- | +| `nic_id` | CloudStack numeric NIC ID. | +| `nic_uuid` | NIC UUID — matches `external_ids:iface-id` written by the KVM agent for OVN port binding. | +| `mac` | VM NIC MAC address. | +| `ip` | VM NIC IPv4 address. | +| `gateway` | VM NIC IPv4 gateway (NIC-level; equals the network gateway for normal guest networks). | +| `netmask` | VM NIC IPv4 netmask (for example `255.255.255.0`). | +| `default_nic` | Stringified boolean — `"false"` for secondary NICs. | +| `device_id` | NIC device index in the VM (slot number). | +| `ip6_address` | VM NIC IPv6 address, when the NIC has IPv6 configured. | +| `ip6_gateway` | VM NIC IPv6 gateway, when available. | +| `ip6_cidr` | VM NIC IPv6 CIDR, when available. | + +### Public-IP fields (added by `addPublicIpToPayload`) + +| Field | Description | +| --- | --- | +| `public_ip` | A public IP address. | +| `public_vlan` | VLAN tag of the public IP segment. | +| `public_gateway` | Gateway of the public IP segment. | +| `public_cidr` | CIDR of the public IP (for example `203.0.113.0/24`). | +| `source_nat` | Stringified boolean (`"true"` / `"false"`) indicating whether the public IP is the source-NAT IP. | +| `private_ip` | A VM's private guest-network IP address (NAT target). | + +### DNS / extension-IP fields + +| Field | Description | +| --- | --- | +| `extension_ip` | The IP the extension device uses on the guest side. Equals the gateway when SourceNat/Gateway is provided; otherwise it is a dedicated IP from the guest subnet. | +| `dns` | Comma-separated DNS server list. | +| `domain` | Network domain suffix. | + +--- + +## Command Reference + +### `ensure-network-device` + +**Called:** Before every network operation, automatically by +`NetworkExtensionElement`. + +**Purpose:** Select (or re-validate) the device/host that will handle this +network. Perform failover to another host if the current one is unreachable. +The returned JSON is stored in `network_details` under key `extension.details` and +passed back to later `ensure-network-device` calls as `payload.current_details`. + +**Payload file shape:** + +```json +{ + "physical-network-extension-details": {}, + "network-extension-details": {}, + "payload": { + "network_id": "42", + "vlan": "100", + "zone_id": "1", + "vpc_id": "7", + "current_details": "{}" + } +} +``` + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. Omitted for VPC-level calls. | +| `vlan` | Guest VLAN. Present only for network-level calls. | +| `zone_id` | CloudStack zone ID. | +| `vpc_id` | VPC ID for VPC-level calls, and also present for VPC tier networks. | +| `current_details` | Previously stored `extension.details` JSON string (`{}` on first call). | + +**Stdout:** A single-line JSON object. CloudStack stores this verbatim. +You can put any fields your script needs (host selection, device ID, segment +ID, namespace name, etc.). + +```json +{"host":"192.168.1.10","device_id":"vrf-42","namespace":"cs-net-42"} +``` + +**Exit 0:** JSON written to stdout is persisted. +**Exit non-zero:** Existing details are kept unchanged; a warning is logged. + +--- + +### `implement-network` + +**Called:** When a network is implemented (first VM deployed, or network +restart). + +**Purpose:** Create / bring up the network segment on the device: create the +virtual segment (VRF, namespace, VLAN, …), attach the guest interface, and +configure the gateway. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `extension_ip` | Device IP on the guest network. | +| `vpc_id` | Present for VPC tier networks. | + +> **IPv6 note:** For network-scoped commands that already include `gateway`/`cidr`, +> CloudStack now also includes `network_ip6_gateway` and `network_ip6_cidr` +> when the guest network has IPv6 configured. + +**Stdout (required when `network.isolation.method=NetworkExtension`):** + +When the Extension detail `network.isolation.method=NetworkExtension` is set, +CloudStack selects `NetworkExtensionGuestNetworkGuru` and applies the JSON object +printed to stdout back to the network record. The following two fields **must** +be present in the output so that CloudStack stores the correct broadcast type and +URI — without them the KVM agent (`OvsVifDriver`) will not set +`external_ids:iface-id` on the OVS tap port and OVN port-binding will fail: + +| Output key | Required value | Description | +| --- | --- | --- | +| `network.broadcast_domain_type` | `"Lswitch"` (OVN) or appropriate type | Sets `BroadcastDomainType` on the network record. | +| `network.broadcast_uri` | e.g. `"ovn://cs-net-"` | Sets the broadcast URI used by the hypervisor agent. | + +Example stdout: + +```json +{"network.broadcast_domain_type": "Lswitch", "network.broadcast_uri": "ovn://cs-net-42"} +``` + +> **Note:** These fields are only consumed when the Extension detail +> `network.isolation.method=NetworkExtension` is set. Without it, the script +> output is accepted but the network record is **not** updated (the update is +> silently skipped). + +--- + +### `shutdown-network` + +**Called:** When a network is shut down (e.g., all VMs removed, before +deletion). + +**Purpose:** Tear down the network segment; release resources. The +per-network `extension.details` blob is removed from CloudStack after a successful +return. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `destroy-network` + +**Called:** When a network is permanently deleted. + +**Purpose:** Same as `shutdown-network`, but called at hard-delete time. The +placeholder NIC IP (if any) and the `extension.details` blob are cleaned up +automatically by CloudStack after a successful return. + +**Payload fields (`payload` object):** Identical to `shutdown-network`. + +--- + +### `implement-vpc` + +**Called:** When a VPC is implemented (before any tier is set up). + +**Purpose:** Create the VPC-level networking state on the device (e.g., a +shared namespace or VRF that all tiers will attach to). If a source-NAT IP +is already allocated for the VPC, its details are also included so the script +can set up the VPC-level SNAT rule at this stage. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `vpc_id` | VPC ID. | +| `vpc_cidr` | VPC supernet CIDR. | +| `public_ip` | Source-NAT IP, when already allocated. | +| `public_vlan` | VLAN of the source-NAT IP, when present. | +| `public_gateway` | Gateway of the source-NAT IP segment, when present. | +| `public_cidr` | CIDR of the source-NAT IP, when present. | +| `source_nat` | `"true"` when the public IP fields are present. | + +--- + +### `shutdown-vpc` + +**Called:** During `shutdownVpc` after all tier networks have been destroyed. + +**Purpose:** Remove the VPC-level namespace / VRF and all associated state. +The `extension.details` blob is removed from CloudStack after a successful return. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `vpc_id` | VPC ID. | + +--- + +### `update-vpc-source-nat-ip` + +**Called:** When the VPC source-NAT IP changes +(`updateVpcSourceNatIp` API). + +**Purpose:** Update the VPC-level SNAT rule to point to the new public IP. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `vpc_id` | VPC ID. | +| `vpc_cidr` | VPC supernet CIDR. | +| `public_ip` | New source-NAT IP. | +| `public_vlan` | VLAN of the new source-NAT IP. | +| `public_gateway` | Gateway of the new source-NAT IP segment. | +| `public_cidr` | CIDR of the new source-NAT IP. | +| `source_nat` | Always `"true"`. | + +--- + +### `assign-ip` / `release-ip` + +**Called:** When a public IP is associated with or disassociated from a +network (source NAT, static NAT, PF, LB allocation). + +**Purpose:** +- `assign-ip` — attach the public IP to the device; add the necessary routing + entry so the device can receive traffic for this IP. +- `release-ip` — detach the public IP; remove routing. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN. | +| `public_ip` | The public IP being assigned or released. | +| `source_nat` | `"true"` if this is the source NAT IP. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `public_gateway` | Gateway of the public IP segment. | +| `public_cidr` | CIDR of the public IP. | +| `public_vlan` | Public VLAN tag. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `add-static-nat` / `delete-static-nat` + +**Called:** When a static NAT rule is created or deleted +(`enableStaticNat` / `disableStaticNat` API). + +**Purpose:** Configure a 1:1 bidirectional NAT mapping between a public IP +and a VM private IP. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `public_ip` | Public IP. | +| `public_cidr` | Public IP CIDR. | +| `public_vlan` | Public VLAN tag. | +| `private_ip` | VM private IP (DNAT destination). | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `add-port-forward` / `delete-port-forward` + +**Called:** When a port forwarding rule is created or deleted +(`createPortForwardingRule` / `deletePortForwardingRule` API). + +**Purpose:** Configure a DNAT rule from `public-ip:public-port` to +`private-ip:private-port`. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `public_ip` | Public IP. | +| `public_cidr` | Public IP CIDR. | +| `public_vlan` | Public VLAN tag. | +| `public_port` | Port range on the public IP, for example `22` or `8080-8090`. | +| `private_ip` | VM private IP. | +| `private_port` | Destination port range on the VM. | +| `protocol` | Protocol such as `tcp` or `udp`. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `apply-fw-rules` + +**Called:** When any firewall rule is created, deleted, or updated for the +network (`createFirewallRule`, `deleteFirewallRule`, `updateEgressFirewallRule` +APIs, and during network restart). + +**Purpose:** Rebuild the entire firewall policy for the network from scratch. +CloudStack calls this with a *narrow* scope (one IP's ingress rules or egress +rules per call), but the `fw_rules` payload field always contains **all** active +rules for the network, so a full rebuild is always safe. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `fw_rules` | JSON object containing the firewall payload shown below. | +| `vpc_id` | Present for VPC tier networks. | + +**`fw_rules` JSON:** + +```json +{ + "default_egress_allow": true, + "cidr": "10.0.0.0/24", + "rules": [ + { + "id": 1, + "type": "ingress", + "protocol": "tcp", + "portStart": 22, + "portEnd": 22, + "publicIp": "203.0.113.5", + "sourceCidrs": ["0.0.0.0/0"], + "destCidrs": [] + }, + { + "id": 2, + "type": "egress", + "protocol": "icmp", + "icmpType": -1, + "icmpCode": -1, + "sourceCidrs": [], + "destCidrs": [] + } + ] +} +``` + +| Field | Description | +| --- | --- | +| `default_egress_allow` | `true` = permissive egress by default (explicit rules are deny rules); `false` = restrictive (explicit rules are allow rules). | +| `cidr` | Guest network CIDR. | +| `rules[].type` | `"ingress"` or `"egress"`. | +| `rules[].protocol` | `"tcp"`, `"udp"`, `"icmp"`, `"all"`. | +| `rules[].portStart` / `portEnd` | TCP/UDP port range (absent for ICMP/all). | +| `rules[].icmpType` / `icmpCode` | ICMP type/code (`-1` = any; absent for TCP/UDP). | +| `rules[].publicIp` | For ingress: the public IP the rule applies to. | +| `rules[].sourceCidrs` | Allowed source IP ranges (ingress: external; egress: VM). | +| `rules[].destCidrs` | Allowed destination IP ranges (egress only). | + +--- + +### `apply-network-acl` + +**Called:** When a VPC network ACL is applied to a tier network +(`createNetworkACLList`, `replaceNetworkACLList`, `updateNetworkACLItem`, +`moveNetworkAclItem` APIs, and during network restart). + +**Purpose:** Rebuild the entire ACL policy for the VPC tier from scratch. +Rules are applied in ascending `number` order. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `acl_rules` | JSON array of ACL rules shown below. | +| `vpc_id` | VPC ID. Always present for VPC tiers. | + +**`acl_rules` JSON:** + +```json +[ + { + "number": 10, + "action": "allow", + "trafficType": "ingress", + "protocol": "tcp", + "portStart": 22, + "portEnd": 22, + "sourceCidrs": ["0.0.0.0/0"] + }, + { + "number": 20, + "action": "deny", + "trafficType": "egress", + "protocol": "all", + "sourceCidrs": [] + } +] +``` + +| Field | Description | +| --- | --- | +| `number` | Rule priority (lower number = higher priority). | +| `action` | `"allow"` or `"deny"`. | +| `trafficType` | `"ingress"` or `"egress"`. | +| `protocol` | `"tcp"`, `"udp"`, `"icmp"`, `"all"`. | +| `portStart` / `portEnd` | TCP/UDP port range (absent for ICMP/all). | +| `icmpType` / `icmpCode` | ICMP type/code (absent for TCP/UDP). | +| `sourceCidrs` | Source CIDR filter list. | + +--- + +### `prepare-nic` / `release-nic` + +**Called:** On every NIC attach (`prepare`) and detach (`release`) regardless +of which services the extension provides. + +**Purpose:** +- `prepare-nic` — set up per-NIC state before the VM boots: create the port + binding on the device (OVN `Logical_Switch_Port`, dnsmasq entry, …). +- `release-nic` — tear down per-NIC state after the VM is destroyed: remove the + port binding and associated metadata. + +These commands fire for **all** NICs on extension-managed networks, not just +those belonging to DHCP/DNS-enabled offerings. + +**`prepare-nic` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `mac` | VM NIC MAC address. | +| `ip` | VM NIC IPv4 address. | +| `nic_ip6_address` | NIC IPv6 address, when configured. | +| `nic_ip6_gateway` | NIC IPv6 gateway, when available. | +| `nic_ip6_cidr` | NIC IPv6 CIDR, when available. | +| `nic_uuid` | NIC UUID — matches `external_ids:iface-id` written by the KVM agent for OVN port binding. | +| `default_nic` | Stringified boolean — `"false"` for secondary NICs. | +| `hostname` | VM hostname. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | + +**`release-nic` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `mac` | VM NIC MAC address. | +| `ip` | VM NIC IPv4 address. | +| `nic_ip6_address` | NIC IPv6 address, when configured. | +| `nic_ip6_gateway` | NIC IPv6 gateway, when available. | +| `nic_ip6_cidr` | NIC IPv6 CIDR, when available. | +| `nic_uuid` | NIC UUID. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `add-dhcp-entry` / `remove-dhcp-entry` + +**Called:** When a VM NIC is reserved (`add`) or released (`remove`) on a +network whose DHCP service is provided by this extension. + +**Purpose:** Add or remove a static DHCP lease for the VM. + +> **IPv6 note:** For NIC-scoped commands (`add/remove-dhcp-entry`, +> `add-dns-entry`, `save-vm-data`, `save-password`, `save-userdata`, +> `save-sshkey`, `save-hypervisor-hostname`), CloudStack includes +> `nic_ip6_address`, `nic_ip6_gateway`, and `nic_ip6_cidr` when the NIC +> has IPv6 information. + +**`add-dhcp-entry` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `mac` | VM NIC MAC address, for example `02:00:00:00:00:01`. | +| `ip` | VM assigned IP. | +| `hostname` | VM hostname. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `default_nic` | Stringified boolean indicating whether this NIC is the default NIC. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +**`remove-dhcp-entry` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `mac` | VM NIC MAC address. | +| `ip` | VM assigned IP. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `config-dhcp-subnet` / `remove-dhcp-subnet` + +**Called:** When a shared-network subnet is configured or removed. + +**Purpose:** Configure the DHCP scope (pool, gateway, DNS) for a subnet +without tying it to a specific VM. + +**`config-dhcp-subnet` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `vlan` | Guest VLAN tag. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +**`remove-dhcp-subnet` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `set-dhcp-options` + +**Called:** When extra DHCP options are set on a NIC +(`updateNicExtraDhcpOption` API). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `nic_id` | CloudStack NIC ID. | +| `options` | Compact JSON string such as `{"15":"example.com","119":"search.example.com"}`. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `add-dns-entry` + +**Called:** When a VM NIC is reserved on a network whose DNS service is +provided by this extension. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `hostname` | VM hostname. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `config-dns-subnet` / `remove-dns-subnet` + +**Called:** When a DNS scope is configured or removed for a subnet. + +**`config-dns-subnet` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `vlan` | Guest VLAN tag. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +**`remove-dns-subnet` payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `save-vm-data` + +**Called:** When a VM is deployed or updated with user data, SSH keys, or +password on a network whose UserData service is provided by this extension. + +**Purpose:** Store the complete cloud-init metadata set (user-data, +meta-data/*, password) for the VM so the metadata HTTP server can serve it. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC on this network. | +| `extension_ip` | Extension IP. | +| `vm_data` | JSON array shown below. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +**`vm_data` JSON:** + +```json +[ + {"dir":"userdata", "file":"user-data", "content":""}, + {"dir":"meta-data", "file":"instance-id", "content":""}, + {"dir":"meta-data", "file":"local-hostname", "content":""}, + {"dir":"meta-data", "file":"public-keys/0/openssh-key", "content":""}, + {"dir":"password", "file":"vm_password", "content":""} +] +``` + +Each `content` field is a plain UTF-8 string. Write it directly to disk. + +Your metadata HTTP server should serve each entry at: +`http:///latest//` + +--- + +### `save-password` + +**Called:** When a password reset is requested for a VM +(`resetPasswordForVirtualMachine` API). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `password` | Plain-text new password. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `save-userdata` + +**Called:** When a VM's user data is updated +(`updateVirtualMachine` with `userdata`). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `userdata` | User-data as plain text. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `save-sshkey` + +**Called:** When an SSH public key is reset for a VM +(`resetSSHKeyForVirtualMachine` API). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `sshkey` | SSH public key (plain text). | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `save-hypervisor-hostname` + +**Called:** When a VM is deployed and UserData service is active. + +**Purpose:** Store the hypervisor hostname in the metadata so VMs can identify +which host they run on (cloud-init `availability-zone` / host detection). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `hypervisor_hostname` | Hypervisor node hostname. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | + +--- + +### `apply-lb-rules` + +**Called:** When a load balancer rule is created, deleted, or its members +change (`createLoadBalancerRule`, `deleteLoadBalancerRule`, +`assignToLoadBalancerRule`, `removeFromLoadBalancerRule` APIs). + +**Purpose:** Configure the load balancer on the device: create/update/delete +virtual server → backend pool mappings. + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `lb_rules` | JSON array of LB rules shown below. | +| `vpc_id` | Present for VPC tier networks. | + +**Decoded `lb_rules` JSON:** + +```json +[ + { + "id": 1, + "name": "lb-web", + "publicIp": "203.0.113.5", + "publicPort": 80, + "privatePort": 8080, + "protocol": "tcp", + "algorithm": "roundrobin", + "revoke": false, + "backends": [ + {"ip": "10.0.0.10", "port": 8080, "revoked": false}, + {"ip": "10.0.0.11", "port": 8080, "revoked": true} + ] + } +] +``` + +| Field | Description | +| --- | --- | +| `revoke` | `true` → delete this rule; `false` → create/update. | +| `backends[].revoked` | `true` → this backend has been removed from the rule. | +| `algorithm` | `roundrobin`, `leastconn`, or `source`. | +| `protocol` | `tcp`, `udp`, or `tcp-proxy`. | + +--- + +### `restore-network` + +**Called:** During a `restartNetwork(cleanup=true)` or `restartVPC(cleanup=true)` +operation, after all rules (firewall/NAT/LB) have been re-applied. + +**Purpose:** Batch-restore all DHCP leases, DNS entries, and metadata for +every VM currently on the network in a single call (instead of N per-VM +calls). + +**Payload fields (`payload` object):** + +| Field | Description | +| --- | --- | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `vlan` | Guest VLAN tag. | +| `extension_ip` | Extension IP. | +| `dns` | Comma-separated DNS server list. | +| `domain` | Network domain suffix. | +| `restore_data` | JSON restore payload shown below. | +| `vpc_id` | Present for VPC tier networks. | + +**`restore_data` JSON:** + +```json +{ + "dhcp_enabled": true, + "dns_enabled": true, + "userdata_enabled": true, + "vms": [ + { + "ip": "10.0.0.10", + "mac": "02:00:00:00:00:01", + "hostname": "vm-1", + "default_nic": true, + "vm_data": [ + {"dir": "userdata", "file": "user-data", "content": ""}, + {"dir": "meta-data", "file": "instance-id", "content": ""}, + {"dir": "meta-data", "file": "local-hostname","content": ""} + ] + } + ] +} +``` + +Each `vm_data[].content` is a plain UTF-8 string (same encoding as in `save-vm-data`). + +--- + +### `custom-action` + +**Called:** Via the `runNetworkCustomAction` API. + +**Purpose:** Allows operators to trigger ad-hoc operations on the device +without defining new CloudStack API calls. + +CloudStack writes the full custom-action request to a temporary JSON payload +file and passes that file directly to the script. Unlike the other commands, +`custom-action` does **not** use the nested `{ "payload": ... }` envelope; +command-specific inputs are provided in top-level `action-params`. + +It still includes the same top-level extension detail objects used elsewhere, +including `physical-network-extension-details` and `network-extension-details`. + +**Top-level payload keys (network-level):** + +| Key | Description | +| --- | --- | +| `network_id` | The CloudStack network ID. | +| `vpc_id` | Present when the network belongs to a VPC. | +| `action` | The action name passed by the operator. | +| `action-params` | JSON object with arbitrary key/value parameters. | +| `physical-network-extension-details` | Physical-network extension details JSON. | +| `network-extension-details` | Stored `extension.details` JSON for the network. | + +**Top-level payload keys (VPC-level):** + +| Key | Description | +| --- | --- | +| `vpc_id` | The CloudStack VPC ID. | +| `action` | The action name passed by the operator. | +| `action-params` | JSON object with arbitrary key/value parameters. | +| `physical-network-extension-details` | Physical-network extension details JSON. | +| `network-extension-details` | Stored `extension.details` JSON for the VPC. | + +Hook scripts should parse the payload file directly. + +**Stdout:** Returned verbatim to the API caller. + +--- + +## Service-to-Command Mapping + +| CloudStack Network Service | Commands triggered | +| --- | --- | +| **SourceNat / Gateway** | `assign-ip`, `release-ip` | +| **StaticNat** | `add-static-nat`, `delete-static-nat` | +| **PortForwarding** | `add-port-forward`, `delete-port-forward` | +| **Firewall** | `apply-fw-rules` | +| **Lb** | `apply-lb-rules` | +| **NetworkACL** | `apply-network-acl` | +| **Dhcp** | `add-dhcp-entry`, `remove-dhcp-entry`, `config-dhcp-subnet`, `remove-dhcp-subnet`, `set-dhcp-options` | +| **Dns** | `add-dns-entry`, `config-dns-subnet`, `remove-dns-subnet` | +| **UserData** | `save-vm-data`, `save-password`, `save-userdata`, `save-sshkey`, `save-hypervisor-hostname` | +| *(NIC lifecycle — all)* | `prepare-nic`, `release-nic` | +| *(network lifecycle — all)* | `ensure-network-device`, `implement-network`, `shutdown-network`, `destroy-network`, `restore-network` | +| *(VPC lifecycle)* | `ensure-network-device`, `implement-vpc`, `shutdown-vpc`, `update-vpc-source-nat-ip` | +| *(operator)* | `custom-action` | + +Your script only needs to implement the commands for the services it declares +in `network.services`. All other commands can safely be no-ops (exit 0). + +--- + +## Capabilities Configuration + +When creating the extension, set `network.service.capabilities` to a JSON +object keyed by service name. The values are capability name → value string +maps. + +```json +{ + "SourceNat": { + "SupportedSourceNatTypes": "peraccount", + "RedundantRouter": "false" + }, + "StaticNat": { + "Supported": "true" + }, + "PortForwarding": { + "SupportedProtocols": "tcp,udp" + }, + "Firewall": { + "TrafficStatistics": "per public ip", + "SupportedProtocols": "tcp,udp,icmp", + "SupportedEgressProtocols":"tcp,udp,icmp,all", + "SupportedTrafficDirection":"ingress,egress", + "MultipleIps": "true" + }, + "Lb": { + "SupportedLBAlgorithms": "roundrobin,leastconn,source", + "SupportedLBIsolation": "dedicated", + "SupportedProtocols": "tcp,udp,tcp-proxy", + "SupportedStickinessMethods": "lbcookie,appsession", + "LbSchemes": "Public", + "SslTermination": "false", + "VmAutoScaling": "false" + }, + "Dhcp": { + "DhcpAccrossMultipleSubnets": "true" + }, + "Dns": { + "AllowDnsSuffixModification": "true", + "ExternalDns": "true" + }, + "Gateway": { + "RedundantRouter": "false" + }, + "NetworkACL": { + "SupportedProtocols": "tcp,udp,icmp" + }, + "UserData": { + "Supported": "true" + } +} +``` + +Only declare services and capabilities your implementation actually supports. + +--- + +## VPC Networks + +For networks that belong to a VPC, `vpc_id` is added to the nested command +payload. Use it to share state across all tiers of the same VPC (e.g., a +single VRF or namespace per VPC instead of per tier). + +In `ensure-network-device`, use `vpc_id` (when present) as the hash key for +host selection so all tiers of a VPC always land on the same device. + +VPC lifecycle commands (`implement-vpc`, `shutdown-vpc`, +`update-vpc-source-nat-ip`) are invoked at the VPC level with no +`network_id` — only `vpc_id`. Tier-level commands such as +`implement-network` and `destroy-network` still receive both +`network_id` and `vpc_id`. + +To use this extension as a VPC provider: + +1. Create the NetworkOffering with `useVpc=on`. +2. Create a VpcOffering with the extension as provider. + +--- + +## Extension IP + +`extension_ip` is the IP the device presents on the guest network side: + +- **With SourceNat or Gateway service:** equals `gateway` (the device is the + gateway; no separate IP needed). +- **Without SourceNat/Gateway** (Dhcp/Dns/UserData only, e.g. a shared network + helper): CloudStack allocates a dedicated IP from the guest subnet and passes + it. The device must listen on this IP for DHCP, DNS, and metadata (port 80) + requests. + +### Opting out of the placeholder IP + +Some extensions (e.g. OVN) manage their own data-plane addressing and do not +need CloudStack to reserve a real IP for networks that provide DHCP/DNS/UserData +without SourceNat or Gateway. This covers **both** shared networks and isolated +networks that omit SourceNat. Set the Extension detail +**`network.allocate.extension.ip=false`** when creating or updating the Extension +to suppress the allocation: + +```bash +cmk createExtension \ + name=my-sdn \ + ... \ + details[N].network.allocate.extension.ip=false +``` + +When this detail is `false`, `ensureExtensionIp` returns `null` for any network +backed by this extension that reaches the placeholder-IP branch — i.e. networks +with DHCP/DNS/UserData but without SourceNat/Gateway. `extension_ip` will not +be set in command payloads, and no IP is consumed from the subnet pool. + +Omitting the detail (or setting any value other than `false`) preserves the +original behaviour — a placeholder NIC is allocated and `extension_ip` is +included in every command payload. + +--- + +## Exit Codes + +| Exit code | Meaning | +| --- | --- | +| `0` | Success. | +| Any non-zero | Failure. CloudStack logs the exit code and script output, and treats the operation as failed. | + +For SSH-proxy scripts you may use sub-codes for diagnostics (they are logged +but not interpreted differently by CloudStack): + +| Suggested code | Suggested meaning | +| --- | --- | +| `1` | Usage / configuration error. | +| `2` | SSH connection / authentication failure. | +| `3` | Remote script returned non-zero. | + +--- + +## Minimal Script Skeleton + +The following Bash skeleton handles all commands and can be used as a starting +point. Replace each `TODO` block with your device's API calls. + +```bash +#!/bin/bash +# my-sdn.sh — CloudStack NetworkOrchestrator extension entry-point +set -euo pipefail + +COMMAND="${1:-}" +PAYLOAD_FILE="${2:-}" +TIMEOUT_SECONDS="${3:-60}" + +if [[ -z "${COMMAND}" || -z "${PAYLOAD_FILE}" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +root_field() { + python3 - "$PAYLOAD_FILE" "$1" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as fh: + data = json.load(fh) +value = data.get(sys.argv[2], "") +if isinstance(value, (dict, list)): + print(json.dumps(value, separators=(",", ":"))) +elif value is None: + print("") +else: + print(value) +PY +} + +payload_field() { + python3 - "$PAYLOAD_FILE" "$1" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as fh: + data = json.load(fh) +value = data.get('payload', {}).get(sys.argv[2], "") +if isinstance(value, (dict, list)): + print(json.dumps(value, separators=(",", ":"))) +elif value is None: + print("") +else: + print(value) +PY +} + +case "${COMMAND}" in + + ensure-network-device) + # TODO: check that the device is reachable; select/validate host + # Print per-network JSON to stdout (stored by CloudStack) + printf '{"device":"%s"}\n' "$(payload_field network_id)" + ;; + + implement-network) + # TODO: create virtual segment (VRF / VLAN / namespace / …) + # TODO: configure gateway IP $(payload_field extension_ip) on $(payload_field cidr) + ;; + + shutdown-network|destroy-network) + # TODO: tear down the virtual segment + ;; + + implement-vpc) + # TODO: create VPC-level namespace / VRF for vpc=$(payload_field vpc_id) + # Optional source-NAT IP: $(payload_field public_ip) + ;; + + shutdown-vpc) + # TODO: remove VPC namespace / VRF for vpc=$(payload_field vpc_id) + ;; + + update-vpc-source-nat-ip) + # TODO: update VPC SNAT rule to use new public IP $(payload_field public_ip) + ;; + + assign-ip) + # TODO: attach public IP $(payload_field public_ip) to the device + ;; + + release-ip) + # TODO: remove public IP $(payload_field public_ip) from the device + ;; + + add-static-nat) + # TODO: DNAT $(payload_field public_ip) → $(payload_field private_ip) + ;; + + delete-static-nat) + # TODO: remove DNAT for $(payload_field public_ip) + ;; + + add-port-forward) + # TODO: DNAT $(payload_field public_ip):$(payload_field public_port) → $(payload_field private_ip):$(payload_field private_port) + ;; + + delete-port-forward) + # TODO: remove the port-forwarding DNAT rule + ;; + + apply-fw-rules) + FW_JSON=$(payload_field fw_rules) + # TODO: parse $FW_JSON and apply to device + ;; + + apply-network-acl) + ACL_JSON=$(payload_field acl_rules) + # TODO: parse $ACL_JSON and apply to VPC tier + ;; + + prepare-nic) + # TODO: create port binding mac=$(payload_field mac) ip=$(payload_field ip) nic_uuid=$(payload_field nic_uuid) + ;; + + release-nic) + # TODO: remove port binding mac=$(payload_field mac) ip=$(payload_field ip) + ;; + + add-dhcp-entry) + # TODO: add static lease mac=$(payload_field mac) ip=$(payload_field ip) + ;; + + remove-dhcp-entry) + # TODO: remove static lease for mac=$(payload_field mac) + ;; + + config-dhcp-subnet|remove-dhcp-subnet) ;; + + set-dhcp-options) ;; + + add-dns-entry) + # TODO: add A record hostname=$(payload_field hostname) ip=$(payload_field ip) + ;; + + config-dns-subnet|remove-dns-subnet) ;; + + save-vm-data) + VM_DATA_JSON=$(payload_field vm_data) + # TODO: iterate entries and write to metadata store + ;; + + save-password|save-userdata|save-sshkey|save-hypervisor-hostname) ;; + + apply-lb-rules) + LB_JSON=$(payload_field lb_rules) + # TODO: parse $LB_JSON and configure load balancer + ;; + + restore-network) + RESTORE_JSON=$(payload_field restore_data) + # TODO: iterate vms and restore leases / DNS / metadata + ;; + + custom-action) + ACTION_NAME=$(root_field action) + ACTION_PARAMS=$(root_field action-params) + # TODO: handle $ACTION_NAME with params $ACTION_PARAMS + echo "custom action ${ACTION_NAME} not implemented" + exit 1 + ;; + + *) + echo "Unknown command: ${COMMAND}" >&2 + exit 1 + ;; +esac + +exit 0 +``` + +For a full production implementation see https://github.com/apache/cloudstack-extensions/tree/network-namespace/Network-Namespace: +- `network-namespace.sh` — management-server entry-point (SSH proxy). +- `network-namespace-wrapper.sh` — KVM-host wrapper that implements all commands using Linux network namespaces. diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java index 535a0f703958..546f33b2a1d4 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java @@ -40,7 +40,7 @@ public class ExtensionDetailsVO implements ResourceDetail { @Column(name = "name", nullable = false, length = 255) private String name; - @Column(name = "value", nullable = false, length = 255) + @Column(name = "value", nullable = false, length = 4096) private String value; @Column(name = "display") diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java index 5cb6f7b85114..c2b23f1eee04 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java @@ -40,7 +40,7 @@ public class ExtensionResourceMapDetailsVO implements ResourceDetail { @Column(name = "name", nullable = false, length = 255) private String name; - @Column(name = "value", nullable = false, length = 255) + @Column(name = "value", nullable = false, length = 4096) private String value; @Column(name = "display") diff --git a/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml index 9d44d8ff7f3d..ee5e98630140 100644 --- a/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml +++ b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml @@ -33,4 +33,7 @@ + + + diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java index 2edb6ea48e3f..2f630966056c 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java @@ -94,4 +94,18 @@ public void testGetDetailsReturnsMap() { setField(cmd, "details", details); assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); } + + @Test + public void getReservedResourceDetailsReturnsValueWhenSet() { + setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3"); + String result = cmd.getReservedResourceDetails(); + assertEquals("detail1,detail2,detail3", result); + } + + @Test + public void getReservedResourceDetailsReturnsNullWhenNotSet() { + setField(cmd, "reservedResourceDetails", null); + String result = cmd.getReservedResourceDetails(); + assertNull(result); + } } diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java index 1ca601293a37..9ae051af00f4 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.framework.extensions.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; @@ -89,4 +90,21 @@ public void testGetDetailsWithInvalidValueThrowsException() { setPrivateField("details", detailsList); cmd.getDetails(); } + + // ----------------------------------------------------------------------- + // Tests for type getter + // ----------------------------------------------------------------------- + + @Test + public void testGetTypeReturnsValueWhenSet() { + setPrivateField("type", "NetworkOrchestrator"); + assertEquals("NetworkOrchestrator", cmd.getType()); + } + + @Test + public void testGetTypeReturnsNullWhenUnset() { + setPrivateField("type", null); + assertNull(cmd.getType()); + } + } diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java index f0a3a6fcf219..5c5c2014a529 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.util.ReflectionTestUtils.setField; import java.util.EnumSet; import java.util.HashMap; @@ -134,6 +135,20 @@ public void isCleanupDetailsReturnsValueWhenSet() { assertTrue(cmd.isCleanupDetails()); } + @Test + public void getReservedResourceDetailsReturnsValueWhenSet() { + setField(cmd, "reservedResourceDetails", "detail1,detail2,detail3"); + String result = cmd.getReservedResourceDetails(); + assertEquals("detail1,detail2,detail3", result); + } + + @Test + public void getReservedResourceDetailsReturnsNullWhenNotSet() { + setField(cmd, "reservedResourceDetails", null); + String result = cmd.getReservedResourceDetails(); + assertNull(result); + } + @Test public void executeSetsExtensionResponseWhenManagerSucceeds() { Extension extension = mock(Extension.class); diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmdTest.java new file mode 100644 index 000000000000..023820237f47 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateRegisteredExtensionCmdTest.java @@ -0,0 +1,160 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.extensions.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.Map; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.user.Account; + +public class UpdateRegisteredExtensionCmdTest { + + private UpdateRegisteredExtensionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new UpdateRegisteredExtensionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void extensionIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "extensionId", null); + assertNull(cmd.getExtensionId()); + } + + @Test + public void extensionIdReturnsValueWhenSet() { + Long extensionId = 42L; + ReflectionTestUtils.setField(cmd, "extensionId", extensionId); + assertEquals(extensionId, cmd.getExtensionId()); + } + + @Test + public void cleanupDetailsReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", null); + assertNull(cmd.isCleanupDetails()); + } + + @Test + public void cleanupDetailsReturnsTrueWhenSet() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", true); + assertTrue(cmd.isCleanupDetails()); + } + + @Test + public void cleanupDetailsReturnsFalseWhenSetToFalse() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", false); + assertFalse(cmd.isCleanupDetails()); + } + + @Test + public void getEntityOwnerIdReturnsSystemAccountId() { + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void getApiResourceTypeReturnsExtension() { + assertEquals(ApiCommandResourceType.Extension, cmd.getApiResourceType()); + } + + @Test + public void getApiResourceIdReturnsExtensionId() { + Long extensionId = 99L; + ReflectionTestUtils.setField(cmd, "extensionId", extensionId); + assertEquals(extensionId, cmd.getApiResourceId()); + } + + @Test + public void resourceIdReturnsValueWhenSet() { + String resourceId = "resource-123"; + ReflectionTestUtils.setField(cmd, "resourceId", resourceId); + assertEquals(resourceId, cmd.getResourceId()); + } + + @Test + public void resourceTypeReturnsValueWhenSet() { + String resourceType = "PhysicalNetwork"; + ReflectionTestUtils.setField(cmd, "resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void detailsReturnsEmptyMapWhenUnset() { + ReflectionTestUtils.setField(cmd, "details", null); + Map details = cmd.getDetails(); + assertNotNull(details); + assertTrue(details.isEmpty()); + } + + @Test + public void executeSetsExtensionResponseWhenManagerSucceeds() { + Extension extension = mock(Extension.class); + ExtensionResponse response = mock(ExtensionResponse.class); + when(extensionsManager.updateRegisteredExtensionWithResource(cmd)).thenReturn(extension); + when(extensionsManager.createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all))) + .thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).updateRegisteredExtensionWithResource(cmd); + verify(extensionsManager).createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all)); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.updateRegisteredExtensionWithResource(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update registered extension")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals("Failed to update registered extension", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java index 76a0175e7576..f4d5099b78b0 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java @@ -83,4 +83,52 @@ public void listResourceIdsByExtensionIdAndTypeReturnsCorrectIds() { when(dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(expectedIds); assertEquals(expectedIds, dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)); } + + // ----------------------------------------------------------------------- + // Tests for new methods: findResourceByExtensionIdAndResourceIdAndType, listResourceIdsByType + // ----------------------------------------------------------------------- + + @Test + public void findResourceByExtensionIdAndResourceIdAndTypeReturnsNullWhenNoMatch() { + when(dao.findResourceByExtensionIdAndResourceIdAndType(999L, 77L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null); + assertNull(dao.findResourceByExtensionIdAndResourceIdAndType(999L, 77L, ExtensionResourceMap.ResourceType.PhysicalNetwork)); + } + + @Test + public void findResourceByExtensionIdAndResourceIdAndTypeReturnsMatchingEntry() { + ExtensionResourceMapVO map1 = new ExtensionResourceMapVO(); + map1.setExtensionId(42L); + map1.setResourceId(7L); + map1.setResourceType(ExtensionResourceMap.ResourceType.PhysicalNetwork); + ExtensionResourceMapVO expected = map1; + when(dao.findResourceByExtensionIdAndResourceIdAndType(42L, 7L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(expected); + ExtensionResourceMapVO result = dao.findResourceByExtensionIdAndResourceIdAndType(42L, 7L, ExtensionResourceMap.ResourceType.PhysicalNetwork); + assertEquals(expected, result); + } + + @Test + public void findResourceByExtensionIdAndResourceIdAndTypeDifferentiatesResourceTypes() { + ExtensionResourceMapVO clusterMap = new ExtensionResourceMapVO(); + clusterMap.setResourceType(ExtensionResourceMap.ResourceType.Cluster); + when(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(clusterMap); + when(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null); + + assertEquals(clusterMap, dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.Cluster)); + assertNull(dao.findResourceByExtensionIdAndResourceIdAndType(10L, 55L, ExtensionResourceMap.ResourceType.PhysicalNetwork)); + } + + @Test + public void listResourceIdsByTypeReturnsEmptyListWhenNoMatch() { + when(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(List.of()); + assertTrue(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork).isEmpty()); + } + + @Test + public void listResourceIdsByTypeReturnsMatchingIds() { + List expectedIds = List.of(5L, 10L, 15L); + when(dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(expectedIds); + List result = dao.listResourceIdsByType(ExtensionResourceMap.ResourceType.PhysicalNetwork); + assertEquals(3, result.size()); + assertEquals(expectedIds, result); + } } diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java index 085ae212b281..fa8c97742697 100644 --- a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java @@ -23,11 +23,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -35,11 +37,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; import java.io.File; import java.security.InvalidParameterException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -49,8 +54,6 @@ import java.util.Map; import java.util.UUID; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.user.AccountService; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; @@ -61,6 +64,7 @@ import org.apache.cloudstack.extension.CustomActionResultResponse; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.extension.ExtensionHelper; import org.apache.cloudstack.extension.ExtensionResourceMap; import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd; @@ -73,6 +77,7 @@ import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateRegisteredExtensionCmd; import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand; import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; import org.apache.cloudstack.framework.extensions.command.GetExtensionPathChecksumCommand; @@ -85,12 +90,16 @@ import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDao; import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO; import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapDetailsVO; import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.collections.CollectionUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -113,15 +122,32 @@ import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.PermissionDeniedException; import com.cloud.host.Host; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.ExternalProvisioner; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkServiceMapDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkDao; +import com.cloud.network.dao.PhysicalNetworkServiceProviderDao; +import com.cloud.network.dao.PhysicalNetworkServiceProviderVO; +import com.cloud.network.dao.PhysicalNetworkVO; +import com.cloud.network.element.NetworkElement; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao; +import com.cloud.network.vpc.dao.VpcServiceMapDao; +import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; +import org.apache.cloudstack.extension.NetworkCustomActionProvider; import com.cloud.org.Cluster; import com.cloud.serializer.GsonHelper; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.user.Account; +import com.cloud.user.AccountService; import com.cloud.utils.Pair; import com.cloud.utils.UuidUtils; import com.cloud.utils.db.EntityManager; @@ -131,7 +157,6 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VmDetailConstants; -import com.cloud.vm.dao.VMInstanceDao; @RunWith(MockitoJUnitRunner.class) public class ExtensionsManagerImplTest { @@ -163,8 +188,6 @@ public class ExtensionsManagerImplTest { @Mock private ExtensionCustomActionDetailsDao extensionCustomActionDetailsDao; @Mock - private VMInstanceDao vmInstanceDao; - @Mock private VirtualMachineManager virtualMachineManager; @Mock private EntityManager entityManager; @@ -180,6 +203,25 @@ public class ExtensionsManagerImplTest { private RoleService roleService; @Mock private AccountService accountService; + @Mock + private PhysicalNetworkDao physicalNetworkDao; + @Mock + private NetworkDao networkDao; + @Mock + private NetworkServiceMapDao networkServiceMapDao; + @Mock + private VpcServiceMapDao vpcServiceMapDao; + @Mock + private NetworkModel networkModel; + + @Mock + private PhysicalNetworkServiceProviderDao physicalNetworkServiceProviderDao; + + @Mock + private NetworkOfferingServiceMapDao networkOfferingServiceMapDao; + + @Mock + private VpcOfferingServiceMapDao vpcOfferingServiceMapDao; @Before public void setUp() { @@ -282,17 +324,9 @@ public void unregisterExtensionWithClusterThrowsIfClusterNotFound() { @Test public void getExtensionFromResourceReturnsNullIfEntityNotFound() { when(entityManager.findByUuid(any(), anyString())).thenReturn(null); - assertNull(extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "uuid")); + assertNull(extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "uuid")); } - @Test - public void getActionMessageReturnsDefaultOnBlank() { - ExtensionCustomAction action = mock(ExtensionCustomAction.class); - Extension ext = mock(Extension.class); - when(action.getSuccessMessage()).thenReturn(null); - String msg = extensionsManager.getActionMessage(true, action, ext, ExtensionCustomAction.ResourceType.VirtualMachine, null); - assertTrue(msg.contains("Successfully completed")); - } @Test public void getActionMessageReturnsDefaultMessageForSuccessWithoutCustomMessage() { @@ -338,14 +372,6 @@ public void getActionMessageReturnsCustomFailureMessage() { assertEquals("Custom failure message", result); } - @Test - public void getActionMessageHandlesNullActionMessage() { - ExtensionCustomAction action = mock(ExtensionCustomAction.class); - when(action.getSuccessMessage()).thenReturn(null); - Extension extension = mock(Extension.class); - String result = extensionsManager.getActionMessage(true, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); - assertTrue(result.contains("Successfully completed")); - } @Test public void getFilteredExternalDetailsReturnsFilteredMap() { @@ -376,26 +402,6 @@ public void sendExtensionPathNotReadyAlertDoesNotCallsAlertManager() { anyLong(), anyLong(), anyString(), anyString()); } - @Test - public void updateExtensionPathReadyUpdatesWhenStateDiffers() { - Extension ext = mock(Extension.class); - when(ext.getId()).thenReturn(1L); - when(ext.isPathReady()).thenReturn(false); - ExtensionVO vo = mock(ExtensionVO.class); - when(extensionDao.createForUpdate(1L)).thenReturn(vo); - when(extensionDao.update(1L, vo)).thenReturn(true); - extensionsManager.updateExtensionPathReady(ext, true); - verify(extensionDao).update(1L, vo); - } - - @Test - public void disableExtensionUpdatesState() { - ExtensionVO vo = mock(ExtensionVO.class); - when(extensionDao.createForUpdate(1L)).thenReturn(vo); - when(extensionDao.update(1L, vo)).thenReturn(true); - extensionsManager.disableExtension(1L); - verify(extensionDao).update(1L, vo); - } @Test public void getExtensionFromResourceReturnsExtensionForValidResource() { @@ -408,7 +414,7 @@ public void getExtensionFromResourceReturnsExtensionForValidResource() { ExtensionVO extension = mock(ExtensionVO.class); when(extensionDao.findById(100L)).thenReturn(extension); - Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); assertEquals(extension, result); } @@ -417,7 +423,7 @@ public void getExtensionFromResourceReturnsExtensionForValidResource() { public void getExtensionFromResourceReturnsNullForInvalidResourceUuid() { when(entityManager.findByUuid(eq(VirtualMachine.class), eq("invalid-uuid"))).thenReturn(null); - Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "invalid-uuid"); + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "invalid-uuid"); assertNull(result); } @@ -428,7 +434,7 @@ public void getExtensionFromResourceReturnsNullForMissingClusterMapping() { when(entityManager.findByUuid(eq(VirtualMachine.class), eq("vm-uuid"))).thenReturn(vm); when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(null, null)); - Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); assertNull(result); } @@ -440,7 +446,7 @@ public void getExtensionFromResourceReturnsNullForMissingExtensionMapping() { when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 1L)); when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null); - Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); assertNull(result); } @@ -553,41 +559,26 @@ public void getExtensionsPathReturnsProvisionerPath() { assertEquals("/tmp/extensions", extensionsManager.getExtensionsPath()); } - @Test - public void getExtensionIdForClusterReturnsNullIfNoMap() { - when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null); - assertNull(extensionsManager.getExtensionIdForCluster(1L)); - } @Test - public void getExtensionIdForClusterReturnsIdIfMapExists() { - ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class); - when(map.getExtensionId()).thenReturn(5L); - when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map); - assertEquals(Long.valueOf(5L), extensionsManager.getExtensionIdForCluster(1L)); - } - - @Test - public void getExtensionReturnsExtension() { - ExtensionVO ext = mock(ExtensionVO.class); - when(extensionDao.findById(1L)).thenReturn(ext); - assertEquals(ext, extensionsManager.getExtension(1L)); - } - - @Test - public void getExtensionForClusterReturnsNullIfNoId() { - when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null); - assertNull(extensionsManager.getExtensionForCluster(1L)); + public void checkExtensionPathSyncUpdatesReadyWhenStateDiffers() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(ext.isPathReady()).thenReturn(false); + extensionsManager.checkExtensionPathState(ext, Collections.emptyList()); + verify(extensionsManager).updateExtensionPathReady(ext, false); } @Test - public void getExtensionForClusterReturnsExtensionIfIdExists() { - ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class); - when(map.getExtensionId()).thenReturn(5L); - when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map); - ExtensionVO ext = mock(ExtensionVO.class); - when(extensionDao.findById(5L)).thenReturn(ext); - assertEquals(ext, extensionsManager.getExtensionForCluster(1L)); + public void checkExtensionPathSyncUpdatesReadyWhenStateUnchanged() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(ext.isPathReady()).thenReturn(true); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123"); + extensionsManager.checkExtensionPathState(ext, Collections.emptyList()); + verify(extensionsManager, times(1)).updateExtensionPathReady(any(), anyBoolean()); } @Test @@ -664,6 +655,8 @@ public void testCreateExtension_Success() { when(cmd.getPath()).thenReturn(null); when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); when(cmd.getState()).thenReturn(null); + String reservedResourceDetails = "abc,xyz"; + when(cmd.getReservedResourceDetails()).thenReturn(reservedResourceDetails); when(extensionDao.findByName("ext1")).thenReturn(null); when(extensionDao.persist(any())).thenAnswer(inv -> { ExtensionVO extensionVO = inv.getArgument(0); @@ -671,11 +664,20 @@ public void testCreateExtension_Success() { return extensionVO; }); when(managementServerHostDao.listBy(any())).thenReturn(Collections.emptyList()); - + List detailsList = new ArrayList<>(); + doAnswer(inv -> { + List detailsVO = inv.getArgument(0); + detailsList.addAll(detailsVO); + return null; + }).when(extensionDetailsDao).saveDetails(anyList()); Extension ext = extensionsManager.createExtension(cmd); assertEquals("ext1", ext.getName()); verify(extensionDao).persist(any()); + assertTrue(CollectionUtils.isNotEmpty(detailsList)); + assertTrue(detailsList.stream() + .anyMatch(detail -> ApiConstants.RESERVED_RESOURCE_DETAILS.equals(detail.getName()) + && reservedResourceDetails.equals(detail.getValue()))); } @Test @@ -934,18 +936,135 @@ public void testUpdateExtension_InvalidState() { extensionsManager.updateExtension(cmd); } + @Test(expected = CloudRuntimeException.class) + public void testUpdateExtension_RemovingUsedNetworkServiceThrows() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(6L); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + Map newDetails = new HashMap<>(); + newDetails.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat"); + when(cmd.getDetails()).thenReturn(newDetails); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(6L); + when(ext.getName()).thenReturn("MyExt"); + when(ext.getType()).thenReturn(Extension.Type.NetworkOrchestrator); + when(extensionDao.findById(6L)).thenReturn(ext); + + Map oldDetailsMap = new HashMap<>(); + oldDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat,StaticNat"); + Map updatedDetailsMap = new HashMap<>(); + updatedDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat"); + when(extensionDetailsDao.listDetailsKeyPairs(6L)).thenReturn(oldDetailsMap).thenReturn(updatedDetailsMap); + + when(networkOfferingServiceMapDao.listOfferingIdsByServiceAndProvider(Network.Service.StaticNat, "MyExt")) + .thenReturn(Collections.singletonList(1L)); + + extensionsManager.updateExtension(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testUpdateExtension_RemovingServiceUsedByVpcOfferingThrows() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(8L); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + Map newDetails = new HashMap<>(); + newDetails.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat"); + when(cmd.getDetails()).thenReturn(newDetails); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(8L); + when(ext.getName()).thenReturn("MyExt"); + when(ext.getType()).thenReturn(Extension.Type.NetworkOrchestrator); + when(extensionDao.findById(8L)).thenReturn(ext); + + Map oldDetailsMap = new HashMap<>(); + oldDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat,StaticNat"); + Map updatedDetailsMap = new HashMap<>(); + updatedDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat"); + when(extensionDetailsDao.listDetailsKeyPairs(8L)).thenReturn(oldDetailsMap).thenReturn(updatedDetailsMap); + + when(networkOfferingServiceMapDao.listOfferingIdsByServiceAndProvider(Network.Service.StaticNat, "MyExt")) + .thenReturn(Collections.emptyList()); + when(vpcOfferingServiceMapDao.listOfferingIdsByServiceAndProvider(Network.Service.StaticNat, "MyExt")) + .thenReturn(Collections.singletonList(1L)); + + extensionsManager.updateExtension(cmd); + } + + @Test + public void testUpdateExtension_UpdatesPhysicalNetworkServicesWhenNotInUse() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(7L); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + Map newDetails = new HashMap<>(); + newDetails.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat,StaticNat"); + when(cmd.getDetails()).thenReturn(newDetails); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(7L); + when(ext.getName()).thenReturn("MyExt"); + when(ext.getType()).thenReturn(Extension.Type.NetworkOrchestrator); + when(extensionDao.findById(7L)).thenReturn(ext); + + Map oldDetailsMap = new HashMap<>(); + oldDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat"); + Map updatedDetailsMap = new HashMap<>(); + updatedDetailsMap.put(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, "SourceNat,StaticNat"); + when(extensionDetailsDao.listDetailsKeyPairs(7L)).thenReturn(oldDetailsMap).thenReturn(updatedDetailsMap); + + when(extensionResourceMapDao.listResourceIdsByExtensionIdAndType(7L, ExtensionResourceMap.ResourceType.PhysicalNetwork)) + .thenReturn(Collections.singletonList(100L)); + PhysicalNetworkServiceProviderVO nsp = mock(PhysicalNetworkServiceProviderVO.class); + when(nsp.getId()).thenReturn(500L); + when(nsp.getEnabledServices()).thenReturn(new ArrayList<>(Collections.singletonList(Network.Service.SourceNat))); + when(physicalNetworkServiceProviderDao.findByServiceProvider(100L, "MyExt")).thenReturn(nsp); + + extensionsManager.updateExtension(cmd); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(nsp).setEnabledServices(captor.capture()); + assertTrue(captor.getValue().contains(Network.Service.SourceNat)); + assertTrue(captor.getValue().contains(Network.Service.StaticNat)); + verify(physicalNetworkServiceProviderDao).update(500L, nsp); + } + @Test public void updateExtensionsDetails_SavesDetails_WhenDetailsProvided() { long extensionId = 10L; Map details = Map.of("foo", "bar", "baz", "qux"); - extensionsManager.updateExtensionsDetails(false, details, null, extensionId); + extensionsManager.updateExtensionsDetails(false, details, null, null, extensionId); verify(extensionDetailsDao).saveDetails(any()); } + @Test + public void updateExtensionsDetails_PersistReservedDetail_WhenProvided() { + long extensionId = 10L; + when(extensionDetailsDao.persist(any())).thenReturn(mock(ExtensionDetailsVO.class)); + extensionsManager.updateExtensionsDetails(false, null, null, "abc,xyz", extensionId); + verify(extensionDetailsDao).persist(any()); + } + + @Test + public void updateExtensionsDetails_UpdateReservedDetail_WhenProvided() { + long extensionId = 10L; + when(extensionDetailsDao.findDetail(anyLong(), eq(ApiConstants.RESERVED_RESOURCE_DETAILS))) + .thenReturn(mock(ExtensionDetailsVO.class)); + when(extensionDetailsDao.update(anyLong(), any())).thenReturn(true); + extensionsManager.updateExtensionsDetails(false, null, null, "abc,xyz", extensionId); + verify(extensionDetailsDao).update(anyLong(), any()); + } + @Test public void updateExtensionsDetails_DoesNothing_WhenDetailsAndCleanupAreNull() { long extensionId = 11L; - extensionsManager.updateExtensionsDetails(null, null, null, extensionId); + extensionsManager.updateExtensionsDetails(null, null, null, null, extensionId); verify(extensionDetailsDao, never()).removeDetails(anyLong()); verify(extensionDetailsDao, never()).saveDetails(any()); } @@ -953,7 +1072,7 @@ public void updateExtensionsDetails_DoesNothing_WhenDetailsAndCleanupAreNull() { @Test public void updateExtensionsDetails_RemovesDetailsOnly_WhenCleanupIsTrue() { long extensionId = 12L; - extensionsManager.updateExtensionsDetails(true, null, null, extensionId); + extensionsManager.updateExtensionsDetails(true, null, null, null, extensionId); verify(extensionDetailsDao).removeDetails(extensionId); verify(extensionDetailsDao, never()).saveDetails(any()); } @@ -961,7 +1080,7 @@ public void updateExtensionsDetails_RemovesDetailsOnly_WhenCleanupIsTrue() { @Test public void updateExtensionsDetails_PersistsOrchestratorFlag_WhenFlagIsNotNull() { long extensionId = 13L; - extensionsManager.updateExtensionsDetails(false, null, true, extensionId); + extensionsManager.updateExtensionsDetails(false, null, true, null, extensionId); verify(extensionDetailsDao).persist(any()); } @@ -970,7 +1089,7 @@ public void updateExtensionsDetails_ThrowsException_WhenPersistFails() { long extensionId = 14L; Map details = Map.of("foo", "bar"); doThrow(CloudRuntimeException.class).when(extensionDetailsDao).saveDetails(any()); - extensionsManager.updateExtensionsDetails(false, details, null, extensionId); + extensionsManager.updateExtensionsDetails(false, details, null, null, extensionId); } @Test @@ -990,13 +1109,6 @@ public void testDeleteExtension_Success() { verify(extensionDao).remove(1L); } - @Test - public void testRegisterExtensionWithResource_InvalidResourceType() { - RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); - when(cmd.getResourceType()).thenReturn("InvalidType"); - - assertThrows(InvalidParameterValueException.class, () -> extensionsManager.registerExtensionWithResource(cmd)); - } @Test public void registerExtensionWithResourceRegistersSuccessfullyForValidResourceType() { @@ -1029,8 +1141,6 @@ public void registerExtensionWithResourceThrowsForMissingExtension() { RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.Cluster.name()); when(cmd.getResourceId()).thenReturn(UUID.randomUUID().toString()); - ClusterVO clusterVO = mock(ClusterVO.class); - when(clusterDao.findByUuid(anyString())).thenReturn(clusterVO); extensionsManager.registerExtensionWithResource(cmd); } @@ -1105,6 +1215,154 @@ public void unregisterExtensionWithClusterHandlesMissingMappingGracefully() { verify(extensionResourceMapDao, never()).remove(anyLong()); } + @Test + public void unregisterExtensionWithResourceThrowsWhenProviderUsedByExistingNetworks() { + UnregisterExtensionCmd cmd = mock(UnregisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("physnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + + PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class); + when(physicalNetwork.getId()).thenReturn(42L); + when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork); + + ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class); + when(existing.getExtensionId()).thenReturn(1L); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(1L, 42L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(existing); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.getName()).thenReturn("extnet-provider"); + when(extensionDao.findById(1L)).thenReturn(extension); + + NetworkVO network = mock(NetworkVO.class); + when(networkDao.listByPhysicalNetworkAndProvider(42L, "extnet-provider")).thenReturn(List.of(network)); + + assertThrows(CloudRuntimeException.class, () -> extensionsManager.unregisterExtensionWithResource(cmd)); + verify(extensionResourceMapDao, never()).remove(anyLong()); + } + + @Test + public void updateRegisteredExtensionWithResourceUpdatesDetailsForExistingMapping() { + UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("physnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getDetails()).thenReturn(Map.of("username", "root", "hosts", "10.10.10.10")); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(extension); + + PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class); + when(physicalNetwork.getId()).thenReturn(42L); + when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork); + + ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class); + when(existing.getId()).thenReturn(100L); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(1L, 42L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(existing); + + Extension result = extensionsManager.updateRegisteredExtensionWithResource(cmd); + + assertEquals(extension, result); + verify(extensionResourceMapDetailsDao, never()).removeDetails(anyLong()); + verify(extensionResourceMapDetailsDao).saveDetails(any()); + } + + @Test + public void updateRegisteredExtensionWithResourceCleanupDetailsFirstThenSaveRequested() { + UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("physnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getDetails()).thenReturn(Map.of("username", "root", "password", "secret")); + when(cmd.isCleanupDetails()).thenReturn(true); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(extension); + + PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class); + when(physicalNetwork.getId()).thenReturn(42L); + when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork); + + ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class); + when(existing.getId()).thenReturn(100L); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(1L, 42L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(existing); + + extensionsManager.updateRegisteredExtensionWithResource(cmd); + + verify(extensionResourceMapDetailsDao).removeDetails(100L); + verify(extensionResourceMapDetailsDao, never()).saveDetails(any()); + } + + @Test + @SuppressWarnings("unchecked") + public void updateRegisteredExtensionWithResourceStoresSensitiveDetailsWithDisplayFalse() { + UpdateRegisteredExtensionCmd cmd = mock(UpdateRegisteredExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("physnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getDetails()).thenReturn(Map.of("username", "root", "password", "newSecret")); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(extension); + + PhysicalNetworkVO physicalNetwork = mock(PhysicalNetworkVO.class); + when(physicalNetwork.getId()).thenReturn(42L); + when(physicalNetworkDao.findByUuid("physnet-uuid")).thenReturn(physicalNetwork); + + ExtensionResourceMapVO existing = mock(ExtensionResourceMapVO.class); + when(existing.getId()).thenReturn(100L); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(1L, 42L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(existing); + extensionsManager.updateRegisteredExtensionWithResource(cmd); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(extensionResourceMapDetailsDao).saveDetails(captor.capture()); + verify(extensionResourceMapDetailsDao, never()).removeDetails(anyLong()); + List savedDetails = captor.getValue(); + + ExtensionResourceMapDetailsVO passwordDetail = savedDetails.stream() + .filter(detail -> "password".equals(detail.getName())) + .findFirst() + .orElse(null); + assertNotNull(passwordDetail); + assertFalse(passwordDetail.isDisplay()); + assertEquals("newSecret", passwordDetail.getValue()); + } + + @Test + @SuppressWarnings("unchecked") + public void registerExtensionWithClusterStoresSensitiveDetailsWithDisplayFalse() { + Cluster cluster = mock(Cluster.class); + when(cluster.getId()).thenReturn(12L); + when(cluster.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + + Extension extension = mock(Extension.class); + when(extension.getId()).thenReturn(5L); + + ExtensionResourceMapVO persistedMap = mock(ExtensionResourceMapVO.class); + when(persistedMap.getId()).thenReturn(120L); + when(extensionResourceMapDao.persist(any())).thenReturn(persistedMap); + + extensionsManager.registerExtensionWithCluster(cluster, extension, + Map.of("username", "admin", "password", "s3cr3t")); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(extensionResourceMapDetailsDao).saveDetails(captor.capture()); + List savedDetails = captor.getValue(); + + ExtensionResourceMapDetailsVO passwordDetail = savedDetails.stream() + .filter(detail -> "password".equals(detail.getName())) + .findFirst() + .orElse(null); + assertNotNull(passwordDetail); + assertFalse(passwordDetail.isDisplay()); + } + @Test public void testCreateExtensionResponse_BasicFields() { Extension extension = mock(Extension.class); @@ -1161,7 +1419,8 @@ public void testCreateExtensionResponse_HiddenDetailsOnly() { when(externalProvisioner.getExtensionPath("entry2.sh")).thenReturn("/some/path/entry2.sh"); Map hiddenDetails = Map.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, "false"); - when(extensionDetailsDao.listDetailsKeyPairs(2L, List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))) + when(extensionDetailsDao.listDetailsKeyPairs(2L, List.of( + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, ApiConstants.RESERVED_RESOURCE_DETAILS))) .thenReturn(hiddenDetails); EnumSet viewDetails = EnumSet.noneOf(ApiConstants.ExtensionDetails.class); @@ -1673,6 +1932,238 @@ public void runCustomAction_CheckAccessThrowsException() throws Exception { } } + @Test + public void runNetworkCustomAction_NoProviderFound_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(11L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(10L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(10L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No network service provider found for this network", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runNetworkCustomAction_ProviderElementMissing_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(12L); + when(networkServiceMapDao.getProviderForServiceInNetwork(12L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(null); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(11L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(11L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("ExtProvider"); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No network element found for provider: ExtProvider", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runNetworkCustomAction_ProviderCannotHandle_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(13L); + when(networkServiceMapDao.getProviderForServiceInNetwork(13L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + when(provider.canHandleCustomAction(network)).thenReturn(false); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(12L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(12L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("ExtProvider"); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("cannot handle custom action")); + } + + @Test + public void runNetworkCustomAction_ProviderDoesNotImplementCustomAction_ReturnsFailureResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(131L); + when(networkServiceMapDao.getProviderForServiceInNetwork(131L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class); + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(121L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(121L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("ExtProvider"); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("does not support custom actions")); + } + + @Test + public void runNetworkCustomAction_SuccessfulExecution_ReturnsSuccessResponse() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(14L); + when(networkServiceMapDao.getProviderForServiceInNetwork(14L, Network.Service.CustomAction)).thenReturn("ExtProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("ExtProvider")).thenReturn(element); + when(provider.canHandleCustomAction(network)).thenReturn(true); + when(provider.runCustomAction(eq(network), eq("dump-config"), any())).thenReturn("dump-output"); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(13L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(13L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("ExtProvider"); + + CustomActionResultResponse response = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Network, Collections.emptyMap()); + + assertTrue(response.getSuccess()); + assertEquals("dump-output", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runVpcCustomAction_ProviderNotCustomActionProvider_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(21L); + when(vpcServiceMapDao.getProviderForServiceInVpc(21L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class); + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(20L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(20L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("VpcProvider"); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("does not support custom actions")); + } + + @Test + public void runVpcCustomAction_NoProviderFound_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(211L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(201L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(201L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertEquals("No VPC service provider found for this VPC", response.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runVpcCustomAction_ProviderCannotHandleVpc_ReturnsFailureResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(212L); + when(vpcServiceMapDao.getProviderForServiceInVpc(212L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + when(provider.canHandleVpcCustomAction(vpc)).thenReturn(false); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(202L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(202L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("VpcProvider"); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertFalse(response.getSuccess()); + assertTrue(response.getResult().get(ApiConstants.DETAILS).contains("cannot handle custom action")); + } + + @Test + public void runVpcCustomAction_SuccessfulExecution_ReturnsSuccessResponse() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(22L); + when(vpcServiceMapDao.getProviderForServiceInVpc(22L, Network.Service.CustomAction)).thenReturn("VpcProvider"); + + NetworkElement element = mock(NetworkElement.class, withSettings().extraInterfaces(NetworkCustomActionProvider.class)); + NetworkCustomActionProvider provider = (NetworkCustomActionProvider) element; + when(networkModel.getElementImplementingProvider("VpcProvider")).thenReturn(element); + when(provider.canHandleVpcCustomAction(vpc)).thenReturn(true); + when(provider.runCustomAction(eq(vpc), eq("dump-config"), any())).thenReturn("vpc-dump-output"); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getId()).thenReturn(21L); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(21L)) + .thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("VpcProvider"); + + CustomActionResultResponse response = extensionsManager.runVpcCustomAction( + vpc, actionVO, extensionVO, ExtensionCustomAction.ResourceType.Vpc, Collections.emptyMap()); + + assertTrue(response.getSuccess()); + assertEquals("vpc-dump-output", response.getResult().get(ApiConstants.DETAILS)); + } + @Test public void createCustomActionResponse_SetsBasicFields() { ExtensionCustomAction action = mock(ExtensionCustomAction.class); @@ -2069,4 +2560,506 @@ public void getCallerDetailsReturnsDetailsWithoutRoleWhenRoleIsNull() { } } + @Test + public void getExtensionReservedResourceDetailsReturnsEmptyListWhenDetailsNotFound() { + long extensionId = 1L; + when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(null); + + List result = extensionsManager.getExtensionReservedResourceDetails(extensionId); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void getExtensionReservedResourceDetailsReturnsEmptyListWhenValueIsBlank() { + long extensionId = 2L; + ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class); + when(detailsVO.getValue()).thenReturn(" "); + when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO); + + List result = extensionsManager.getExtensionReservedResourceDetails(extensionId); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void getExtensionReservedResourceDetailsReturnsListOfTrimmedDetails() { + long extensionId = 3L; + ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class); + when(detailsVO.getValue()).thenReturn(" detail1 , detail2,detail3 "); + when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO); + + List result = extensionsManager.getExtensionReservedResourceDetails(extensionId); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("detail1", result.get(0)); + assertEquals("detail2", result.get(1)); + assertEquals("detail3", result.get(2)); + } + + @Test + public void getExtensionReservedResourceDetailsHandlesEmptyPartsGracefully() { + long extensionId = 4L; + ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class); + when(detailsVO.getValue()).thenReturn("detail1,,detail2, ,detail3"); + when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO); + + List result = extensionsManager.getExtensionReservedResourceDetails(extensionId); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("detail1", result.get(0)); + assertEquals("detail2", result.get(1)); + assertEquals("detail3", result.get(2)); + } + + @Test + public void getExtensionReservedResourceDetailsReturnsEmptyListWhenSplitResultsInNoParts() { + long extensionId = 5L; + ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class); + when(detailsVO.getValue()).thenReturn(","); + when(extensionDetailsDao.findDetail(extensionId, ApiConstants.RESERVED_RESOURCE_DETAILS)).thenReturn(detailsVO); + + List result = extensionsManager.getExtensionReservedResourceDetails(extensionId); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void addInbuiltExtensionReservedResourceDetailsDoesNothingWhenExtensionNotFound() { + when(extensionDao.findById(1L)).thenReturn(null); + List reservedResourceDetails = new ArrayList<>(); + extensionsManager.addInbuiltExtensionReservedResourceDetails(1L, reservedResourceDetails); + assertTrue(reservedResourceDetails.isEmpty()); + } + + @Test + public void addInbuiltExtensionReservedResourceDetailsDoesNothingForUserDefinedExtension() { + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.isUserDefined()).thenReturn(true); + when(extensionDao.findById(2L)).thenReturn(extension); + List reservedResourceDetails = new ArrayList<>(); + reservedResourceDetails.add("existing-detail"); + extensionsManager.addInbuiltExtensionReservedResourceDetails(2L, reservedResourceDetails); + assertEquals(1, reservedResourceDetails.size()); + assertTrue(reservedResourceDetails.contains("existing-detail")); + } + + @Test + public void addInbuiltExtensionReservedResourceDetailsDoesNothingWhenNoMatchFound() { + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.isUserDefined()).thenReturn(false); + when(extension.getName()).thenReturn("no-such-inbuilt-key-expected"); + when(extensionDao.findById(3L)).thenReturn(extension); + List reservedResourceDetails = new ArrayList<>(); + extensionsManager.addInbuiltExtensionReservedResourceDetails(3L, reservedResourceDetails); + assertTrue(reservedResourceDetails.isEmpty()); + } + + @Test + public void addInbuiltExtensionReservedResourceDetailsAddedDetails() { + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.isUserDefined()).thenReturn(false); + Map.Entry> entry = + ExtensionsManagerImpl.INBUILT_RESERVED_RESOURCE_DETAILS.entrySet().iterator().next(); + when(extension.getName()).thenReturn(entry.getKey()); + when(extensionDao.findById(3L)).thenReturn(extension); + List reservedResourceDetails = new ArrayList<>(); + extensionsManager.addInbuiltExtensionReservedResourceDetails(3L, reservedResourceDetails); + assertFalse(reservedResourceDetails.isEmpty()); + assertEquals(reservedResourceDetails.size(), entry.getValue().size()); + assertTrue(reservedResourceDetails.containsAll(entry.getValue())); + } + + // ----------------------------------------------------------------------- + // Tests for network custom action behavior + // ----------------------------------------------------------------------- + + + // Helper: a mock object that is both a NetworkElement and a NetworkCustomActionProvider + interface MockNetworkElement extends NetworkElement, NetworkCustomActionProvider {} + + @Test + public void runNetworkCustomActionSucceeds() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(5L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("reboot-device"); + when(actionVO.getId()).thenReturn(1L); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("my-extnet"); + + Pair, Map> details = new Pair<>(new HashMap<>(), new HashMap<>()); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(1L)).thenReturn(details); + + // networkServiceMapDao returns provider name for SourceNat + when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn("my-extnet"); + + // element implements both NetworkElement and NetworkCustomActionProvider + MockNetworkElement element = mock(MockNetworkElement.class); + when(element.canHandleCustomAction(eq(network))).thenReturn(true); + when(element.runCustomAction(eq(network), eq("reboot-device"), any())).thenReturn("OK: bridge bounced"); + when(networkModel.getElementImplementingProvider("my-extnet")).thenReturn(element); + + CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, + ExtensionCustomAction.ResourceType.Network, new HashMap<>()); + + assertTrue(resp.isSuccess()); + assertEquals("OK: bridge bounced", resp.getResult().get(ApiConstants.DETAILS)); + } + + @Test + public void runNetworkCustomActionFailsWhenNoProvider() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(5L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("dump-config"); + when(actionVO.getId()).thenReturn(2L); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + + Pair, Map> details = new Pair<>(new HashMap<>(), new HashMap<>()); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(2L)).thenReturn(details); + + // No provider found for any service + when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn(null); + + CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, + ExtensionCustomAction.ResourceType.Network, new HashMap<>()); + + assertFalse(resp.isSuccess()); + assertTrue(resp.getResult().get(ApiConstants.DETAILS).contains("No network service provider")); + } + + // ----------------------------------------------------------------------- + // Tests for getExtensionFromResource with Network resource type + // ----------------------------------------------------------------------- + + @Test + public void getExtensionFromResourceReturnsExtensionForNetworkWithProviderMatch() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(10L); + when(network.getPhysicalNetworkId()).thenReturn(5L); + when(entityManager.findByUuid(eq(Network.class), eq("net-uuid"))).thenReturn(network); + + String providerName ="my-ext-provider"; + when(networkServiceMapDao.getProviderForServiceInNetwork(10L, Network.Service.CustomAction)).thenReturn(providerName); + + ExtensionVO ext = mock(ExtensionVO.class); + doReturn(ext).when(extensionsManager).getExtensionForPhysicalNetworkAndProvider(5L, "my-ext-provider"); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Network, "net-uuid"); + assertEquals(ext, result); + } + + @Test + public void getExtensionFromResourceFallsBackToFirstMappingForNetwork() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(10L); + when(network.getPhysicalNetworkId()).thenReturn(5L); + when(entityManager.findByUuid(eq(Network.class), eq("net-uuid"))).thenReturn(network); + + when(networkServiceMapDao.getProviderForServiceInNetwork(10L, Network.Service.CustomAction)).thenReturn(null); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Network, "net-uuid"); + assertNull(result); + } + + @Test + public void getExtensionFromResourceReturnsNullForNetworkWithNullPhysicalNetworkId() { + Network network = mock(Network.class); + when(network.getPhysicalNetworkId()).thenReturn(null); + when(entityManager.findByUuid(eq(Network.class), eq("net-uuid"))).thenReturn(network); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Network, "net-uuid"); + assertNull(result); + } + + // ----------------------------------------------------------------------- + // Tests for getExtensionFromResource with Vpc resource type + // ----------------------------------------------------------------------- + + @Test + public void getExtensionFromResourceReturnsExtensionForVpcWithProviderMatch() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(20L); + when(entityManager.findByUuid(eq(Vpc.class), eq("vpc-uuid"))).thenReturn(vpc); + + when(vpcServiceMapDao.getProviderForServiceInVpc(20L, Network.Service.CustomAction)).thenReturn("my-vpc-provider"); + + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findByNameAndType("my-vpc-provider", Extension.Type.NetworkOrchestrator)).thenReturn(ext); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Vpc, "vpc-uuid"); + + assertEquals(ext, result); + } + + @Test + public void getExtensionFromResourceReturnsNullForVpcWithoutProvider() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(20L); + when(entityManager.findByUuid(eq(Vpc.class), eq("vpc-uuid"))).thenReturn(vpc); + + when(vpcServiceMapDao.getProviderForServiceInVpc(20L, Network.Service.CustomAction)).thenReturn(null); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Vpc, "vpc-uuid"); + + assertNull(result); + verify(extensionDao, never()).findByName(anyString()); + } + + @Test + public void getExtensionFromResourceReturnsNullForVpcWhenProviderExtensionNotFound() { + Vpc vpc = mock(Vpc.class); + when(vpc.getId()).thenReturn(20L); + when(entityManager.findByUuid(eq(Vpc.class), eq("vpc-uuid"))).thenReturn(vpc); + + when(vpcServiceMapDao.getProviderForServiceInVpc(20L, Network.Service.CustomAction)).thenReturn("missing-provider"); + when(extensionDao.findByNameAndType("missing-provider", Extension.Type.NetworkOrchestrator)).thenReturn(null); + + Extension result = extensionsManager.getExtensionWithCustomActionFromResource(ExtensionCustomAction.ResourceType.Vpc, "vpc-uuid"); + + assertNull(result); + } + + + // ----------------------------------------------------------------------- + // Tests for registerExtensionWithPhysicalNetwork + // ----------------------------------------------------------------------- + + @Test + public void registerExtensionWithPhysicalNetworkSucceeds() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("pnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getDetails()).thenReturn(null); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.getType()).thenReturn(Extension.Type.NetworkOrchestrator); + when(extension.getName()).thenReturn("my-ext"); + when(extension.getId()).thenReturn(1L); + when(extensionDao.findById(1L)).thenReturn(extension); + + PhysicalNetworkVO physNet = mock(PhysicalNetworkVO.class); + when(physNet.getId()).thenReturn(42L); + when(physicalNetworkDao.findByUuid("pnet-uuid")).thenReturn(physNet); + + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(1L, 42L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null); + when(extensionDetailsDao.listDetailsKeyPairs(1L)).thenReturn(Collections.emptyMap()); + + ExtensionResourceMapVO savedMap = mock(ExtensionResourceMapVO.class); + when(savedMap.getExtensionId()).thenReturn(1L); + when(extensionResourceMapDao.persist(any())).thenReturn(savedMap); + + when(physicalNetworkServiceProviderDao.findByServiceProvider(42L, "my-ext")).thenReturn(null); + + Extension result = extensionsManager.registerExtensionWithResource(cmd); + assertEquals(extension, result); + } + + @Test(expected = InvalidParameterValueException.class) + public void registerExtensionWithPhysicalNetworkFailsForNonNetworkOrchestratorType() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.PhysicalNetwork.name()); + when(cmd.getResourceId()).thenReturn("pnet-uuid"); + when(cmd.getExtensionId()).thenReturn(1L); + + ExtensionVO extension = mock(ExtensionVO.class); + when(extension.getType()).thenReturn(Extension.Type.Orchestrator); + when(extension.getName()).thenReturn("orch-ext"); + when(extensionDao.findById(1L)).thenReturn(extension); + + PhysicalNetworkVO physNet = mock(PhysicalNetworkVO.class); + when(physicalNetworkDao.findByUuid("pnet-uuid")).thenReturn(physNet); + + extensionsManager.registerExtensionWithResource(cmd); + } + + + // ----------------------------------------------------------------------- + // Tests for getExtensionForPhysicalNetworkAndProvider + // ----------------------------------------------------------------------- + + @Test + public void getExtensionForPhysicalNetworkAndProviderReturnsMatchingExtension() { + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(10L); + when(extensionDao.findByName("myext")).thenReturn(ext); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(10L, 5L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(mock(ExtensionResourceMapVO.class)); + + Extension result = extensionsManager.getExtensionForPhysicalNetworkAndProvider(5L, "myext"); + assertEquals(ext, result); + } + + @Test + public void getExtensionForPhysicalNetworkAndProviderReturnsNullWhenNameDoesNotMatch() { + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(10L); + when(extensionDao.findByName("myext")).thenReturn(ext); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(10L, 5L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null); + + Extension result = extensionsManager.getExtensionForPhysicalNetworkAndProvider(5L, "myext"); + assertNull(result); + } + + @Test + public void getExtensionForPhysicalNetworkAndProviderReturnsNullForNullProviderName() { + Extension result = extensionsManager.getExtensionForPhysicalNetworkAndProvider(5L, null); + assertNull(result); + } + + // ----------------------------------------------------------------------- + // Tests for getAllResourceMapDetailsForExtensionOnPhysicalNetwork + // ----------------------------------------------------------------------- + + @Test + public void getAllResourceMapDetailsForExtensionOnPhysicalNetworkReturnsDetails() { + ExtensionResourceMapVO mapVO = mock(ExtensionResourceMapVO.class); + when(mapVO.getId()).thenReturn(100L); + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(10L, 5L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(mapVO); + when(extensionResourceMapDetailsDao.listDetailsKeyPairs(100L)) + .thenReturn(Map.of("host", "192.168.1.1")); + + Map result = extensionsManager.getAllResourceMapDetailsForExtensionOnPhysicalNetwork(5L, 10L); + assertEquals("192.168.1.1", result.get("host")); + } + + @Test + public void getAllResourceMapDetailsForExtensionOnPhysicalNetworkReturnsEmptyWhenNoMapping() { + when(extensionResourceMapDao.findResourceByExtensionIdAndResourceIdAndType(10L, 5L, + ExtensionResourceMap.ResourceType.PhysicalNetwork)).thenReturn(null); + + Map result = extensionsManager.getAllResourceMapDetailsForExtensionOnPhysicalNetwork(5L, 10L); + assertTrue(result.isEmpty()); + } + + // ----------------------------------------------------------------------- + // Tests for isNetworkExtensionProvider + // ----------------------------------------------------------------------- + + @Test + public void isNetworkExtensionProviderReturnsTrueWhenProviderMatchesExtension() { + when(extensionDao.findByNameAndType("my-ext", Extension.Type.NetworkOrchestrator)).thenReturn(mock(ExtensionVO.class)); + + assertTrue(extensionsManager.isNetworkExtensionProvider("my-ext")); + } + + @Test + public void isNetworkExtensionProviderReturnsFalseWhenNoMatch() { + when(extensionDao.findByNameAndType("unknown", Extension.Type.NetworkOrchestrator)).thenReturn(null); + assertFalse(extensionsManager.isNetworkExtensionProvider("unknown")); + } + + @Test + public void isNetworkExtensionProviderReturnsFalseForNullProvider() { + assertFalse(extensionsManager.isNetworkExtensionProvider(null)); + } + + // ----------------------------------------------------------------------- + // Tests for listExtensionsByType + // ----------------------------------------------------------------------- + + @Test + public void listExtensionsByTypeReturnsExtensionsForType() { + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.listByType(Extension.Type.NetworkOrchestrator)).thenReturn(List.of(ext)); + + List result = extensionsManager.listExtensionsByType(Extension.Type.NetworkOrchestrator); + assertEquals(1, result.size()); + assertEquals(ext, result.get(0)); + } + + @Test + public void listExtensionsByTypeReturnsEmptyForNullType() { + List result = extensionsManager.listExtensionsByType(null); + assertTrue(result.isEmpty()); + } + + @Test + public void listExtensionsByTypeReturnsEmptyWhenNoExtensions() { + when(extensionDao.listByType(Extension.Type.NetworkOrchestrator)).thenReturn(Collections.emptyList()); + List result = extensionsManager.listExtensionsByType(Extension.Type.NetworkOrchestrator); + assertTrue(result.isEmpty()); + } + + // ----------------------------------------------------------------------- + // Tests for getNetworkCapabilitiesForProvider + // ----------------------------------------------------------------------- + + @Test + public void getNetworkCapabilitiesForProviderReturnsCapabilitiesFromExtensionDetails() { + long physNetId = 10L; + String providerName = "my-ext"; + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getId()).thenReturn(5L); + doReturn(ext).when(extensionsManager).getExtensionForPhysicalNetworkAndProvider(physNetId, providerName); + + when(extensionDetailsDao.listDetailsKeyPairs(5L)).thenReturn(Map.of( + ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY, + "SourceNat,StaticNat")); + + Map> result = + extensionsManager.getNetworkCapabilitiesForProvider(physNetId, providerName); + assertNotNull(result); + assertTrue(result.containsKey(Network.Service.SourceNat)); + } + + @Test + public void getNetworkCapabilitiesForProviderReturnsEmptyMapForNullProvider() { + Map> result = + extensionsManager.getNetworkCapabilitiesForProvider(10L, null); + assertTrue(result.isEmpty()); + } + + @Test + public void runNetworkCustomActionFailsWhenProviderReturnsNull() { + Network network = mock(Network.class); + when(network.getId()).thenReturn(5L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(actionVO.getUuid()).thenReturn("action-uuid"); + when(actionVO.getName()).thenReturn("unknown-action"); + when(actionVO.getId()).thenReturn(3L); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getName()).thenReturn("my-extnet"); + + Pair, Map> details = new Pair<>(new HashMap<>(), new HashMap<>()); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(3L)).thenReturn(details); + + // networkServiceMapDao returns provider name + when(networkServiceMapDao.getProviderForServiceInNetwork(eq(5L), any())).thenReturn("my-extnet"); + + // element implements both NetworkElement and NetworkCustomActionProvider but action returns null + MockNetworkElement element = mock(MockNetworkElement.class); + when(element.canHandleCustomAction(eq(network))).thenReturn(true); + when(element.runCustomAction(eq(network), eq("unknown-action"), any())).thenReturn(null); + when(networkModel.getElementImplementingProvider("my-extnet")).thenReturn(element); + + CustomActionResultResponse resp = extensionsManager.runNetworkCustomAction( + network, actionVO, extensionVO, + ExtensionCustomAction.ResourceType.Network, new HashMap<>()); + + assertFalse(resp.isSuccess()); + assertTrue(resp.getResult().get(ApiConstants.DETAILS).contains("Action failed")); + } + } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java index 9f7a4ad6e058..c334e91feb84 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java @@ -23,12 +23,30 @@ import com.cloud.utils.db.GenericDao; +import javax.annotation.Nullable; + public interface AsyncJobDao extends GenericDao { AsyncJobVO findInstancePendingAsyncJob(String instanceType, long instanceId); List findInstancePendingAsyncJobs(String instanceType, Long accountId); + /** + * Finds async job matching the given parameters. + * Non-null parameters are added to search criteria. + * Returns the most recent job by creation date. + *

+ * When searching by resourceId and resourceType, only one active job + * is expected per resource, so returning a single result is sufficient. + * + * @param id job ID + * @param resourceId resource ID (instanceId) + * @param resourceType resource type (instanceType) + * @return matching job or null + */ + @Nullable + AsyncJobVO findJob(Long id, Long resourceId, String resourceType); + AsyncJobVO findPseudoJob(long threadId, long msid); void cleanupPseduoJobs(long msid); @@ -50,4 +68,6 @@ public interface AsyncJobDao extends GenericDao { // Returns the number of pending jobs for the given Management server msids. // NOTE: This is the msid and NOT the id long countPendingNonPseudoJobs(Long... msIds); + + List listPendingJobIdsForAccount(long accountId); } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java index a2f1f36b8637..d8385e9aecd1 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java @@ -22,6 +22,8 @@ import java.util.List; import org.apache.cloudstack.api.ApiConstants; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; import org.apache.cloudstack.jobs.JobInfo; @@ -45,6 +47,7 @@ public class AsyncJobDaoImpl extends GenericDaoBase implements private final SearchBuilder expiringUnfinishedAsyncJobSearch; private final SearchBuilder expiringCompletedAsyncJobSearch; private final SearchBuilder failureMsidAsyncJobSearch; + private final SearchBuilder byIdResourceIdResourceTypeSearch; private final GenericSearchBuilder asyncJobTypeSearch; private final GenericSearchBuilder pendingNonPseudoAsyncJobsSearch; @@ -95,6 +98,12 @@ public AsyncJobDaoImpl() { failureMsidAsyncJobSearch.and("job_cmd", failureMsidAsyncJobSearch.entity().getCmd(), Op.IN); failureMsidAsyncJobSearch.done(); + byIdResourceIdResourceTypeSearch = createSearchBuilder(); + byIdResourceIdResourceTypeSearch.and("id", byIdResourceIdResourceTypeSearch.entity().getId(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.and("instanceId", byIdResourceIdResourceTypeSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.and("instanceType", byIdResourceIdResourceTypeSearch.entity().getInstanceType(), SearchCriteria.Op.EQ); + byIdResourceIdResourceTypeSearch.done(); + asyncJobTypeSearch = createSearchBuilder(Long.class); asyncJobTypeSearch.select(null, SearchCriteria.Func.COUNT, asyncJobTypeSearch.entity().getId()); asyncJobTypeSearch.and("job_info", asyncJobTypeSearch.entity().getCmdInfo(),Op.LIKE); @@ -140,6 +149,30 @@ public List findInstancePendingAsyncJobs(String instanceType, Long a return listBy(sc); } + @Override + public AsyncJobVO findJob(Long id, Long resourceId, String resourceType) { + SearchCriteria sc = byIdResourceIdResourceTypeSearch.create(); + + if (id == null && resourceId == null && StringUtils.isBlank(resourceType)) { + logger.debug("findJob called with all null parameters"); + return null; + } + + if (id != null) { + sc.setParameters("id", id); + } + if (resourceId != null && StringUtils.isNotBlank(resourceType)) { + sc.setParameters("instanceType", resourceType); + sc.setParameters("instanceId", resourceId); + } + Filter filter = new Filter(AsyncJobVO.class, "created", false, 0L, 1L); + List result = searchIncludingRemoved(sc, filter, Boolean.FALSE, false); + if (CollectionUtils.isNotEmpty(result)) { + return result.get(0); + } + return null; + } + @Override public AsyncJobVO findPseudoJob(long threadId, long msid) { SearchCriteria sc = pseudoJobSearch.create(); @@ -266,4 +299,14 @@ public long countPendingJobs(String havingInfo, String... cmds) { List results = customSearch(sc, null); return results.get(0); } + + @Override + public List listPendingJobIdsForAccount(long accountId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.EQ); + sb.selectFields(sb.entity().getId()); + SearchCriteria sc = sb.create(); + sc.setParameters("accountId", accountId); + return customSearch(sc, null); + } } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java index 7672b9dc6f97..be0953581dd7 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java @@ -31,6 +31,8 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.inject.Inject; import javax.naming.ConfigurationException; @@ -116,6 +118,8 @@ import org.apache.logging.log4j.ThreadContext; public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager, ClusterManagerListener, Configurable { + private static final Pattern PASSWORD_FIELD_PATTERN = Pattern.compile("\\\"password\\\":\\\"([^\\\"]*)\\\"+"); + // Advanced public static final ConfigKey JobExpireMinutes = new ConfigKey("Advanced", Long.class, "job.expire.minutes", "1440", "Time (in minutes) for async-jobs to be kept in system", true, ConfigKey.Scope.Global); @@ -184,6 +188,7 @@ public class AsyncJobManagerImpl extends ManagerBase implements AsyncJobManager, private volatile long _executionRunNumber = 1; private final ScheduledExecutorService _heartbeatScheduler = Executors.newScheduledThreadPool(1, new NamedThreadFactory("AsyncJobMgr-Heartbeat")); + private final ExecutorService _eventBusPublisher = Executors.newSingleThreadExecutor(new NamedThreadFactory("AsyncJobMgr-EventBus")); private ExecutorService _apiJobExecutor; private ExecutorService _workerJobExecutor; @@ -554,22 +559,26 @@ public AsyncJob queryJob(final long jobId, final boolean updatePollTime) { } public String obfuscatePassword(String result, boolean hidePassword) { - if (hidePassword) { - String pattern = "\"password\":"; - if (result != null) { - if (result.contains(pattern)) { - String[] resp = result.split(pattern); - String psswd = resp[1].toString().split(",")[0]; - if (psswd.endsWith("}")) { - psswd = psswd.substring(0, psswd.length() - 1); - result = resp[0] + pattern + psswd.replace(psswd.substring(2, psswd.length() - 1), "*****") + "}," + resp[1].split(",", 2)[1]; - } else { - result = resp[0] + pattern + psswd.replace(psswd.substring(2, psswd.length() - 1), "*****") + "," + resp[1].split(",", 2)[1]; - } - } - } + if (!hidePassword || StringUtils.isBlank(result)) { + return result; + } + + Matcher matcher = PASSWORD_FIELD_PATTERN.matcher(result); + StringBuilder obfuscatedResult = new StringBuilder(); + while (matcher.find()) { + String password = matcher.group(1); + String replacement = "\"password\":\"" + obfuscatePasswordValue(password) + "\""; + matcher.appendReplacement(obfuscatedResult, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(obfuscatedResult); + return obfuscatedResult.toString(); + } + + private String obfuscatePasswordValue(String password) { + if (StringUtils.isEmpty(password)) { + return password; } - return result; + return password.charAt(0) + "*****"; } private void scheduleExecution(final AsyncJobVO job) { @@ -1378,6 +1387,7 @@ public boolean start() { @Override public boolean stop() { _heartbeatScheduler.shutdown(); + _eventBusPublisher.shutdown(); _apiJobExecutor.shutdown(); _workerJobExecutor.shutdown(); return true; @@ -1397,8 +1407,26 @@ protected AsyncJobManagerImpl() { } private void publishOnEventBus(AsyncJob job, String jobEvent) { - _messageBus.publish(null, AsyncJob.Topics.JOB_EVENT_PUBLISH, PublishScope.LOCAL, - new Pair(job, jobEvent)); + try { + _eventBusPublisher.submit(new ManagedContextRunnable() { + @Override + protected void runInContext() { + publishJobEvent(job, jobEvent); + } + }); + } catch (RejectedExecutionException e) { + logger.warn("Failed to publish async job event, event bus publisher is shut down", e); + } + } + + private void publishJobEvent(AsyncJob job, String jobEvent) { + try { + _messageBus.publish(null, AsyncJob.Topics.JOB_EVENT_PUBLISH, PublishScope.LOCAL, + new Pair<>(job, jobEvent)); + } catch (Throwable t) { + logger.warn("Failed to publish async job event on message bus. jobId={}, jobEvent={}", + job != null ? job.getId() : null, jobEvent, t); + } } @Override diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java index 41eaac598bf3..050fe4e5215c 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/VmWorkJobVO.java @@ -69,6 +69,14 @@ public VmWorkJobVO(String related) { setRelated(related); } + public VmWorkJobVO(String related, long userId, long accountId, String cmd, Long instanceId, VirtualMachine.Type vmType, Step step) { + super(null, userId, accountId, cmd, null, instanceId, null, null); + setRelated(related); + this.vmType = vmType; + this.step = step; + this.vmInstanceId = instanceId; + } + public Step getStep() { return step; } diff --git a/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/AsyncJobManagerTest.java b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/AsyncJobManagerTest.java index 7130873e4eed..f3cd37188456 100644 --- a/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/AsyncJobManagerTest.java +++ b/framework/jobs/src/test/java/org/apache/cloudstack/framework/jobs/AsyncJobManagerTest.java @@ -17,12 +17,15 @@ package org.apache.cloudstack.framework.jobs; import org.apache.cloudstack.framework.jobs.impl.AsyncJobManagerImpl; +import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.utils.HumanReadableJson; + @RunWith (MockitoJUnitRunner.class) public class AsyncJobManagerTest { @@ -37,6 +40,12 @@ public class AsyncJobManagerTest { String inputNoBraces = "\"password\":\"password\"\",\"action\":\"OFF\""; String expectedNoBraces = "\"password\":\"p*****\",\"action\":\"OFF\""; + String realUserVmResponseWithPasswordInput = "{\"id\":\"f75b0990-5801-4b78-bcb0-58a503afa49c\",\"name\":\"pw-vm\"," + + "\"displayname\":\"pw-vm\",\"account\":\"admin\",\"password\":\"67wSK5\",\"instancename\":\"i-2-17-VM\"," + + "\"details\":{\"password\":\"3WTVryPJZJwMZGcJJ+OOYf84+uixk/1FraomPG9N6/Uvng\\u003d\\u003d\"," + + "\"Message.ReservedCapacityFreed.Flag\":\"true\",\"rootDiskController\":\"osdefault\"}," + + "\"arch\":\"x86_64\",\"jobid\":\"c13865d3-61ec-4269-979a-3d799181d5fe\",\"jobstatus\":0}"; + @Test public void obfuscatePasswordTest() { String result = asyncJobManager.obfuscatePassword(input, true); @@ -79,4 +88,15 @@ public void obfuscatePasswordTestHidePasswordNoPassword() { Assert.assertEquals(noPassword, result); } + @Test + public void obfuscatePasswordTestHidePasswordRealInput() { + String result = asyncJobManager.obfuscatePassword(realUserVmResponseWithPasswordInput, true); + + Assert.assertNotNull(result); + Assert.assertFalse(result.contains("\"password\":\"3WTVryPJZJwMZGcJJ+OOYf84+uixk\"")); + String jsonObject = HumanReadableJson.getHumanReadableBytesJson(result); + Assert.assertTrue(StringUtils.isNotEmpty(jsonObject)); + Assert.assertTrue(jsonObject.contains("\"password\":\"3*****\"")); + } + } diff --git a/framework/kms/pom.xml b/framework/kms/pom.xml new file mode 100644 index 000000000000..719072ac493a --- /dev/null +++ b/framework/kms/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + cloud-framework-kms + Apache CloudStack Framework - Key Management Service + Core KMS framework with provider-agnostic interfaces + + + org.apache.cloudstack + cloudstack-framework + 4.23.0.0-SNAPSHOT + ../pom.xml + + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-framework-config + ${project.version} + + + diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java new file mode 100644 index 000000000000..8f15ad24ac6e --- /dev/null +++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java @@ -0,0 +1,181 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.kms; + +import com.cloud.utils.exception.CloudRuntimeException; + +/** + * Exception class for KMS-related errors with structured error types + * to enable proper retry logic and error handling. + */ +public class KMSException extends CloudRuntimeException { + + /** + * Error types for KMS operations to enable intelligent retry logic + */ + public enum ErrorType { + CONNECTION_FAILED(true), + /** + * Authentication failed (e.g., incorrect PIN) + */ + AUTHENTICATION_FAILED(false), + /** + * Provider not initialized or unavailable + */ + PROVIDER_NOT_INITIALIZED(false), + + /** + * KEK not found in backend + */ + KEK_NOT_FOUND(false), + + /** + * KEK with given label already exists + */ + KEY_ALREADY_EXISTS(false), + + /** + * Invalid parameters provided + */ + INVALID_PARAMETER(false), + + /** + * Wrap/unwrap operation failed + */ + WRAP_UNWRAP_FAILED(true), + + /** + * KEK operation (create/delete) failed + */ + KEK_OPERATION_FAILED(true), + + /** + * Health check failed + */ + HEALTH_CHECK_FAILED(true), + + /** + * Transient network or communication error + */ + TRANSIENT_ERROR(true), + + /** + * Unknown error + */ + UNKNOWN(false); + + private final boolean retryable; + + ErrorType(boolean retryable) { + this.retryable = retryable; + } + + public boolean isRetryable() { + return retryable; + } + } + + private final ErrorType errorType; + + public KMSException(String message) { + super(message); + this.errorType = ErrorType.UNKNOWN; + } + + public KMSException(String message, Throwable cause) { + super(message, cause); + this.errorType = ErrorType.UNKNOWN; + } + + public KMSException(ErrorType errorType, String message) { + super(message); + this.errorType = errorType; + } + + public KMSException(ErrorType errorType, String message, Throwable cause) { + super(message, cause); + this.errorType = errorType; + } + + public static KMSException providerNotInitialized(String details) { + return new KMSException(ErrorType.PROVIDER_NOT_INITIALIZED, + "KMS provider not initialized: " + details); + } + + public static KMSException kekNotFound(String kekId) { + return new KMSException(ErrorType.KEK_NOT_FOUND, + "KEK not found: " + kekId); + } + + public static KMSException keyAlreadyExists(String details) { + return new KMSException(ErrorType.KEY_ALREADY_EXISTS, + "Key already exists: " + details); + } + + public static KMSException invalidParameter(String details) { + return new KMSException(ErrorType.INVALID_PARAMETER, + "Invalid parameter: " + details); + } + + public static KMSException wrapUnwrapFailed(String details, Throwable cause) { + return new KMSException(ErrorType.WRAP_UNWRAP_FAILED, + "Wrap/unwrap operation failed: " + details, cause); + } + + public static KMSException wrapUnwrapFailed(String details) { + return new KMSException(ErrorType.WRAP_UNWRAP_FAILED, + "Wrap/unwrap operation failed: " + details); + } + + public static KMSException kekOperationFailed(String details, Throwable cause) { + return new KMSException(ErrorType.KEK_OPERATION_FAILED, + "KEK operation failed: " + details, cause); + } + + public static KMSException kekOperationFailed(String details) { + return new KMSException(ErrorType.KEK_OPERATION_FAILED, + "KEK operation failed: " + details); + } + + public static KMSException healthCheckFailed(String details, Throwable cause) { + return new KMSException(ErrorType.HEALTH_CHECK_FAILED, + "Health check failed: " + details, cause); + } + + public static KMSException transientError(String details, Throwable cause) { + return new KMSException(ErrorType.TRANSIENT_ERROR, + "Transient error: " + details, cause); + } + + public ErrorType getErrorType() { + return errorType; + } + + @Override + public String toString() { + return "KMSException{" + + "errorType=" + errorType + + ", retryable=" + isRetryable() + + ", message='" + getMessage() + '\'' + + '}'; + } + + public boolean isRetryable() { + return errorType.isRetryable(); + } +} diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java new file mode 100644 index 000000000000..388d464caa75 --- /dev/null +++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java @@ -0,0 +1,255 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.kms; + +import com.cloud.utils.component.Adapter; +import org.apache.cloudstack.framework.config.Configurable; + +/** + * Abstract provider contract for Key Management Service operations. + *

+ * Implementations provide the cryptographic backend (HSM via PKCS#11, database, cloud KMS, etc.) + * for secure key wrapping/unwrapping using envelope encryption. + *

+ * Design principles: + * - KEKs (Key Encryption Keys) never leave the secure backend + * - DEKs (Data Encryption Keys) are wrapped by KEKs for storage + * - Plaintext DEKs exist only transiently in memory during wrap/unwrap + * - All operations are purpose-scoped to prevent key reuse + *

+ * Thread-safety: Implementations must be thread-safe for concurrent operations. + */ +public interface KMSProvider extends Configurable, Adapter { + + /** + * Returns {@code true} if the given HSM profile configuration key name refers + * to a + * sensitive value (PIN, password, secret, or private key) that must be + * encrypted at + * rest and masked in API responses. + * + *

+ * This is a shared naming-convention helper used by both KMS providers (when + * loading/storing profile details) and the KMS manager (when building API + * responses). + * + * @param key configuration key name (case-insensitive); null returns false + * @return true if the key is considered sensitive + */ + static boolean isSensitiveKey(String key) { + if (key == null) { + return false; + } + return key.equalsIgnoreCase("pin") || + key.equalsIgnoreCase("password") || + key.toLowerCase().contains("secret") || + key.equalsIgnoreCase("private_key"); + } + + /** + * Get the unique name of this provider + * + * @return provider name (e.g., "database", "pkcs11") + */ + String getProviderName(); + + /** + * Create a new Key Encryption Key (KEK) in the secure backend. + * Delegates to {@link #createKek(KeyPurpose, String, int, Long)} with null profile ID. + * + * @param purpose the purpose/scope for this KEK + * @param label human-readable label for the KEK (must be unique within purpose) + * @param keyBits key size in bits (typically 128, 192, or 256) + * @return the KEK identifier (label or handle) for later reference + * @throws KMSException if KEK creation fails + */ + default String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException { + return createKek(purpose, label, keyBits, null); + } + + /** + * Create a new Key Encryption Key (KEK) in the secure backend with explicit HSM profile. + * + * @param purpose the purpose/scope for this KEK + * @param label human-readable label for the KEK (must be unique within purpose) + * @param keyBits key size in bits (typically 128, 192, or 256) + * @param hsmProfileId optional HSM profile ID to create the KEK in (null for auto-resolution/default) + * @return the KEK identifier (label or handle) for later reference + * @throws KMSException if KEK creation fails + */ + String createKek(KeyPurpose purpose, String label, int keyBits, Long hsmProfileId) throws KMSException; + + /** + * Delete a KEK from the secure backend. + * WARNING: This will make all DEKs wrapped by this KEK unrecoverable. + * + * @param kekId the KEK identifier to delete + * @throws KMSException if deletion fails or KEK not found + */ + void deleteKek(String kekId) throws KMSException; + + /** + * Validates the configuration details for this provider before saving an HSM + * profile. + * Implementations should override this to perform provider-specific validation. + * + * @param details the configuration details to validate + * @throws KMSException if validation fails + */ + default void validateProfileConfig(java.util.Map details) throws KMSException { + // default no-op + } + + /** + * Check if a KEK exists and is accessible + * + * @param kekId the KEK identifier to check + * @return true if KEK is available + * @throws KMSException if check fails + */ + boolean isKekAvailable(String kekId) throws KMSException; + + /** + * Wrap (encrypt) a plaintext Data Encryption Key with a KEK. + * Delegates to {@link #wrapKey(byte[], KeyPurpose, String, Long)} with null profile ID. + * + * @param plainDek the plaintext DEK to wrap (caller must zeroize after call) + * @param purpose the intended purpose of this DEK + * @param kekLabel the label of the KEK to use for wrapping + * @return WrappedKey containing the encrypted DEK and metadata + * @throws KMSException if wrapping fails or KEK not found + */ + default WrappedKey wrapKey(byte[] plainDek, KeyPurpose purpose, String kekLabel) throws KMSException { + return wrapKey(plainDek, purpose, kekLabel, null); + } + + /** + * Wrap (encrypt) a plaintext Data Encryption Key with a KEK using explicit HSM profile. + * + * @param plainDek the plaintext DEK to wrap (caller must zeroize after call) + * @param purpose the intended purpose of this DEK + * @param kekLabel the label of the KEK to use for wrapping + * @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default) + * @return WrappedKey containing the encrypted DEK and metadata + * @throws KMSException if wrapping fails or KEK not found + */ + WrappedKey wrapKey(byte[] plainDek, KeyPurpose purpose, String kekLabel, Long hsmProfileId) throws KMSException; + + /** + * Unwrap (decrypt) a wrapped DEK to obtain the plaintext key. + * Delegates to {@link #unwrapKey(WrappedKey, Long)} with null profile ID. + *

+ * SECURITY: Caller MUST zeroize the returned byte array after use + * + * @param wrappedKey the wrapped key to decrypt + * @return plaintext DEK (caller must zeroize!) + * @throws KMSException if unwrapping fails or KEK not found + */ + default byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException { + return unwrapKey(wrappedKey, null); + } + + /** + * Unwrap (decrypt) a wrapped DEK to obtain the plaintext key using explicit HSM profile. + *

+ * SECURITY: Caller MUST zeroize the returned byte array after use + * + * @param wrappedKey the wrapped key to decrypt + * @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default) + * @return plaintext DEK (caller must zeroize!) + * @throws KMSException if unwrapping fails or KEK not found + */ + byte[] unwrapKey(WrappedKey wrappedKey, Long hsmProfileId) throws KMSException; + + /** + * Generate a new random DEK and immediately wrap it with a KEK. + * Delegates to {@link #generateAndWrapDek(KeyPurpose, String, int, Long)} with null profile ID. + * (convenience method combining generation + wrapping) + * + * @param purpose the intended purpose of the new DEK + * @param kekLabel the label of the KEK to use for wrapping + * @param keyBits DEK size in bits (typically 128, 192, or 256) + * @return WrappedKey containing the newly generated and wrapped DEK + * @throws KMSException if generation or wrapping fails + */ + default WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException { + return generateAndWrapDek(purpose, kekLabel, keyBits, null); + } + + /** + * Generate a new random DEK and immediately wrap it with a KEK using explicit HSM profile. + * (convenience method combining generation + wrapping) + * + * @param purpose the intended purpose of the new DEK + * @param kekLabel the label of the KEK to use for wrapping + * @param keyBits DEK size in bits (typically 128, 192, or 256) + * @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default) + * @return WrappedKey containing the newly generated and wrapped DEK + * @throws KMSException if generation or wrapping fails + */ + WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits, + Long hsmProfileId) throws KMSException; + + /** + * Rewrap a DEK with a different KEK (used during key rotation). + * Delegates to {@link #rewrapKey(WrappedKey, String, Long)} with null profile ID. + * This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK. + * + * @param oldWrappedKey the currently wrapped key + * @param newKekLabel the label of the new KEK to wrap with + * @return new WrappedKey encrypted with the new KEK + * @throws KMSException if rewrapping fails + */ + default WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException { + return rewrapKey(oldWrappedKey, newKekLabel, null); + } + + /** + * Rewrap a DEK with a different KEK (used during key rotation) using explicit target HSM profile. + * This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK. + * + * @param oldWrappedKey the currently wrapped key + * @param newKekLabel the label of the new KEK to wrap with + * @param targetHsmProfileId optional target HSM profile ID to wrap with (null for auto-resolution/default) + * @return new WrappedKey encrypted with the new KEK + * @throws KMSException if rewrapping fails + */ + WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel, Long targetHsmProfileId) throws KMSException; + + /** + * Perform health check on the provider backend + * + * @return true if provider is healthy and operational + * @throws KMSException if health check fails with critical error + */ + boolean healthCheck() throws KMSException; + + /** + * Invalidates any cached state (config, sessions) associated with the given HSM profile. + * Must be called after an HSM profile is updated or deleted so that the next operation + * re-reads the profile details from the database instead of using stale cached values. + * + *

Providers that do not cache per-profile state (e.g. the database provider) can + * leave this as a no-op. + * + * @param profileId the HSM profile ID whose cache should be evicted + */ + default void invalidateProfileCache(Long profileId) { + // no-op for providers that don't cache per-profile state + } +} diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java new file mode 100644 index 000000000000..41f5cf461fcc --- /dev/null +++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.kms; + +/** + * Defines the purpose/usage scope for cryptographic keys in the KMS system. + * This enables proper key segregation and prevents key reuse across different contexts. + */ +public enum KeyPurpose { + /** + * Keys used for encrypting VM disk volumes (LUKS, encrypted storage) + */ + VOLUME_ENCRYPTION("volume", "Volume disk encryption keys"), + + /** + * Keys used for protecting TLS certificate private keys + */ + TLS_CERT("tls", "TLS certificate private keys"); + + private final String name; + private final String description; + + KeyPurpose(String name, String description) { + this.name = name; + this.description = description; + } + + /** + * Convert string name to KeyPurpose enum + * + * @param name the string representation of the purpose + * @return matching KeyPurpose + * @throws IllegalArgumentException if no matching purpose found + */ + public static KeyPurpose fromString(String name) { + for (KeyPurpose purpose : KeyPurpose.values()) { + if (purpose.getName().equalsIgnoreCase(name)) { + return purpose; + } + } + throw new IllegalArgumentException("Unknown KeyPurpose: " + name); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + /** + * Generate a globally unique, collision-resistant KEK label with context + * + * @param domainId the domain ID associated with this key + * @param accountId the account ID associated with this key + * @param uuid the unique identifier of the key entity + * @param version the version number of the key + * @return formatted KEK label (e.g., "volume-kek-1-2-a8054d8f-...-1") + */ + public String generateKekLabel(long domainId, long accountId, String uuid, int version) { + return name + "-kek-" + domainId + "-" + accountId + "-" + uuid.replace("-", "") + "-v" + version; + } +} diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java new file mode 100644 index 000000000000..e70c5e32c46a --- /dev/null +++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java @@ -0,0 +1,131 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.framework.kms; + +import java.util.Arrays; +import java.util.Date; +import java.util.Objects; + +/** + * Immutable Data Transfer Object representing an encrypted (wrapped) Data Encryption Key. + * The wrapped key material contains the DEK encrypted by a Key Encryption Key (KEK) + * stored in a secure backend (HSM, database, etc.). + *

+ * This follows the envelope encryption pattern: + * - DEK: encrypts actual data (e.g., disk volume) + * - KEK: encrypts the DEK (never leaves secure storage) + * - Wrapped Key: DEK encrypted by KEK, safe to store in database + */ +public class WrappedKey { + private final String uuid; + private final String kekId; + private final KeyPurpose purpose; + private final String algorithm; + private final byte[] wrappedKeyMaterial; + private final String providerName; + private final Date created; + private final Long zoneId; + + /** + * Create a new WrappedKey instance + * + * @param kekId ID/label of the KEK used to wrap this key + * @param purpose the intended use of this key + * @param algorithm encryption algorithm (e.g., "AES/GCM/NoPadding") + * @param wrappedKeyMaterial the encrypted DEK blob + * @param providerName name of the KMS provider that created this key + * @param created timestamp when key was wrapped + * @param zoneId optional zone ID for zone-scoped keys + */ + public WrappedKey(String kekId, KeyPurpose purpose, String algorithm, + byte[] wrappedKeyMaterial, String providerName, + Date created, Long zoneId) { + this(null, kekId, purpose, algorithm, wrappedKeyMaterial, providerName, created, zoneId); + } + + /** + * Constructor for database-loaded keys with ID + */ + public WrappedKey(String uuid, String kekId, KeyPurpose purpose, String algorithm, + byte[] wrappedKeyMaterial, String providerName, + Date created, Long zoneId) { + this.uuid = uuid; + this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null"); + this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null"); + this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null"); + this.providerName = providerName; + + if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) { + throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty"); + } + this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length); + + this.created = created != null ? new Date(created.getTime()) : new Date(); + this.zoneId = zoneId; + } + + public String getUuid() { + return uuid; + } + + public String getKekId() { + return kekId; + } + + public KeyPurpose getPurpose() { + return purpose; + } + + public String getAlgorithm() { + return algorithm; + } + + /** + * Get wrapped key material. Returns a defensive copy to prevent modification. + * Caller is responsible for zeroizing the returned array after use. + */ + public byte[] getWrappedKeyMaterial() { + return Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length); + } + + public String getProviderName() { + return providerName; + } + + public Date getCreated() { + return created != null ? new Date(created.getTime()) : null; + } + + public Long getZoneId() { + return zoneId; + } + + @Override + public String toString() { + return "WrappedKey{" + + "uuid='" + uuid + '\'' + + ", kekId='" + kekId + '\'' + + ", purpose=" + purpose + + ", algorithm='" + algorithm + '\'' + + ", providerName='" + providerName + '\'' + + ", materialLength=" + (wrappedKeyMaterial != null ? wrappedKeyMaterial.length : 0) + + ", created=" + created + + ", zoneId=" + zoneId + + '}'; + } +} diff --git a/framework/pom.xml b/framework/pom.xml index 337e5b0268b2..95d0bd0694c6 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -54,6 +54,7 @@ extensions ipc jobs + kms managed-context quota rest diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java new file mode 100644 index 000000000000..cc6ca203ef79 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.quota; + +import org.apache.commons.lang3.StringUtils; + +public enum QuotaAccountStateFilter { + ALL, ACTIVE, REMOVED; + + public static QuotaAccountStateFilter getValue(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + for (QuotaAccountStateFilter state : values()) { + if (state.name().equalsIgnoreCase(value)) { + return state; + } + } + return null; + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 7949259bc821..5afef8bc95b6 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -32,6 +33,9 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.quota.activationrule.presetvariables.Configuration; import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable; @@ -43,9 +47,11 @@ import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -86,7 +92,8 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { private QuotaBalanceDao _quotaBalanceDao; @Inject private ConfigurationDao _configDao; - + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; @Inject protected PresetVariableHelper presetVariableHelper; @@ -150,8 +157,9 @@ protected void processQuotaBalanceForAccount(AccountVO accountVo, List> periods = accountQuotaUsages.stream() @@ -215,7 +223,7 @@ protected BigDecimal retrieveBalanceForUsageCalculation(long accountId, long dom logger.debug(String.format("Persisting the first quota balance [%s] for account [%s].", firstBalance, accountToString)); _quotaBalanceDao.saveQuotaBalance(firstBalance); } else { - QuotaBalanceVO lastRealBalance = _quotaBalanceDao.findLastBalanceEntry(accountId, domainId, startDate); + QuotaBalanceVO lastRealBalance = _quotaBalanceDao.getLastQuotaBalanceEntry(accountId, domainId, startDate); if (lastRealBalance == null) { logger.warn("Account [{}] has quota usage entries, however it does not have a quota balance.", accountToString); @@ -244,7 +252,7 @@ protected void saveQuotaAccount(long accountId, BigDecimal aggregatedUsage, Date } protected BigDecimal aggregateCreditBetweenDates(Long accountId, Long domainId, Date startDate, Date endDate, String accountToString) { - List creditsReceived = _quotaBalanceDao.findCreditBalance(accountId, domainId, startDate, endDate); + List creditsReceived = _quotaBalanceDao.findCreditBalances(accountId, domainId, startDate, endDate); logger.debug("Account [{}] has [{}] credit entries before [{}].", accountToString, creditsReceived.size(), DateUtil.displayDateInTimezone(usageAggregationTimeZone, endDate)); @@ -310,14 +318,14 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO String accountToString = account.reflectionToString(); logger.info("Calculating quota usage of [{}] usage records for account [{}].", usageRecords.size(), accountToString); - List> pairsUsageAndQuotaUsage = new ArrayList<>(); + Map>> mapUsageAndQuotaUsage = new LinkedHashMap<>(); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); if (!shouldCalculateUsageRecord(account, usageRecord)) { - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, null)); + mapUsageAndQuotaUsage.put(usageRecord, null); continue; } @@ -325,18 +333,31 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO List quotaTariffs = pairQuotaTariffsPerUsageTypeAndHasActivationRule.first(); boolean hasAnyQuotaTariffWithActivationRule = pairQuotaTariffsPerUsageTypeAndHasActivationRule.second(); - BigDecimal aggregatedQuotaTariffsValue = aggregateQuotaTariffsValues(usageRecord, quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + Map aggregatedQuotaTariffsAndValues = aggregateQuotaTariffsValues(usageRecord, + quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + BigDecimal aggregatedQuotaTariffsValue = aggregatedQuotaTariffsAndValues.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); + logger.debug("The aggregation of the quota tariffs of account [{}] resulted in [{}] for the usage record [{}].", + account, aggregatedQuotaTariffsValue, usageRecord); QuotaUsageVO quotaUsage = createQuotaUsageAccordingToUsageUnit(usageRecord, aggregatedQuotaTariffsValue, accountToString); + if (quotaUsage == null) { + mapUsageAndQuotaUsage.put(usageRecord, null); + continue; + } - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, quotaUsage)); + List quotaTariffUsages = new ArrayList<>(); + for (Map.Entry entry : aggregatedQuotaTariffsAndValues.entrySet()) { + QuotaTariffUsageVO quotaTariffUsage = createQuotaTariffUsage(usageRecord, entry.getKey(), entry.getValue()); + quotaTariffUsages.add(quotaTariffUsage); + } + mapUsageAndQuotaUsage.put(usageRecord, new Pair<>(quotaUsage, quotaTariffUsages)); } } catch (Exception e) { logger.error(String.format("Failed to calculate the quota usage for account [%s] due to [%s].", accountToString, e.getMessage()), e); return new ArrayList<>(); } - return persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(pairsUsageAndQuotaUsage); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageAndQuotaUsage)); } protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageRecord) { @@ -348,31 +369,41 @@ protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageR return true; } - protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(List> pairsUsageAndQuotaUsage) { + protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(Map>> mapUsageAndQuotaTariffUsage) { List quotaUsages = new ArrayList<>(); - for (Pair pairUsageAndQuotaUsage : pairsUsageAndQuotaUsage) { - UsageVO usageVo = pairUsageAndQuotaUsage.first(); + for (Map.Entry>> usageAndTariffUsage : mapUsageAndQuotaTariffUsage.entrySet()) { + UsageVO usageVo = usageAndTariffUsage.getKey(); usageVo.setQuotaCalculated(1); _usageDao.persistUsage(usageVo); - QuotaUsageVO quotaUsageVo = pairUsageAndQuotaUsage.second(); - if (quotaUsageVo != null) { - _quotaUsageDao.persistQuotaUsage(quotaUsageVo); - quotaUsages.add(quotaUsageVo); + Pair> pairUsageAndTariffUsages = usageAndTariffUsage.getValue(); + if (pairUsageAndTariffUsages != null) { + QuotaUsageVO quotaUsage = pairUsageAndTariffUsages.first(); + _quotaUsageDao.persistQuotaUsage(quotaUsage); + quotaUsages.add(quotaUsage); + + persistQuotaTariffUsages(pairUsageAndTariffUsages.second(), quotaUsage.getId()); } } return quotaUsages; } - protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, - JsInterpreter jsInterpreter, String accountToString) { + protected void persistQuotaTariffUsages(List quotaTariffUsages, Long quotaUsageId) { + for (QuotaTariffUsageVO quotaTariffUsage : quotaTariffUsages) { + quotaTariffUsage.setQuotaUsageId(quotaUsageId); + quotaTariffUsageDao.persistQuotaTariffUsage(quotaTariffUsage); + } + } + + protected Map aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, + JsInterpreter jsInterpreter, String accountToString) { String usageRecordToString = usageRecord.toString(usageAggregationTimeZone); logger.debug("Validating usage record [{}] for account [{}] against [{}] quota tariffs.", usageRecordToString, accountToString, quotaTariffs.size()); PresetVariables presetVariables = getPresetVariables(hasAnyQuotaTariffWithActivationRule, usageRecord); - BigDecimal aggregatedQuotaTariffsValue = BigDecimal.ZERO; + Map aggregatedQuotaTariffsAndValues = new HashMap<>(); quotaTariffs.sort(Comparator.comparing(QuotaTariffVO::getPosition)); @@ -381,10 +412,9 @@ protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List *

    @@ -429,7 +479,7 @@ protected BigDecimal getQuotaTariffValueToBeApplied(QuotaTariffVO quotaTariff, J } injectPresetVariablesIntoJsInterpreter(jsInterpreter, presetVariables); - jsInterpreter.injectVariable("lastTariffs", lastAppliedTariffsList.toString()); + jsInterpreter.injectVariable("lastTariffs", lastAppliedTariffsList); String scriptResult = jsInterpreter.executeScript(activationRule).toString(); @@ -459,12 +509,12 @@ protected BigDecimal getQuotaTariffValueToBeApplied(QuotaTariffVO quotaTariff, J protected void injectPresetVariablesIntoJsInterpreter(JsInterpreter jsInterpreter, PresetVariables presetVariables) { jsInterpreter.discardCurrentVariables(); - jsInterpreter.injectVariable("account", presetVariables.getAccount().toString()); - jsInterpreter.injectVariable("domain", presetVariables.getDomain().toString()); + jsInterpreter.injectVariable("account", presetVariables.getAccount()); + jsInterpreter.injectVariable("domain", presetVariables.getDomain()); GenericPresetVariable project = presetVariables.getProject(); if (project != null) { - jsInterpreter.injectVariable("project", project.toString()); + jsInterpreter.injectVariable("project", project); } @@ -474,8 +524,8 @@ protected void injectPresetVariablesIntoJsInterpreter(JsInterpreter jsInterprete } jsInterpreter.injectVariable("resourceType", presetVariables.getResourceType()); - jsInterpreter.injectVariable("value", presetVariables.getValue().toString()); - jsInterpreter.injectVariable("zone", presetVariables.getZone().toString()); + jsInterpreter.injectVariable("value", presetVariables.getValue()); + jsInterpreter.injectVariable("zone", presetVariables.getZone()); } /** diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java index 2420d577f10b..e34c8d3f31dd 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java @@ -36,7 +36,6 @@ public Role getRole() { public void setRole(Role role) { this.role = role; - fieldNamesToIncludeInToString.add("role"); } public String getCreated() { @@ -45,6 +44,5 @@ public String getCreated() { public void setCreated(Date created) { this.created = DateUtil.displayDateInTimezone(TimeZone.getTimeZone("GMT"), created); - fieldNamesToIncludeInToString.add("created"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java index d8457d294ec3..e3927d967f78 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java @@ -29,6 +29,5 @@ public String getExternalId() { public void setExternalId(String externalId) { this.externalId = externalId; - fieldNamesToIncludeInToString.add("externalId"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java index 09182711ca8a..74cb695010ba 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java @@ -32,7 +32,6 @@ public boolean isCustomized() { public void setCustomized(boolean customized) { this.customized = customized; - fieldNamesToIncludeInToString.add("customized"); } public boolean offerHa() { @@ -41,7 +40,5 @@ public boolean offerHa() { public void setOfferHa(boolean offerHa) { this.offerHa = offerHa; - fieldNamesToIncludeInToString.add("offerHa"); } - } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java index e59f78af8d97..48fee552c5a0 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java @@ -30,6 +30,5 @@ public boolean getForceHa() { public void setForceHa(boolean forceHa) { this.forceHa = forceHa; - fieldNamesToIncludeInToString.add("forceHa"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java index b2f5f69502fb..68ba6a331ebe 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java @@ -61,7 +61,6 @@ public Long getBytesReadRate() { public void setBytesReadRate(Long bytesReadRate) { this.bytesReadRate = bytesReadRate; - fieldNamesToIncludeInToString.add("bytesReadRate"); } public Long getBytesReadBurst() { @@ -70,7 +69,6 @@ public Long getBytesReadBurst() { public void setBytesReadBurst(Long bytesReadBurst) { this.bytesReadBurst = bytesReadBurst; - fieldNamesToIncludeInToString.add("bytesReadBurst"); } public Long getBytesReadBurstLength() { @@ -79,7 +77,6 @@ public Long getBytesReadBurstLength() { public void setBytesReadBurstLength(Long bytesReadBurstLength) { this.bytesReadBurstLength = bytesReadBurstLength; - fieldNamesToIncludeInToString.add("bytesReadBurstLength"); } public Long getBytesWriteRate() { @@ -88,7 +85,6 @@ public Long getBytesWriteRate() { public void setBytesWriteRate(Long bytesWriteRate) { this.bytesWriteRate = bytesWriteRate; - fieldNamesToIncludeInToString.add("bytesWriteRate"); } public Long getBytesWriteBurst() { @@ -97,7 +93,6 @@ public Long getBytesWriteBurst() { public void setBytesWriteBurst(Long bytesWriteBurst) { this.bytesWriteBurst = bytesWriteBurst; - fieldNamesToIncludeInToString.add("bytesWriteBurst"); } public Long getBytesWriteBurstLength() { @@ -106,7 +101,6 @@ public Long getBytesWriteBurstLength() { public void setBytesWriteBurstLength(Long bytesWriteBurstLength) { this.bytesWriteBurstLength = bytesWriteBurstLength; - fieldNamesToIncludeInToString.add("bytesWriteBurstLength"); } public Long getIopsReadRate() { @@ -115,7 +109,6 @@ public Long getIopsReadRate() { public void setIopsReadRate(Long iopsReadRate) { this.iopsReadRate = iopsReadRate; - fieldNamesToIncludeInToString.add("iopsReadRate"); } public Long getIopsReadBurst() { @@ -124,7 +117,6 @@ public Long getIopsReadBurst() { public void setIopsReadBurst(Long iopsReadBurst) { this.iopsReadBurst = iopsReadBurst; - fieldNamesToIncludeInToString.add("iopsReadBurst"); } public Long getIopsReadBurstLength() { @@ -133,7 +125,6 @@ public Long getIopsReadBurstLength() { public void setIopsReadBurstLength(Long iopsReadBurstLength) { this.iopsReadBurstLength = iopsReadBurstLength; - fieldNamesToIncludeInToString.add("iopsReadBurstLength"); } public Long getIopsWriteRate() { @@ -142,7 +133,6 @@ public Long getIopsWriteRate() { public void setIopsWriteRate(Long iopsWriteRate) { this.iopsWriteRate = iopsWriteRate; - fieldNamesToIncludeInToString.add("iopsWriteRate"); } public Long getIopsWriteBurst() { @@ -151,7 +141,6 @@ public Long getIopsWriteBurst() { public void setIopsWriteBurst(Long iopsWriteBurst) { this.iopsWriteBurst = iopsWriteBurst; - fieldNamesToIncludeInToString.add("iopsWriteBurst"); } public Long getIopsWriteBurstLength() { @@ -160,6 +149,5 @@ public Long getIopsWriteBurstLength() { public void setIopsWriteBurstLength(Long iopsWriteBurstLength) { this.iopsWriteBurstLength = iopsWriteBurstLength; - fieldNamesToIncludeInToString.add("iopsWriteBurstLength"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java index 6d83da4cd8fb..cbdfa3e4bb42 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java @@ -27,7 +27,6 @@ public String getPath() { public void setPath(String path) { this.path = path; - fieldNamesToIncludeInToString.add("path"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java index 7073d2760d73..4db099ca479c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java @@ -17,10 +17,8 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; -import java.util.HashSet; -import java.util.Set; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; public class GenericPresetVariable { @PresetVariableDefinition(description = "ID of the resource.") @@ -29,15 +27,12 @@ public class GenericPresetVariable { @PresetVariableDefinition(description = "Name of the resource.") private String name; - protected transient Set fieldNamesToIncludeInToString = new HashSet<>(); - public String getId() { return id; } public void setId(String id) { this.id = id; - fieldNamesToIncludeInToString.add("id"); } public String getName() { @@ -46,15 +41,10 @@ public String getName() { public void setName(String name) { this.name = name; - fieldNamesToIncludeInToString.add("name"); } - /*** - * Converts the preset variable into a valid JSON object that will be injected into the JS interpreter. - * This method should not be overridden or changed. - */ @Override - public final String toString() { - return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, fieldNamesToIncludeInToString.toArray(new String[0])); + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java index 4a0fd2f5a078..6d54a1438340 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java @@ -32,7 +32,6 @@ public List getTags() { public void setTags(List tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public Boolean getIsTagARule() { @@ -41,6 +40,5 @@ public Boolean getIsTagARule() { public void setIsTagARule(Boolean isTagARule) { this.isTagARule = isTagARule; - fieldNamesToIncludeInToString.add("isTagARule"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java index 2a6ad132f63e..23020292027c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java @@ -28,12 +28,14 @@ import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterDetailsVO; import com.cloud.host.HostTagVO; +import com.cloud.hypervisor.Hypervisor; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.vpc.VpcOfferingVO; import com.cloud.network.vpc.VpcVO; import javax.inject.Inject; -import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.StoragePoolTagVO; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.backup.BackupOfferingVO; @@ -66,6 +68,7 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.host.dao.HostTagsDao; +import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.server.ResourceTag; @@ -191,6 +194,9 @@ public class PresetVariableHelper { @Inject ClusterDetailsDao clusterDetailsDao; + @Inject + VpcOfferingDao vpcOfferingDao; + protected boolean backupSnapshotAfterTakingSnapshot = SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value(); private List runningAndAllocatedVmUsageTypes = Arrays.asList(UsageTypes.RUNNING_VM, UsageTypes.ALLOCATED_VM); @@ -254,7 +260,7 @@ protected Role getPresetVariableRole(Long roleId) { Role role = new Role(); role.setId(roleVo.getUuid()); role.setName(roleVo.getName()); - role.setType(roleVo.getRoleType()); + role.setType(roleVo.getRoleType().toString()); return role; } @@ -538,8 +544,8 @@ protected void loadPresetVariableValueForVolume(UsageVO usageRecord, Value value value.setDiskOffering(getPresetVariableValueDiskOffering(volumeVo.getDiskOfferingId())); value.setId(volumeVo.getUuid()); value.setName(volumeVo.getName()); - value.setProvisioningType(volumeVo.getProvisioningType()); - value.setVolumeType(volumeVo.getVolumeType()); + value.setVolumeType(volumeVo.getVolumeType().toString()); + value.setProvisioningType(volumeVo.getProvisioningType().toString()); Long poolId = volumeVo.getPoolId(); if (poolId == null) { @@ -594,7 +600,7 @@ protected Storage getPresetVariableValueStorage(Long storageId, int usageType) { storage = new Storage(); storage.setId(storagePoolVo.getUuid()); storage.setName(storagePoolVo.getName()); - storage.setScope(storagePoolVo.getScope()); + storage.setScope(storagePoolVo.getScope().toString()); List storagePoolTagVOList = storagePoolTagsDao.findStoragePoolTags(storageId); List storageTags = new ArrayList<>(); boolean isTagARule = false; @@ -663,7 +669,7 @@ protected void loadPresetVariableValueForSnapshot(UsageVO usageRecord, Value val value.setId(snapshotVo.getUuid()); value.setName(snapshotVo.getName()); value.setSize(ByteScaleUtils.bytesToMebibytes(snapshotVo.getSize())); - value.setSnapshotType(Snapshot.Type.values()[snapshotVo.getSnapshotType()]); + value.setSnapshotType(Snapshot.Type.values()[snapshotVo.getSnapshotType()].toString()); value.setStorage(getPresetVariableValueStorage(getSnapshotDataStoreId(snapshotId, usageRecord.getZoneId()), usageType)); value.setTags(getPresetVariableValueResourceTags(snapshotId, ResourceObjectType.Snapshot)); Hypervisor.HypervisorType hypervisorType = snapshotVo.getHypervisorType(); @@ -732,7 +738,7 @@ protected void loadPresetVariableValueForVmSnapshot(UsageVO usageRecord, Value v value.setId(vmSnapshotVo.getUuid()); value.setName(vmSnapshotVo.getName()); value.setTags(getPresetVariableValueResourceTags(vmSnapshotId, ResourceObjectType.VMSnapshot)); - value.setVmSnapshotType(vmSnapshotVo.getType()); + value.setVmSnapshotType(vmSnapshotVo.getType().toString()); VMInstanceVO vmVo = vmInstanceDao.findByIdIncludingRemoved(vmSnapshotVo.getVmId()); if (vmVo != null && vmVo.getHypervisorType() != null) { @@ -778,6 +784,30 @@ protected void loadPresetVariableValueForNetwork(UsageVO usageRecord, Value valu value.setId(network.getUuid()); value.setName(network.getName()); value.setState(usageRecord.getState()); + value.setResourceCounting(getPresetVariableValueNetworkResourceCounting(networkId)); + value.setNetworkOffering(getPresetVariableValueNetworkOffering(network.getNetworkOfferingId())); + } + + protected ResourceCounting getPresetVariableValueNetworkResourceCounting(Long networkId) { + ResourceCounting resourceCounting = new ResourceCounting(); + List vmInstancesVO = vmInstanceDao.listNonRemovedVmsByTypeAndNetwork(networkId, VirtualMachine.Type.User); + int runningVms = (int) vmInstancesVO.stream().filter(vm -> vm.getState().equals(VirtualMachine.State.Running)).count(); + int stoppedVms = (int) vmInstancesVO.stream().filter(vm -> vm.getState().equals(VirtualMachine.State.Stopped)).count(); + + resourceCounting.setRunningVms(runningVms); + resourceCounting.setStoppedVms(stoppedVms); + return resourceCounting; + } + + protected GenericPresetVariable getPresetVariableValueNetworkOffering(Long networkOfferingId) { + NetworkOfferingVO networkOfferingVo = networkOfferingDao.findByIdIncludingRemoved(networkOfferingId); + validateIfObjectIsNull(networkOfferingVo, networkOfferingId, "network offering"); + + GenericPresetVariable networkOffering = new GenericPresetVariable(); + networkOffering.setId(networkOfferingVo.getUuid()); + networkOffering.setName(networkOfferingVo.getName()); + + return networkOffering; } protected void loadPresetVariableValueForVpc(UsageVO usageRecord, Value value) { @@ -793,6 +823,18 @@ protected void loadPresetVariableValueForVpc(UsageVO usageRecord, Value value) { value.setId(vpc.getUuid()); value.setName(vpc.getName()); + value.setVpcOffering(getPresetVariableValueVpcOffering(vpc.getVpcOfferingId())); + } + + protected GenericPresetVariable getPresetVariableValueVpcOffering(Long vpcOfferingId) { + VpcOfferingVO vpcOfferingVo = vpcOfferingDao.findByIdIncludingRemoved(vpcOfferingId); + validateIfObjectIsNull(vpcOfferingVo, vpcOfferingId, "vpc offering"); + + GenericPresetVariable vpcOffering = new GenericPresetVariable(); + vpcOffering.setId(vpcOfferingVo.getUuid()); + vpcOffering.setName(vpcOfferingVo.getName()); + + return vpcOffering; } /** diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java similarity index 54% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java rename to framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java index f7978f16e04e..75049c3486a3 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceCounting.java @@ -17,24 +17,36 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; + +import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -@RunWith(MockitoJUnitRunner.class) -public class ComputingResourcesTest { +public class ResourceCounting { + + @PresetVariableDefinition(description = "The number of running user instances.", supportedTypes = {QuotaTypes.NETWORK}) + private int runningVms; + @PresetVariableDefinition(description = "The number of stopped user instances.", supportedTypes = {QuotaTypes.NETWORK}) + private int stoppedVms; - @Test - public void toStringTestReturnAJson() { - ComputingResources variable = new ComputingResources(); + public int getRunningVms() { + return runningVms; + } - String expected = ToStringBuilder.reflectionToString(variable, ToStringStyle.JSON_STYLE); - String result = variable.toString(); + public void setRunningVms(int runningVms) { + this.runningVms = runningVms; + } - Assert.assertEquals(expected, result); + public int getStoppedVms() { + return stoppedVms; } + public void setStoppedVms(int stoppedVms) { + this.stoppedVms = stoppedVms; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java index 3f953b3a4ff8..3c61786cb0a3 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java @@ -17,19 +17,16 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; -import org.apache.cloudstack.acl.RoleType; - public class Role extends GenericPresetVariable { @PresetVariableDefinition(description = "Role type of the resource's owner.") - private RoleType type; + private String type; - public RoleType getType() { + public String getType() { return type; } - public void setType(RoleType type) { + public void setType(String type) { this.type = type; - fieldNamesToIncludeInToString.add("type"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java index 9b6cfb310922..8ddae82f383b 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java @@ -19,8 +19,6 @@ import java.util.List; -import com.cloud.storage.ScopeType; - public class Storage extends GenericPresetVariable { @PresetVariableDefinition(description = "List of string representing the tags of the storage where the volume is (i.e.: [\"a\", \"b\"]).") private List tags; @@ -29,7 +27,7 @@ public class Storage extends GenericPresetVariable { private Boolean isTagARule; @PresetVariableDefinition(description = "Scope of the storage where the volume is. Values can be: ZONE, CLUSTER or HOST. Applicable only for primary storages.") - private ScopeType scope; + private String scope; public List getTags() { return tags; @@ -37,7 +35,6 @@ public List getTags() { public void setTags(List tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public Boolean getIsTagARule() { @@ -46,16 +43,14 @@ public Boolean getIsTagARule() { public void setIsTagARule(Boolean isTagARule) { this.isTagARule = isTagARule; - fieldNamesToIncludeInToString.add("isTagARule"); } - public ScopeType getScope() { + public String getScope() { return scope; } - public void setScope(ScopeType scope) { + public void setScope(String scope) { this.scope = scope; - fieldNamesToIncludeInToString.add("scope"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java index 3703820a1a40..9414908b3a25 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java @@ -28,6 +28,5 @@ public BigDecimal getValue() { public void setValue(BigDecimal value) { this.value = value; - fieldNamesToIncludeInToString.add("value"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java index 77e539db0f3d..286fe5c60fd5 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java @@ -20,10 +20,6 @@ import java.util.List; import java.util.Map; -import com.cloud.storage.Snapshot; -import com.cloud.storage.Storage.ProvisioningType; -import com.cloud.storage.Volume; -import com.cloud.vm.snapshot.VMSnapshot; import org.apache.cloudstack.quota.constant.QuotaTypes; public class Value extends GenericPresetVariable { @@ -61,13 +57,13 @@ public class Value extends GenericPresetVariable { private Long virtualSize; @PresetVariableDefinition(description = "Provisioning type of the resource. Values can be: thin, sparse or fat.", supportedTypes = {QuotaTypes.VOLUME}) - private ProvisioningType provisioningType; + private String provisioningType; @PresetVariableDefinition(description = "Type of the snapshot. Values can be: MANUAL, RECURRING, HOURLY, DAILY, WEEKLY and MONTHLY.", supportedTypes = {QuotaTypes.SNAPSHOT}) - private Snapshot.Type snapshotType; + private String snapshotType; @PresetVariableDefinition(description = "Type of the VM snapshot. Values can be: Disk or DiskAndMemory.", supportedTypes = {QuotaTypes.VM_SNAPSHOT}) - private VMSnapshot.Type vmSnapshotType; + private String vmSnapshotType; @PresetVariableDefinition(description = "Computing offering of the VM.", supportedTypes = {QuotaTypes.RUNNING_VM, QuotaTypes.ALLOCATED_VM}) private ComputeOffering computeOffering; @@ -88,6 +84,9 @@ public class Value extends GenericPresetVariable { @PresetVariableDefinition(description = "Backup offering of the backup.", supportedTypes = {QuotaTypes.BACKUP}) private BackupOffering backupOffering; + @PresetVariableDefinition(description = "The amount of resources of the usage type.") + private ResourceCounting resourceCounting; + @PresetVariableDefinition(description = "The hypervisor where the resource was deployed. Values can be: XenServer, KVM, VMware, Hyperv, BareMetal, Ovm, Ovm3 and LXC.", supportedTypes = {QuotaTypes.RUNNING_VM, QuotaTypes.ALLOCATED_VM, QuotaTypes.VM_SNAPSHOT, QuotaTypes.SNAPSHOT}) private String hypervisorType; @@ -96,17 +95,22 @@ public class Value extends GenericPresetVariable { private String volumeFormat; @PresetVariableDefinition(description = "The volume type. Values can be: UNKNOWN, ROOT, SWAP, DATADISK and ISO.", supportedTypes = {QuotaTypes.VOLUME}) - private Volume.Type volumeType; + private String volumeType; private String state; + @PresetVariableDefinition(description = "Network offering of the network.", supportedTypes = {QuotaTypes.NETWORK}) + private GenericPresetVariable networkOffering; + + @PresetVariableDefinition(description = "VPC offering of the VPC.", supportedTypes = {QuotaTypes.VPC}) + private GenericPresetVariable vpcOffering; + public Host getHost() { return host; } public void setHost(Host host) { this.host = host; - fieldNamesToIncludeInToString.add("host"); } public String getOsName() { @@ -115,7 +119,6 @@ public String getOsName() { public void setOsName(String osName) { this.osName = osName; - fieldNamesToIncludeInToString.add("osName"); } public List getAccountResources() { @@ -124,7 +127,6 @@ public List getAccountResources() { public void setAccountResources(List accountResources) { this.accountResources = accountResources; - fieldNamesToIncludeInToString.add("accountResources"); } public Map getTags() { @@ -133,7 +135,6 @@ public Map getTags() { public void setTags(Map tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public String getTag() { @@ -142,7 +143,6 @@ public String getTag() { public void setTag(String tag) { this.tag = tag; - fieldNamesToIncludeInToString.add("tag"); } public Long getSize() { @@ -151,34 +151,30 @@ public Long getSize() { public void setSize(Long size) { this.size = size; - fieldNamesToIncludeInToString.add("size"); } - public ProvisioningType getProvisioningType() { + public String getProvisioningType() { return provisioningType; } - public void setProvisioningType(ProvisioningType provisioningType) { + public void setProvisioningType(String provisioningType) { this.provisioningType = provisioningType; - fieldNamesToIncludeInToString.add("provisioningType"); } - public Snapshot.Type getSnapshotType() { + public String getSnapshotType() { return snapshotType; } - public void setSnapshotType(Snapshot.Type snapshotType) { + public void setSnapshotType(String snapshotType) { this.snapshotType = snapshotType; - fieldNamesToIncludeInToString.add("snapshotType"); } - public VMSnapshot.Type getVmSnapshotType() { + public String getVmSnapshotType() { return vmSnapshotType; } - public void setVmSnapshotType(VMSnapshot.Type vmSnapshotType) { + public void setVmSnapshotType(String vmSnapshotType) { this.vmSnapshotType = vmSnapshotType; - fieldNamesToIncludeInToString.add("vmSnapshotType"); } public ComputeOffering getComputeOffering() { @@ -187,7 +183,6 @@ public ComputeOffering getComputeOffering() { public void setComputeOffering(ComputeOffering computeOffering) { this.computeOffering = computeOffering; - fieldNamesToIncludeInToString.add("computeOffering"); } public GenericPresetVariable getTemplate() { @@ -196,7 +191,6 @@ public GenericPresetVariable getTemplate() { public void setTemplate(GenericPresetVariable template) { this.template = template; - fieldNamesToIncludeInToString.add("template"); } public DiskOfferingPresetVariables getDiskOffering() { @@ -205,7 +199,6 @@ public DiskOfferingPresetVariables getDiskOffering() { public void setDiskOffering(DiskOfferingPresetVariables diskOffering) { this.diskOffering = diskOffering; - fieldNamesToIncludeInToString.add("diskOffering"); } public Storage getStorage() { @@ -214,7 +207,6 @@ public Storage getStorage() { public void setStorage(Storage storage) { this.storage = storage; - fieldNamesToIncludeInToString.add("storage"); } public ComputingResources getComputingResources() { @@ -223,7 +215,6 @@ public ComputingResources getComputingResources() { public void setComputingResources(ComputingResources computingResources) { this.computingResources = computingResources; - fieldNamesToIncludeInToString.add("computingResources"); } public Long getVirtualSize() { @@ -232,7 +223,6 @@ public Long getVirtualSize() { public void setVirtualSize(Long virtualSize) { this.virtualSize = virtualSize; - fieldNamesToIncludeInToString.add("virtualSize"); } public BackupOffering getBackupOffering() { @@ -241,12 +231,10 @@ public BackupOffering getBackupOffering() { public void setBackupOffering(BackupOffering backupOffering) { this.backupOffering = backupOffering; - fieldNamesToIncludeInToString.add("backupOffering"); } public void setHypervisorType(String hypervisorType) { this.hypervisorType = hypervisorType; - fieldNamesToIncludeInToString.add("hypervisorType"); } public String getHypervisorType() { @@ -255,20 +243,18 @@ public String getHypervisorType() { public void setVolumeFormat(String volumeFormat) { this.volumeFormat = volumeFormat; - fieldNamesToIncludeInToString.add("volumeFormat"); } public String getVolumeFormat() { return volumeFormat; } - public Volume.Type getVolumeType() { + public String getVolumeType() { return volumeType; } - public void setVolumeType(Volume.Type volumeType) { + public void setVolumeType(String volumeType) { this.volumeType = volumeType; - fieldNamesToIncludeInToString.add("volumeType"); } public String getState() { @@ -277,6 +263,29 @@ public String getState() { public void setState(String state) { this.state = state; - fieldNamesToIncludeInToString.add("state"); + } + + public ResourceCounting getResourceCounting() { + return resourceCounting; + } + + public void setResourceCounting(ResourceCounting resourceCounting) { + this.resourceCounting = resourceCounting; + } + + public GenericPresetVariable getNetworkOffering() { + return networkOffering; + } + + public void setNetworkOffering(GenericPresetVariable networkOffering) { + this.networkOffering = networkOffering; + } + + public GenericPresetVariable getVpcOffering() { + return vpcOffering; + } + + public void setVpcOffering(GenericPresetVariable vpcOffering) { + this.vpcOffering = vpcOffering; } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java index 0da0d6e53f77..7b725d57c6ef 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java @@ -20,11 +20,23 @@ import java.util.HashMap; import java.util.Map; -import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.VpnUser; +import com.cloud.network.rules.LoadBalancer; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.storage.object.Bucket; import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.usage.UsageUnitTypes; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.commons.lang3.StringUtils; public class QuotaTypes extends UsageTypes { private final Integer quotaType; @@ -32,44 +44,46 @@ public class QuotaTypes extends UsageTypes { private final String quotaUnit; private final String description; private final String discriminator; + private final Class clazz; private final static Map quotaTypeMap; static { final HashMap quotaTypeList = new HashMap(); - quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage")); - quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage")); - quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage")); - quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)")); - quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)")); - quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage")); - quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage")); - quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage")); - quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage")); - quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage")); - quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage")); - quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage")); - quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage")); - quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage")); - quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)")); - quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)")); - quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)")); - quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)")); - quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage")); - quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage")); - quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage")); - quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage")); - quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage")); - quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage")); - quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage")); + quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage", VirtualMachine.class)); + quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage", VirtualMachine.class)); + quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage", IpAddress.class)); + quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)", Network.class)); + quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)", Network.class)); + quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage", Volume.class)); + quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage", Snapshot.class)); + quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage", SecurityGroup.class)); + quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage", LoadBalancer.class)); + quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage", PortForwardingRule.class)); + quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage", NetworkOffering.class)); + quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage", VpnUser.class)); + quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)", Volume.class)); + quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage", Snapshot.class)); + quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage", Snapshot.class)); + quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage", BackupOffering.class)); + quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage", Bucket.class)); + quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage", Network.class)); + quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage", Vpc.class)); quotaTypeMap = Collections.unmodifiableMap(quotaTypeList); } - private QuotaTypes(Integer quotaType, String name, String unit, String description) { + private QuotaTypes(Integer quotaType, String name, String unit, String description, Class clazz) { this.quotaType = quotaType; this.description = description; this.quotaName = name; this.quotaUnit = unit; this.discriminator = "None"; + this.clazz = clazz; } public static Map listQuotaTypes() { @@ -104,22 +118,20 @@ static public String getDescription(int quotaType) { return null; } + public Class getClazz() { + return clazz; + } + static public QuotaTypes getQuotaType(int quotaType) { return quotaTypeMap.get(quotaType); } - static public QuotaTypes getQuotaTypeByName(String name) { - if (StringUtils.isBlank(name)) { - throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank."); - } - - for (QuotaTypes type : quotaTypeMap.values()) { - if (type.getQuotaName().equals(name)) { - return type; - } + static public Class getClazz(int quotaType) { + QuotaTypes t = quotaTypeMap.get(quotaType); + if (t != null) { + return t.getClazz(); } - - throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name)); + return null; } @Override diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java index c694eaeefbe8..2964746bdf06 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java @@ -28,16 +28,14 @@ public interface QuotaBalanceDao extends GenericDao { QuotaBalanceVO saveQuotaBalance(QuotaBalanceVO qb); - List findCreditBalance(Long accountId, Long domainId, Date startDate, Date endDate); + List findCreditBalances(Long accountId, Long domainId, Date startDate, Date endDate); - QuotaBalanceVO findLastBalanceEntry(Long accountId, Long domainId, Date beforeThis); + QuotaBalanceVO getLastQuotaBalanceEntry(Long accountId, Long domainId, Date beforeThis); QuotaBalanceVO findLaterBalanceEntry(Long accountId, Long domainId, Date afterThis); - List findQuotaBalance(Long accountId, Long domainId, Date startDate, Date endDate); + List listQuotaBalances(Long accountId, Long domainId, Date startDate, Date endDate); - List lastQuotaBalanceVO(Long accountId, Long domainId, Date startDate); - - BigDecimal lastQuotaBalance(Long accountId, Long domainId, Date startDate); + BigDecimal getLastQuotaBalance(Long accountId, Long domainId); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java index 01272d1a6184..21553ebd27b0 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java @@ -18,11 +18,14 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import com.cloud.utils.db.Filter; @@ -32,160 +35,104 @@ import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.db.TransactionLegacy; -import com.cloud.utils.db.TransactionStatus; @Component public class QuotaBalanceDaoImpl extends GenericDaoBase implements QuotaBalanceDao { + private static final Logger logger = LogManager.getLogger(QuotaBalanceDaoImpl.class); @Override - public QuotaBalanceVO findLastBalanceEntry(final Long accountId, final Long domainId, final Date beforeThis) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback() { - @Override - public QuotaBalanceVO doInTransaction(final TransactionStatus status) { - List quotaBalanceEntries = new ArrayList<>(); - Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 1L); - QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0); + public QuotaBalanceVO getLastQuotaBalanceEntry(final Long accountId, final Long domainId, final Date beforeThis) { + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback) status -> { + Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 1L); + QueryBuilder qb = getQuotaBalanceQueryBuilder(accountId, domainId); + qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0); + + if (beforeThis != null) { qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LT, beforeThis); - quotaBalanceEntries = search(qb.create(), filter); - return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null; } + + List quotaBalanceEntries = search(qb.create(), filter); + return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null; }); } @Override public QuotaBalanceVO findLaterBalanceEntry(final Long accountId, final Long domainId, final Date afterThis) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback() { - @Override - public QuotaBalanceVO doInTransaction(final TransactionStatus status) { - List quotaBalanceEntries = new ArrayList<>(); - Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, 1L); - QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0); - qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GT, afterThis); - quotaBalanceEntries = search(qb.create(), filter); - return quotaBalanceEntries.size() > 0 ? quotaBalanceEntries.get(0) : null; - } + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback) status -> { + Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, 1L); + QueryBuilder qb = getQuotaBalanceQueryBuilder(accountId, domainId); + qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0); + qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GT, afterThis); + + List quotaBalanceEntries = search(qb.create(), filter); + return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null; }); } @Override public QuotaBalanceVO saveQuotaBalance(final QuotaBalanceVO qb) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback() { - @Override - public QuotaBalanceVO doInTransaction(final TransactionStatus status) { - return persist(qb); - } - }); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback) status -> persist(qb)); } @Override - public List findCreditBalance(final Long accountId, final Long domainId, final Date lastbalancedate, final Date beforeThis) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback>() { - @Override - public List doInTransaction(final TransactionStatus status) { - if ((lastbalancedate != null) && (beforeThis != null) && lastbalancedate.before(beforeThis)) { - Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); - QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.GT, 0); - qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, lastbalancedate, beforeThis); - return search(qb.create(), filter); - } else { - return new ArrayList(); - } + public List findCreditBalances(final Long accountId, final Long domainId, final Date startDate, final Date endDate) { + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> { + if (ObjectUtils.anyNull(startDate, endDate) || startDate.after(endDate)) { + return new ArrayList<>(); } - }); - } - @Override - public List findQuotaBalance(final Long accountId, final Long domainId, final Date startDate, final Date endDate) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback>() { - @Override - public List doInTransaction(final TransactionStatus status) { - List quotaUsageRecords = null; - QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); - if (accountId != null) { - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - } - if (domainId != null) { - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - } - if ((startDate != null) && (endDate != null) && startDate.before(endDate)) { - qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate); - } else { - return Collections. emptyList(); - } - quotaUsageRecords = listBy(qb.create()); - if (quotaUsageRecords.size() == 0) { - quotaUsageRecords.addAll(lastQuotaBalanceVO(accountId, domainId, startDate)); - } - return quotaUsageRecords; + Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); + QueryBuilder qb = getQuotaBalanceQueryBuilder(accountId, domainId); + qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.GT, 0); + qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate); - } + return search(qb.create(), filter); }); - } @Override - public List lastQuotaBalanceVO(final Long accountId, final Long domainId, final Date pivotDate) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback>() { - @Override - public List doInTransaction(final TransactionStatus status) { - List quotaUsageRecords = null; - List trimmedRecords = new ArrayList(); - Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 100L); - // ASSUMPTION there will be less than 100 continuous credit - // transactions - QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); - if (accountId != null) { - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - } - if (domainId != null) { - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - } - if ((pivotDate != null)) { - qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LTEQ, pivotDate); - } - quotaUsageRecords = search(qb.create(), filter); - - // get records before startDate to find start balance - for (QuotaBalanceVO entry : quotaUsageRecords) { - if (logger.isDebugEnabled()) { - logger.debug("FindQuotaBalance Entry=" + entry); - } - if (entry.getCreditsId() > 0) { - trimmedRecords.add(entry); - } else { - trimmedRecords.add(entry); - break; // add only consecutive credit entries and last balance entry - } - } - return trimmedRecords; + public List listQuotaBalances(final Long accountId, final Long domainId, final Date startDate, final Date endDate) { + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> { + QueryBuilder qb = getQuotaBalanceQueryBuilder(accountId, domainId); + qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0); + if (startDate != null) { + qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GTEQ, startDate); + } + if (endDate != null) { + qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LTEQ, endDate); } + + Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true); + return listBy(qb.create(), filter); }); } @Override - public BigDecimal lastQuotaBalance(final Long accountId, final Long domainId, Date startDate) { - List quotaBalance = lastQuotaBalanceVO(accountId, domainId, startDate); - BigDecimal finalBalance = new BigDecimal(0); - if (quotaBalance.isEmpty()) { - logger.info("There are no balance entries on or before the requested date."); - return finalBalance; + public BigDecimal getLastQuotaBalance(final Long accountId, final Long domainId) { + QuotaBalanceVO quotaBalance = getLastQuotaBalanceEntry(accountId, domainId, null); + BigDecimal finalBalance = BigDecimal.ZERO; + Date startDate = DateUtils.addDays(new Date(), -1); + if (quotaBalance == null) { + logger.info("There are no balance entries for account [{}] and domain [{}]. Considering only new added credits.", accountId, domainId); + } else { + finalBalance = quotaBalance.getCreditBalance(); + startDate = quotaBalance.getUpdatedOn(); } - for (QuotaBalanceVO entry : quotaBalance) { - if (logger.isDebugEnabled()) { - logger.debug("lastQuotaBalance Entry=" + entry); - } - finalBalance = finalBalance.add(entry.getCreditBalance()); + + List credits = findCreditBalances(accountId, domainId, startDate, new Date()); + + for (QuotaBalanceVO credit : credits) { + finalBalance = finalBalance.add(credit.getCreditBalance()); } + return finalBalance; } + private QueryBuilder getQuotaBalanceQueryBuilder(Long accountId, Long domainId) { + QueryBuilder qb = QueryBuilder.create(QuotaBalanceVO.class); + qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); + qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); + return qb; + } + } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java index da36bc0b98c5..faf5ce363393 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java @@ -25,7 +25,7 @@ public interface QuotaCreditsDao extends GenericDao { - List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive); + List findCredits(Long accountId, List domainIds, Date startDate, Date endDate); QuotaCreditsVO saveCredits(QuotaCreditsVO credits); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java index ce51177d0aec..ceb901ca30fd 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java @@ -21,7 +21,6 @@ import javax.inject.Inject; -import com.cloud.domain.dao.DomainDao; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; @@ -39,8 +38,6 @@ @Component public class QuotaCreditsDaoImpl extends GenericDaoBase implements QuotaCreditsDao { - @Inject - DomainDao domainDao; @Inject QuotaBalanceDao quotaBalanceDao; @@ -50,19 +47,18 @@ public QuotaCreditsDaoImpl() { quotaCreditsVoSearch = createSearchBuilder(); quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN); quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ); - quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN); + quotaCreditsVoSearch.and("domainIds", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN); quotaCreditsVoSearch.done(); } @Override - public List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) { + public List findCredits(Long accountId, List domainIds, Date startDate, Date endDate) { SearchCriteria sc = quotaCreditsVoSearch.create(); Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); sc.setParametersIfNotNull("accountId", accountId); - if (domainId != null) { - List domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId); - sc.setParameters("domainId", domainIds.toArray()); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); } if (ObjectUtils.allNotNull(startDate, endDate)) { diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java new file mode 100644 index 000000000000..d8ee26075014 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.quota.dao; + +import java.util.List; + +import org.apache.cloudstack.quota.QuotaAccountStateFilter; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +public interface QuotaSummaryDao extends GenericDao { + + Pair, Integer> listQuotaSummariesForAccountAndOrDomain(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter, Long startIndex, Long pageSize); +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java new file mode 100644 index 000000000000..d90d5a75859e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.quota.dao; + +import java.util.List; + +import org.apache.cloudstack.quota.QuotaAccountStateFilter; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; + +public class QuotaSummaryDaoImpl extends GenericDaoBase implements QuotaSummaryDao { + + @Override + public Pair, Integer> listQuotaSummariesForAccountAndOrDomain(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter, Long startIndex, Long pageSize) { + SearchCriteria searchCriteria = createListQuotaSummariesSearchCriteria(accountId, accountName, domainId, domainPath, accountStateFilter); + Filter filter = new Filter(QuotaSummaryVO.class, "accountName", true, startIndex, pageSize); + + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback, Integer>>) status -> searchAndCount(searchCriteria, filter)); + } + + protected SearchCriteria createListQuotaSummariesSearchCriteria(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter) { + SearchCriteria searchCriteria = createListQuotaSummariesSearchBuilder(accountStateFilter).create(); + + searchCriteria.setParametersIfNotNull("accountId", accountId); + searchCriteria.setParametersIfNotNull("domainId", domainId); + + if (accountName != null) { + searchCriteria.setParameters("accountName", "%" + accountName + "%"); + } + + if (domainPath != null) { + searchCriteria.setParameters("domainPath", domainPath + "%"); + } + + return searchCriteria; + } + + protected SearchBuilder createListQuotaSummariesSearchBuilder(QuotaAccountStateFilter accountStateFilter) { + SearchBuilder searchBuilder = createSearchBuilder(); + + searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); + searchBuilder.and("accountName", searchBuilder.entity().getAccountName(), SearchCriteria.Op.LIKE); + searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainPath", searchBuilder.entity().getDomainPath(), SearchCriteria.Op.LIKE); + + if (QuotaAccountStateFilter.REMOVED.equals(accountStateFilter)) { + searchBuilder.and("accountRemoved", searchBuilder.entity().getAccountRemoved(), SearchCriteria.Op.NNULL); + } else if (QuotaAccountStateFilter.ACTIVE.equals(accountStateFilter)) { + searchBuilder.and("accountRemoved", searchBuilder.entity().getAccountRemoved(), SearchCriteria.Op.NULL); + } + + return searchBuilder; + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java new file mode 100644 index 000000000000..9684ca117b56 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDao.java @@ -0,0 +1,29 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.quota.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import java.util.List; + +public interface QuotaTariffUsageDao extends GenericDao { + + void persistQuotaTariffUsage(QuotaTariffUsageVO quotaTariffUsage); + + List listQuotaTariffUsages(Long quotaUsageId); + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java new file mode 100644 index 000000000000..556f552fed69 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaTariffUsageDaoImpl.java @@ -0,0 +1,56 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.quota.dao; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Component +public class QuotaTariffUsageDaoImpl extends GenericDaoBase implements QuotaTariffUsageDao { + private SearchBuilder searchQuotaTariffUsages; + + @PostConstruct + public void init() { + searchQuotaTariffUsages = createSearchBuilder(); + searchQuotaTariffUsages.and("quotaUsageId", searchQuotaTariffUsages.entity().getQuotaUsageId(), SearchCriteria.Op.EQ); + searchQuotaTariffUsages.done(); + } + + @Override + public void persistQuotaTariffUsage(final QuotaTariffUsageVO quotaTariffUsage) { + logger.trace("Persisting quota tariff usage [{}].", quotaTariffUsage); + Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback) status -> persist(quotaTariffUsage)); + } + + @Override + public List listQuotaTariffUsages(Long quotaUsageId) { + SearchCriteria sc = searchQuotaTariffUsages.create(); + sc.setParameters("quotaUsageId", quotaUsageId); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> listBy(sc)); + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java new file mode 100644 index 000000000000..ead70ae35012 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.quota.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; + +import java.util.Date; +import java.util.List; + +public interface QuotaUsageJoinDao extends GenericDao { + + List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java new file mode 100644 index 000000000000..e69b1a05ba4d --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package org.apache.cloudstack.quota.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Date; +import java.util.List; + +@Component +public class QuotaUsageJoinDaoImpl extends GenericDaoBase implements QuotaUsageJoinDao { + + private SearchBuilder searchQuotaUsages; + + private SearchBuilder searchQuotaUsagesJoinTariffUsages; + + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + + @PostConstruct + public void init() { + searchQuotaUsages = createSearchBuilder(); + prepareQuotaUsageSearchBuilder(searchQuotaUsages); + searchQuotaUsages.done(); + + SearchBuilder searchQuotaTariffUsages = quotaTariffUsageDao.createSearchBuilder(); + searchQuotaTariffUsages.and("tariffId", searchQuotaTariffUsages.entity().getTariffId(), SearchCriteria.Op.EQ); + searchQuotaUsagesJoinTariffUsages = createSearchBuilder(); + prepareQuotaUsageSearchBuilder(searchQuotaUsagesJoinTariffUsages); + searchQuotaUsagesJoinTariffUsages.join("searchQuotaTariffUsages", searchQuotaTariffUsages, searchQuotaUsagesJoinTariffUsages.entity().getId(), + searchQuotaTariffUsages.entity().getQuotaUsageId(), JoinBuilder.JoinType.INNER); + searchQuotaUsagesJoinTariffUsages.done(); + } + + private void prepareQuotaUsageSearchBuilder(SearchBuilder searchBuilder) { + searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainIds", searchBuilder.entity().getDomainId(), SearchCriteria.Op.IN); + searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ); + searchBuilder.and("resourceId", searchBuilder.entity().getResourceId(), SearchCriteria.Op.EQ); + searchBuilder.and("networkId", searchBuilder.entity().getNetworkId(), SearchCriteria.Op.EQ); + searchBuilder.and("offeringId", searchBuilder.entity().getOfferingId(), SearchCriteria.Op.EQ); + searchBuilder.and("startDate", searchBuilder.entity().getStartDate(), SearchCriteria.Op.BETWEEN); + searchBuilder.and("endDate", searchBuilder.entity().getEndDate(), SearchCriteria.Op.BETWEEN); + } + + @Override + public List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { + SearchCriteria sc = tariffId == null ? searchQuotaUsages.create() : searchQuotaUsagesJoinTariffUsages.create(); + + sc.setParametersIfNotNull("accountId", accountId); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } + sc.setParametersIfNotNull("usageType", usageType); + sc.setParametersIfNotNull("resourceId", resourceId); + sc.setParametersIfNotNull("networkId", networkId); + sc.setParametersIfNotNull("offeringId", offeringId); + + if (ObjectUtils.allNotNull(startDate, endDate)) { + sc.setParameters("startDate", startDate, endDate); + sc.setParameters("endDate", startDate, endDate); + } + + sc.setJoinParametersIfNotNull("searchQuotaTariffUsages", "tariffId", tariffId); + + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> listBy(sc)); + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java new file mode 100644 index 000000000000..f9796497d57d --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java @@ -0,0 +1,154 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.quota.vo; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.user.Account; + +@Entity +@Table(name = "quota_summary_view") +public class QuotaSummaryVO { + + @Id + @Column(name = "account_id") + private Long accountId = null; + + @Column(name = "quota_enforce") + private Integer quotaEnforce = 0; + + @Column(name = "quota_balance") + private BigDecimal quotaBalance; + + @Column(name = "quota_balance_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date quotaBalanceDate = null; + + @Column(name = "quota_min_balance") + private BigDecimal quotaMinBalance; + + @Column(name = "quota_alert_type") + private Integer quotaAlertType = null; + + @Column(name = "quota_alert_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date quotaAlertDate = null; + + @Column(name = "last_statement_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date lastStatementDate = null; + + @Column(name = "account_uuid") + private String accountUuid; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "account_state") + @Enumerated(EnumType.STRING) + private Account.State accountState; + + @Column(name = "account_removed") + private Date accountRemoved; + + @Column(name = "domain_id") + private Long domainId; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_path") + private String domainPath; + + @Column(name = "domain_removed") + private Date domainRemoved; + + @Column(name = "project_uuid") + private String projectUuid; + + @Column(name = "project_name") + private String projectName; + + @Column(name = "project_removed") + private Date projectRemoved; + + public Long getAccountId() { + return accountId; + } + + public BigDecimal getQuotaBalance() { + return quotaBalance; + } + + public String getAccountUuid() { + return accountUuid; + } + + public String getAccountName() { + return accountName; + } + + public Date getAccountRemoved() { + return accountRemoved; + } + + public Account.State getAccountState() { + return accountState; + } + + public Long getDomainId() { + return domainId; + } + + public String getDomainUuid() { + return domainUuid; + } + + public String getDomainPath() { + return domainPath; + } + + public Date getDomainRemoved() { + return domainRemoved; + } + + public String getProjectUuid() { + return projectUuid; + } + + public String getProjectName() { + return projectName; + } + + public Date getProjectRemoved() { + return projectRemoved; + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java new file mode 100644 index 000000000000..4fa9e771713e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaTariffUsageVO.java @@ -0,0 +1,86 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.quota.vo; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +@Entity +@Table(name = "quota_tariff_usage") +public class QuotaTariffUsageVO implements InternalIdentity { + @Id + @Column(name = "id") + private Long id; + + @Column(name = "tariff_id") + private Long tariffId; + + @Column(name = "quota_usage_id") + private Long quotaUsageId; + + @Column(name = "quota_used") + private BigDecimal quotaUsed; + + public QuotaTariffUsageVO() { + quotaUsed = new BigDecimal(0); + } + + @Override + public long getId() { + return id; + } + + public Long getTariffId() { + return tariffId; + } + + public Long getQuotaUsageId() { + return quotaUsageId; + } + + public BigDecimal getQuotaUsed() { + return quotaUsed; + } + + public void setId(Long id) { + this.id = id; + } + + public void setTariffId(Long tariffId) { + this.tariffId = tariffId; + } + + public void setQuotaUsageId(Long quotaUsageId) { + this.quotaUsageId = quotaUsageId; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this, ToStringStyle.JSON_STYLE).toString(); + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java new file mode 100644 index 000000000000..df9577e23c3e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageJoinVO.java @@ -0,0 +1,179 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.quota.vo; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.math.BigDecimal; +import java.util.Date; + +@Entity +@Table(name = "quota_usage_view") +public class QuotaUsageJoinVO implements InternalIdentity { + + @Id + @Column(name = "id", updatable = false, nullable = false) + private Long id; + + @Column(name = "zone_id") + private Long zoneId = null; + + @Column(name = "account_id") + private Long accountId = null; + + @Column(name = "domain_id") + private Long domainId = null; + + @Column(name = "usage_item_id") + private Long usageItemId; + + @Column(name = "usage_type") + private int usageType; + + @Column(name = "quota_used") + private BigDecimal quotaUsed; + + @Column(name = "start_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date startDate = null; + + @Column(name = "end_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date endDate = null; + + @Column(name = "resource_id") + private Long resourceId = null; + + @Column(name = "network_id") + private Long networkId = null; + + @Column(name = "offering_id") + private Long offeringId = null; + + @Override + public long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getZoneId() { + return zoneId; + } + + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public Long getUsageItemId() { + return usageItemId; + } + + public void setUsageItemId(Long usageItemId) { + this.usageItemId = usageItemId; + } + + public int getUsageType() { + return usageType; + } + + public void setUsageType(int usageType) { + this.usageType = usageType; + } + + public BigDecimal getQuotaUsed() { + return quotaUsed; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Long getResourceId() { + return resourceId; + } + + public void setResourceId(Long resourceId) { + this.resourceId = resourceId; + } + + public Long getNetworkId() { + return networkId; + } + + public void setNetworkId(Long networkId) { + this.networkId = networkId; + } + + public Long getOfferingId() { + return offeringId; + } + + public void setOfferingId(Long offeringId) { + this.offeringId = offeringId; + } + + public QuotaUsageJoinVO () { + } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "zoneId", "accountId", "domainId", "usageItemId", "usageType", "quotaUsed", "startDate", + "endDate", "resourceId"); + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java new file mode 100644 index 000000000000..924824231005 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaUsageResourceVO.java @@ -0,0 +1,62 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package org.apache.cloudstack.quota.vo; + +import java.util.Date; + +public class QuotaUsageResourceVO { + private String uuid; + private String name; + private Date removed; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + public boolean isRemoved() { + return this.removed != null; + } + + public QuotaUsageResourceVO(String uuid, String name, Date removed) { + this.uuid = uuid; + this.name = name; + this.removed = removed; + } +} diff --git a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml index 453355c8522d..5ca2679c388c 100644 --- a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml +++ b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml @@ -19,13 +19,16 @@ - + + - + + + diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index 3b2ea54e86d1..1e08e7d7fc00 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,9 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -89,6 +92,9 @@ public class QuotaManagerImplTest { @Mock PresetVariables presetVariablesMock; + @Mock + QuotaTariffUsageDao quotaTariffUsageDaoMock; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Test @@ -267,12 +273,12 @@ public void injectPresetVariablesIntoJsInterpreterTestProjectIsNullDoNotInjectPr quotaManagerImplSpy.injectPresetVariablesIntoJsInterpreter(jsInterpreterMock, presetVariablesMock); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock, Mockito.never()).injectVariable(Mockito.eq("project"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.any()); + Mockito.verify(jsInterpreterMock, Mockito.never()).injectVariable(Mockito.eq("project"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.any()); } @Test @@ -288,12 +294,12 @@ public void injectPresetVariablesIntoJsInterpreterTestProjectIsNotNullInjectProj quotaManagerImplSpy.injectPresetVariablesIntoJsInterpreter(jsInterpreterMock, presetVariablesMock); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("project"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("project"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.any()); } @Test @@ -484,47 +490,49 @@ public void getPresetVariablesTestHasTariffsWithActivationRuleReturnPresetVariab } @Test - public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnZero() { + public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnEmptyMap() { List tariffs = createTariffList(); Mockito.doReturn(false).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnZero() { - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); + public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnEmptyMap() { + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnAggregation() { + public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnTariffsWithTheirValues() { List tariffs = createTariffList(); Mockito.doReturn(true, false, true).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getQuotaTariffValueToBeApplied(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.TEN.multiply(new BigDecimal(2)), result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.values().stream().allMatch(value -> value.equals(BigDecimal.TEN))); } @Test public void persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsagesTestReturnOnlyPersistedQuotaUsageVo() { - List> listPair = new ArrayList<>(); + Map>> mapUsageQuotaUsage = new LinkedHashMap<>(); QuotaUsageVO quotaUsageVoMock1 = Mockito.mock(QuotaUsageVO.class); QuotaUsageVO quotaUsageVoMock2 = Mockito.mock(QuotaUsageVO.class); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock1)); - listPair.add(new Pair<>(new UsageVO(), null)); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock2)); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock1, new ArrayList<>())); + mapUsageQuotaUsage.put(new UsageVO(), null); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock2, new ArrayList<>())); Mockito.doReturn(null).when(usageDaoMock).persistUsage(Mockito.any()); Mockito.doReturn(null).when(quotaUsageDaoMock).persistQuotaUsage(Mockito.any()); + Mockito.doNothing().when(quotaManagerImplSpy).persistQuotaTariffUsages(Mockito.any(), Mockito.any()); - List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(listPair); + List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageQuotaUsage); Assert.assertEquals(2, result.size()); Assert.assertEquals(quotaUsageVoMock1, result.get(0)); @@ -551,4 +559,41 @@ private static List createLastAppliedTariffsPresetVariableList(int numbe return lastTariffs; } + @Test + public void createQuotaTariffUsageTestTariffValueIsNotZeroReturnDetailedVo() { + Mockito.doReturn(1).when(usageVoMock).getUsageType(); + Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getUsageValueAccordingToUsageUnitType(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(1L).when(quotaTariffVoMock).getId(); + + QuotaTariffUsageVO result = quotaManagerImplSpy.createQuotaTariffUsage(usageVoMock, quotaTariffVoMock, BigDecimal.TEN); + + Assert.assertEquals(1L, result.getTariffId().longValue()); + Assert.assertEquals(BigDecimal.TEN, result.getQuotaUsed()); + } + + @Test + public void persistQuotaTariffUsagesTestZeroQuotaTariffUsagesZeroPersisted() { + List quotaTariffUsages = new ArrayList<>(); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaTariffUsages, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.never()).persistQuotaTariffUsage(Mockito.any()); + } + + @Test + public void persistQuotaTariffUsagesTestTwoQuotaUsageDetailsTwoPersisted() { + List quotaUsageDetails = new ArrayList<>(); + QuotaTariffUsageVO quotaUsageDetailVoMock1 = Mockito.mock(QuotaTariffUsageVO.class); + QuotaTariffUsageVO quotaUsageDetailVoMock2 = Mockito.mock(QuotaTariffUsageVO.class); + + quotaUsageDetails.add(quotaUsageDetailVoMock1); + quotaUsageDetails.add(quotaUsageDetailVoMock2); + + Mockito.doNothing().when(quotaTariffUsageDaoMock).persistQuotaTariffUsage(Mockito.any()); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaUsageDetails, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.times(2)).persistQuotaTariffUsage(Mockito.any()); + } + } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java deleted file mode 100644 index 4f594ee5d001..000000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class GenericPresetVariableTest { - - @Test - public void setIdTestAddFieldIdToCollection() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test"); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("id")); - } - - @Test - public void setNameTestAddFieldNameToCollection() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setName("test"); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("name")); - } - - @Test - public void toStringTestSetAllFieldsAndReturnAJson() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test id"); - variable.setName("test name"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "id", "name"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - - @Test - public void toStringTestSetSomeFieldsAndReturnAJson() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test id"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "id"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - - variable = new GenericPresetVariable(); - variable.setName("test name"); - - expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name"); - result = variable.toString(); - - Assert.assertEquals(expected, result); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java index c692cb7c1e73..bcdbc3b46cec 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java @@ -31,14 +31,22 @@ import com.cloud.dc.ClusterDetailsVO; import com.cloud.host.HostTagVO; import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.vpc.VpcOfferingVO; +import com.cloud.network.vpc.VpcVO; +import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.storage.StoragePoolTagVO; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.acl.dao.RoleDao; import org.apache.cloudstack.backup.BackupOfferingVO; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.quota.constant.QuotaTypes; +import org.apache.cloudstack.quota.dao.NetworkDao; import org.apache.cloudstack.quota.dao.VmTemplateDao; +import org.apache.cloudstack.quota.dao.VpcDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -188,6 +196,15 @@ public class PresetVariableHelperTest { @Mock BackupOfferingDao backupOfferingDaoMock; + @Mock + NetworkDao networkDaoMock; + + @Mock + VpcDao vpcDaoMock; + + @Mock + VpcOfferingDao vpcOfferingDaoMock; + List runningAndAllocatedVmUsageTypes = Arrays.asList(UsageTypes.RUNNING_VM, UsageTypes.ALLOCATED_VM); List templateAndIsoUsageTypes = Arrays.asList(UsageTypes.TEMPLATE, UsageTypes.ISO); @@ -215,14 +232,18 @@ private Value getValueForTests() { value.setTags(Collections.singletonMap("tag1", "value1")); value.setTemplate(getGenericPresetVariableForTests()); value.setDiskOffering(getDiskOfferingForTests()); - value.setProvisioningType(ProvisioningType.THIN); + value.setProvisioningType(ProvisioningType.THIN.toString()); value.setStorage(getStorageForTests()); value.setSize(ByteScaleUtils.GiB); - value.setSnapshotType(Snapshot.Type.HOURLY); + value.setSnapshotType(Snapshot.Type.HOURLY.toString()); value.setTag("tag_test"); - value.setVmSnapshotType(VMSnapshot.Type.Disk); + value.setVmSnapshotType(VMSnapshot.Type.Disk.toString()); value.setComputingResources(getComputingResourcesForTests()); - value.setVolumeType(Volume.Type.DATADISK); + value.setVolumeType(Volume.Type.DATADISK.toString()); + value.setState(Network.State.Implemented.toString()); + value.setResourceCounting(getResourceCountingForTests()); + value.setNetworkOffering(getNetworkOfferingForTests()); + value.setVpcOffering(getVpcOfferingForTests()); return value; } @@ -259,6 +280,13 @@ private Configuration getConfigurationForTests() { return configuration; } + private ResourceCounting getResourceCountingForTests() { + ResourceCounting resourceCounting = new ResourceCounting(); + resourceCounting.setRunningVms(1); + resourceCounting.setStoppedVms(1); + return resourceCounting; + } + private List getHostTagsForTests() { return Arrays.asList(new HostTagVO(1, "tag1", false), new HostTagVO(1, "tag2", false)); } @@ -272,7 +300,7 @@ private Storage getStorageForTests() { storage.setId("storage_id"); storage.setName("storage_name"); storage.setTags(Arrays.asList("tag1", "tag2")); - storage.setScope(ScopeType.ZONE); + storage.setScope(ScopeType.ZONE.toString()); return storage; } @@ -298,9 +326,9 @@ private Set> getQuotaTypesForTests(Integer... typ private List getVmDetailsForTests() { List details = new LinkedList<>(); - details.add(new VMInstanceDetailVO(1l, "test_with_value", "277", false)); - details.add(new VMInstanceDetailVO(1l, "test_with_invalid_value", "invalid", false)); - details.add(new VMInstanceDetailVO(1l, "test_with_null", null, false)); + details.add(new VMInstanceDetailVO(1L, "test_with_value", "277", false)); + details.add(new VMInstanceDetailVO(1L, "test_with_invalid_value", "invalid", false)); + details.add(new VMInstanceDetailVO(1L, "test_with_null", null, false)); return details; } @@ -309,13 +337,6 @@ private void assertPresetVariableIdAndName(GenericPresetVariable expected, Gener Assert.assertEquals(expected.getName(), result.getName()); } - private void validateFieldNamesToIncludeInToString(List expected, GenericPresetVariable resultObject) { - List result = new ArrayList<>(resultObject.fieldNamesToIncludeInToString); - Collections.sort(expected); - Collections.sort(result); - Assert.assertEquals(expected, result); - } - private BackupOffering getBackupOfferingForTests() { BackupOffering backupOffering = new BackupOffering(); backupOffering.setId("backup_offering_id"); @@ -331,6 +352,20 @@ private DiskOfferingPresetVariables getDiskOfferingForTests() { return diskOffering; } + private GenericPresetVariable getNetworkOfferingForTests() { + GenericPresetVariable networkOffering = new GenericPresetVariable(); + networkOffering.setId("network_offering_id"); + networkOffering.setName("network_offering_name"); + return networkOffering; + } + + private GenericPresetVariable getVpcOfferingForTests() { + GenericPresetVariable vpcOffering = new GenericPresetVariable(); + vpcOffering.setId("vpc_offering_id"); + vpcOffering.setName("vpc_offering_name"); + return vpcOffering; + } + private void mockMethodValidateIfObjectIsNull() { Mockito.doNothing().when(presetVariableHelperSpy).validateIfObjectIsNull(Mockito.any(), Mockito.anyLong(), Mockito.anyString()); } @@ -415,7 +450,6 @@ public void setPresetVariableProjectTestAccountWithoutRoleSetAsProject() { Assert.assertNotNull(result.getProject()); assertPresetVariableIdAndName(account, result.getProject()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result.getProject()); } @Test @@ -430,10 +464,9 @@ public void getPresetVariableAccountTestSetValuesAndReturnObject() { Mockito.doReturn(account.getName()).when(accountVoMock).getName(); Mockito.doReturn(account.getCreated()).when(accountVoMock).getCreated(); - Account result = presetVariableHelperSpy.getPresetVariableAccount(1l); + Account result = presetVariableHelperSpy.getPresetVariableAccount(1L); assertPresetVariableIdAndName(account, result); - validateFieldNamesToIncludeInToString(Arrays.asList("created", "id", "name"), result); } @Test @@ -463,18 +496,16 @@ public void getPresetVariableRoleTestSetValuesAndReturnObject() { Role role = new Role(); role.setId("test_id"); role.setName("test_name"); - role.setType(roleType); + role.setType(roleType.toString()); Mockito.doReturn(role.getId()).when(roleVoMock).getUuid(); Mockito.doReturn(role.getName()).when(roleVoMock).getName(); - Mockito.doReturn(role.getType()).when(roleVoMock).getRoleType(); + Mockito.doReturn(RoleType.fromString(role.getType())).when(roleVoMock).getRoleType(); - Role result = presetVariableHelperSpy.getPresetVariableRole(1l); + Role result = presetVariableHelperSpy.getPresetVariableRole(1L); assertPresetVariableIdAndName(role, result); Assert.assertEquals(role.getType(), result.getType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "type"), result); }); } @@ -489,12 +520,10 @@ public void getPresetVariableDomainTestSetValuesAndReturnObject() { Mockito.doReturn(domain.getName()).when(domainVoMock).getName(); Mockito.doReturn(domain.getPath()).when(domainVoMock).getPath(); - Domain result = presetVariableHelperSpy.getPresetVariableDomain(1l); + Domain result = presetVariableHelperSpy.getPresetVariableDomain(1L); assertPresetVariableIdAndName(domain, result); Assert.assertEquals(domain.getPath(), result.getPath()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "path"), result); } @Test @@ -507,10 +536,9 @@ public void getPresetVariableZoneTestSetValuesAndReturnObject() { Mockito.doReturn(expected.getId()).when(dataCenterVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(dataCenterVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableZone(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableZone(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -531,7 +559,6 @@ public void getPresetVariableValueTestSetFieldsAndReturnObject() { Value result = presetVariableHelperSpy.getPresetVariableValue(usageVoMock); Assert.assertEquals(resources, result.getAccountResources()); - validateFieldNamesToIncludeInToString(Arrays.asList("accountResources"), result); } @Test @@ -541,7 +568,7 @@ public void getPresetVariableAccountResourcesTestSetFieldsAndReturnObject() { Mockito.doReturn(new Date()).when(usageVoMock).getEndDate(); Mockito.doReturn(expected).when(usageDaoMock).listAccountResourcesInThePeriod(Mockito.anyLong(), Mockito.anyInt(), Mockito.any(Date.class), Mockito.any(Date.class)); - List result = presetVariableHelperSpy.getPresetVariableAccountResources(usageVoMock, 1l, 0); + List result = presetVariableHelperSpy.getPresetVariableAccountResources(usageVoMock, 1L, 0); for (int i = 0; i < expected.size(); i++) { Assert.assertEquals(expected.get(i).first(), result.get(i).getZoneId()); @@ -590,8 +617,6 @@ public void loadPresetVariableValueForRunningAndAllocatedVmTestRecordIsRunningOr Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expected.getTemplate(), result.getTemplate()); Assert.assertEquals(hypervisorType.name(), result.getHypervisorType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "osName", "tags", "template", "hypervisorType"), result); }); } @@ -614,7 +639,6 @@ public void setPresetVariableHostInValueIfUsageTypeIsRunningVmTestQuotaTypeDiffe public void setPresetVariableHostInValueIfUsageTypeIsRunningVmTestQuotaTypeIsRunningVmSetHost() { Value result = new Value(); Host expectedHost = getHostForTests(); - List expectedHostTags = getHostTagsForTests(); Mockito.doReturn(expectedHost).when(presetVariableHelperSpy).getPresetVariableValueHost(Mockito.anyLong()); presetVariableHelperSpy.setPresetVariableHostInValueIfUsageTypeIsRunningVm(result, UsageTypes.RUNNING_VM, vmInstanceVoMock); @@ -623,7 +647,6 @@ public void setPresetVariableHostInValueIfUsageTypeIsRunningVmTestQuotaTypeIsRun assertPresetVariableIdAndName(expectedHost, result.getHost()); Assert.assertEquals(expectedHost.getTags(), result.getHost().getTags()); - validateFieldNamesToIncludeInToString(Arrays.asList("host"), result); } @Test @@ -638,11 +661,10 @@ public void getPresetVariableValueHostTestSetFieldsAndReturnObject() { Mockito.doReturn(expected.getName()).when(hostVoMock).getName(); Mockito.doReturn(hostTagVOListMock).when(hostTagsDaoMock).getHostTags(Mockito.anyLong()); - Host result = presetVariableHelperSpy.getPresetVariableValueHost(1l); + Host result = presetVariableHelperSpy.getPresetVariableValueHost(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getTags(), result.getTags()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "tags"), result); } @Test @@ -657,12 +679,11 @@ public void getPresetVariableValueHostTestSetFieldsWithRuleTagAndReturnObject() Mockito.doReturn(expected.getName()).when(hostVoMock).getName(); Mockito.doReturn(hostTagVOListMock).when(hostTagsDaoMock).getHostTags(Mockito.anyLong()); - Host result = presetVariableHelperSpy.getPresetVariableValueHost(1l); + Host result = presetVariableHelperSpy.getPresetVariableValueHost(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(new ArrayList<>(), result.getTags()); Assert.assertTrue(result.getIsTagARule()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "tags"), result); } @Test @@ -674,7 +695,7 @@ public void getPresetVariableValueOsNameTestReturnDisplayName() { String expected = "os_display_name"; Mockito.doReturn(expected).when(guestOsVoMock).getDisplayName(); - String result = presetVariableHelperSpy.getPresetVariableValueOsName(1l); + String result = presetVariableHelperSpy.getPresetVariableValueOsName(1L); Assert.assertEquals(expected, result); } @@ -692,7 +713,6 @@ public void getPresetVariableValueComputeOfferingForTestSetFieldsAndReturnObject assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.isCustomized(), result.isCustomized()); Assert.assertEquals(expected.offerHa(), result.offerHa()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "customized", "offerHa"), result); } @Test @@ -706,10 +726,8 @@ public void getPresetVariableValueComputeOfferingForTestSetFieldsAndReturnObject assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.isCustomized(), result.isCustomized()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "customized"), result); } - @Test public void getPresetVariableValueTemplateTestSetValuesAndReturnObject() { VMTemplateVO vmTemplateVoMock = Mockito.mock(VMTemplateVO.class); @@ -720,10 +738,9 @@ public void getPresetVariableValueTemplateTestSetValuesAndReturnObject() { Mockito.doReturn(expected.getId()).when(vmTemplateVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(vmTemplateVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueTemplate(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueTemplate(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -735,7 +752,7 @@ public void getPresetVariableValueResourceTagsTestAllCases() { Mockito.doReturn(listExpected).when(resourceTagDaoMock).listBy(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Arrays.asList(ResourceObjectType.values()).forEach(type -> { - Map result = presetVariableHelperSpy.getPresetVariableValueResourceTags(1l, type); + Map result = presetVariableHelperSpy.getPresetVariableValueResourceTags(1L, type); for (ResourceTag expected: listExpected) { Assert.assertEquals(expected.getValue(), result.get(expected.getKey())); @@ -760,15 +777,15 @@ public void loadPresetVariableValueForVolumeTestRecordIsVolumeAndHasStorageSetFi VolumeVO volumeVoMock = Mockito.mock(VolumeVO.class); Mockito.doReturn(volumeVoMock).when(volumeDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - Mockito.doReturn(1l).when(volumeVoMock).getPoolId(); + Mockito.doReturn(1L).when(volumeVoMock).getPoolId(); mockMethodValidateIfObjectIsNull(); Mockito.doReturn(expected.getId()).when(volumeVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(volumeVoMock).getName(); Mockito.doReturn(expected.getDiskOffering()).when(presetVariableHelperSpy).getPresetVariableValueDiskOffering(Mockito.anyLong()); - Mockito.doReturn(expected.getProvisioningType()).when(volumeVoMock).getProvisioningType(); - Mockito.doReturn(expected.getVolumeType()).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(ProvisioningType.getProvisioningType(expected.getProvisioningType())).when(volumeVoMock).getProvisioningType(); + Mockito.doReturn(Volume.Type.valueOf(expected.getVolumeType())).when(volumeVoMock).getVolumeType(); Mockito.doReturn(expected.getStorage()).when(presetVariableHelperSpy).getPresetVariableValueStorage(Mockito.anyLong(), Mockito.anyInt()); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(expected.getSize()).when(volumeVoMock).getSize(); @@ -789,8 +806,6 @@ public void loadPresetVariableValueForVolumeTestRecordIsVolumeAndHasStorageSetFi Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(imageFormat.name(), result.getVolumeFormat()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "diskOffering", "provisioningType", "volumeType", "storage", "tags", "size", "volumeFormat"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(ImageFormat.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -811,8 +826,8 @@ public void loadPresetVariableValueForVolumeTestRecordIsVolumeAndDoesNotHaveStor Mockito.doReturn(expected.getId()).when(volumeVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(volumeVoMock).getName(); Mockito.doReturn(expected.getDiskOffering()).when(presetVariableHelperSpy).getPresetVariableValueDiskOffering(Mockito.anyLong()); - Mockito.doReturn(expected.getProvisioningType()).when(volumeVoMock).getProvisioningType(); - Mockito.doReturn(expected.getVolumeType()).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(Volume.Type.valueOf(expected.getVolumeType())).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(ProvisioningType.getProvisioningType(expected.getProvisioningType())).when(volumeVoMock).getProvisioningType(); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(expected.getSize()).when(volumeVoMock).getSize(); Mockito.doReturn(imageFormat).when(volumeVoMock).getFormat(); @@ -832,8 +847,6 @@ public void loadPresetVariableValueForVolumeTestRecordIsVolumeAndDoesNotHaveStor Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(imageFormat.name(), result.getVolumeFormat()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "diskOffering", "provisioningType", "volumeType", "tags", "size", "volumeFormat"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(ImageFormat.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -850,11 +863,9 @@ public void getPresetVariableValueDiskOfferingTestSetValuesAndReturnObject() { Mockito.doReturn(expected.getId()).when(diskOfferingVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(diskOfferingVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueDiskOffering(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueDiskOffering(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("bytesReadBurst", "bytesReadBurstLength", "bytesReadRate", "bytesWriteBurst", "bytesWriteBurstLength", "bytesWriteRate", - "id", "iopsReadBurst", "iopsReadBurstLength", "iopsReadRate", "iopsWriteBurst", "iopsWriteBurstLength", "iopsWriteRate", "name"), result); } @Test @@ -862,7 +873,7 @@ public void getPresetVariableValueStorageTestGetSecondaryStorageForSnapshot() { Storage expected = getStorageForTests(); Mockito.doReturn(expected).when(presetVariableHelperSpy).getSecondaryStorageForSnapshot(Mockito.anyLong(), Mockito.anyInt()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); Assert.assertEquals(expected, result); Mockito.verify(primaryStorageDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); @@ -880,16 +891,14 @@ public void getPresetVariableValueStorageTestGetSecondaryStorageForSnapshotRetur Mockito.doReturn(expected.getId()).when(storagePoolVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(storagePoolVoMock).getName(); - Mockito.doReturn(expected.getScope()).when(storagePoolVoMock).getScope(); + Mockito.doReturn(ScopeType.validateAndGetScopeType(expected.getScope())).when(storagePoolVoMock).getScope(); Mockito.doReturn(storageTagVOListMock).when(storagePoolTagsDaoMock).findStoragePoolTags(Mockito.anyLong()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getScope(), result.getScope()); Assert.assertEquals(expected.getTags(), result.getTags()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "scope", "tags"), result); } @Test @@ -904,24 +913,22 @@ public void getPresetVariableValueStorageTestGetSecondaryStorageForSnapshotRetur Mockito.doReturn(expected.getId()).when(storagePoolVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(storagePoolVoMock).getName(); - Mockito.doReturn(expected.getScope()).when(storagePoolVoMock).getScope(); + Mockito.doReturn(ScopeType.validateAndGetScopeType(expected.getScope())).when(storagePoolVoMock).getScope(); Mockito.doReturn(storageTagVOListMock).when(storagePoolTagsDaoMock).findStoragePoolTags(Mockito.anyLong()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getScope(), result.getScope()); Assert.assertEquals(new ArrayList<>(), result.getTags()); Assert.assertTrue(result.getIsTagARule()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "scope", "tags"), result); } @Test public void getSecondaryStorageForSnapshotTestAllTypesAndDoNotBackupSnapshotReturnNull() { presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = false; getQuotaTypesForTests().forEach(type -> { - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, type.getKey()); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, type.getKey()); Assert.assertNull(result); }); } @@ -930,7 +937,7 @@ public void getSecondaryStorageForSnapshotTestAllTypesAndDoNotBackupSnapshotRetu public void getSecondaryStorageForSnapshotTestAllTypesExceptSnapshotAndBackupSnapshotReturnNull() { presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; getQuotaTypesForTests(UsageTypes.SNAPSHOT).forEach(type -> { - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, type.getKey()); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, type.getKey()); Assert.assertNull(result); }); } @@ -947,10 +954,9 @@ public void getSecondaryStorageForSnapshotTestRecordIsSnapshotAndBackupSnapshotS Mockito.doReturn(expected.getName()).when(imageStoreVoMock).getName(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, UsageTypes.SNAPSHOT); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, UsageTypes.SNAPSHOT); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -991,8 +997,6 @@ public void loadPresetVariableValueForTemplateAndIsoTestRecordIsVolumeSetFields( Assert.assertEquals(expected.getOsName(), result.getOsName()); Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "osName", "tags", "size"), result); }); Mockito.verify(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.eq(ResourceObjectType.Template)); @@ -1025,7 +1029,7 @@ public void loadPresetVariableValueForSnapshotTestRecordIsSnapshotSetFields() { Mockito.doReturn(expected.getName()).when(snapshotVoMock).getName(); Mockito.doReturn(expected.getSize()).when(snapshotVoMock).getSize(); Mockito.doReturn((short) 3).when(snapshotVoMock).getSnapshotType(); - Mockito.doReturn(1l).when(presetVariableHelperSpy).getSnapshotDataStoreId(Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(1L).when(presetVariableHelperSpy).getSnapshotDataStoreId(Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(expected.getStorage()).when(presetVariableHelperSpy).getPresetVariableValueStorage(Mockito.anyLong(), Mockito.anyInt()); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(hypervisorType).when(snapshotVoMock).getHypervisorType(); @@ -1043,8 +1047,6 @@ public void loadPresetVariableValueForSnapshotTestRecordIsSnapshotSetFields() { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(hypervisorType.name(), result.getHypervisorType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "snapshotType", "storage", "tags", "size", "hypervisorType"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(Hypervisor.HypervisorType.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -1056,12 +1058,12 @@ public void loadPresetVariableValueForSnapshotTestRecordIsSnapshotSetFields() { public void getSnapshotDataStoreIdTestDoNotBackupSnapshotToSecondaryRetrievePrimaryStorage() { SnapshotDataStoreVO snapshotDataStoreVoMock = Mockito.mock(SnapshotDataStoreVO.class); - Long expected = 1l; + Long expected = 1L; Mockito.doReturn(snapshotDataStoreVoMock).when(snapshotDataStoreDaoMock).findOneBySnapshotAndDatastoreRole(Mockito.anyLong(), Mockito.any(DataStoreRole.class)); Mockito.doReturn(expected).when(snapshotDataStoreVoMock).getDataStoreId(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = false; - Long result = presetVariableHelperSpy.getSnapshotDataStoreId(1l, 1l); + Long result = presetVariableHelperSpy.getSnapshotDataStoreId(1L, 1L); Assert.assertEquals(expected, result); @@ -1078,7 +1080,7 @@ public void getSnapshotDataStoreIdTestDoNotBackupSnapshotToSecondaryRetrievePrim public void getSnapshotDataStoreIdTestBackupSnapshotToSecondaryRetrieveSecondaryStorage() { SnapshotDataStoreVO snapshotDataStoreVoMock = Mockito.mock(SnapshotDataStoreVO.class); - Long expected = 2l; + Long expected = 2L; ImageStoreVO imageStore = Mockito.mock(ImageStoreVO.class); Mockito.when(imageStoreDaoMock.findById(Mockito.anyLong())).thenReturn(imageStore); Mockito.when(imageStore.getDataCenterId()).thenReturn(1L); @@ -1086,7 +1088,7 @@ public void getSnapshotDataStoreIdTestBackupSnapshotToSecondaryRetrieveSecondary Mockito.doReturn(expected).when(snapshotDataStoreVoMock).getDataStoreId(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; - Long result = presetVariableHelperSpy.getSnapshotDataStoreId(2l, 1L); + Long result = presetVariableHelperSpy.getSnapshotDataStoreId(2L, 1L); Assert.assertEquals(expected, result); @@ -1129,8 +1131,6 @@ public void loadPresetVariableValueForNetworkOfferingTestRecordIsSnapshotSetFiel assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getTag(), result.getTag()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "tag"), result); } @Test @@ -1155,7 +1155,7 @@ public void loadPresetVariableValueForVmSnapshotTestRecordIsVmSnapshotSetFields( Mockito.doReturn(expected.getId()).when(vmSnapshotVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(vmSnapshotVoMock).getName(); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); - Mockito.doReturn(expected.getVmSnapshotType()).when(vmSnapshotVoMock).getType(); + Mockito.doReturn(VMSnapshot.Type.valueOf(expected.getVmSnapshotType())).when(vmSnapshotVoMock).getType(); Mockito.doReturn(UsageTypes.VM_SNAPSHOT).when(usageVoMock).getUsageType(); @@ -1166,8 +1166,6 @@ public void loadPresetVariableValueForVmSnapshotTestRecordIsVmSnapshotSetFields( Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expected.getVmSnapshotType(), result.getVmSnapshotType()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "tags", "vmSnapshotType"), result); - Mockito.verify(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.eq(ResourceObjectType.VMSnapshot)); } @@ -1200,9 +1198,6 @@ public void setPresetVariableValueServiceOfferingAndComputingResourcesTestSetCom if (typeInt == UsageTypes.RUNNING_VM) { Assert.assertEquals(expected.getComputingResources(), result.getComputingResources()); - validateFieldNamesToIncludeInToString(Arrays.asList("computeOffering", "computingResources"), result); - } else { - validateFieldNamesToIncludeInToString(Arrays.asList("computeOffering"), result); } }); } @@ -1228,7 +1223,7 @@ public void getDetailByNameTestValueIsInvalidThrowsNumberFormatException() { @Test public void getDetailByNameTestReturnsValue() { - int expected = Integer.valueOf(getVmDetailsForTests().get(0).getValue()); + int expected = Integer.parseInt(getVmDetailsForTests().get(0).getValue()); int result = presetVariableHelperSpy.getDetailByName(getVmDetailsForTests(), "test_with_value", expected); Assert.assertEquals(expected, result); } @@ -1295,8 +1290,6 @@ public void loadPresetVariableValueForBackupTestRecordIsBackupSetAllFields() { Assert.assertEquals(expected.getVirtualSize(), result.getVirtualSize()); Assert.assertEquals(expected.getBackupOffering(), result.getBackupOffering()); - validateFieldNamesToIncludeInToString(Arrays.asList("size", "virtualSize", "backupOffering"), result); - Mockito.verify(presetVariableHelperSpy).getPresetVariableValueBackupOffering(Mockito.anyLong()); } @@ -1311,11 +1304,10 @@ public void getPresetVariableValueBackupOfferingTestSetValuesAndReturnObject() { Mockito.doReturn(expected.getName()).when(backupOfferingVoMock).getName(); Mockito.doReturn(expected.getExternalId()).when(backupOfferingVoMock).getExternalId(); - BackupOffering result = presetVariableHelperSpy.getPresetVariableValueBackupOffering(1l); + BackupOffering result = presetVariableHelperSpy.getPresetVariableValueBackupOffering(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getExternalId(), result.getExternalId()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "externalId"), result); } @Test @@ -1339,4 +1331,120 @@ public void testGetSnapshotImageStoreRefNotNull() { Mockito.when(imageStoreDaoMock.findById(1L)).thenReturn(store); Assert.assertNotNull(presetVariableHelperSpy.getSnapshotImageStoreRef(1L, 1L)); } + + @Test + public void loadPresetVariableValueForNetworkTestRecordIsNotANetworkDoNothing() { + getQuotaTypesForTests(UsageTypes.NETWORK).forEach(type -> { + Mockito.doReturn(type.getKey()).when(usageVoMock).getUsageType(); + presetVariableHelperSpy.loadPresetVariableValueForNetwork(usageVoMock, null); + }); + + Mockito.verifyNoInteractions(networkDaoMock); + } + + @Test + public void loadPresetVariableValueForNetworkTestRecordIsNetworkSetFields() { + Value expected = getValueForTests(); + + NetworkVO networkVoMock = Mockito.mock(NetworkVO.class); + Mockito.doReturn(networkVoMock).when(networkDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + mockMethodValidateIfObjectIsNull(); + + Mockito.doReturn(expected.getId()).when(networkVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkVoMock).getName(); + Mockito.doReturn(expected.getState()).when(usageVoMock).getState(); + Mockito.doReturn(expected.getResourceCounting()).when(presetVariableHelperSpy).getPresetVariableValueNetworkResourceCounting(Mockito.anyLong()); + Mockito.doReturn(expected.getNetworkOffering()).when(presetVariableHelperSpy).getPresetVariableValueNetworkOffering(Mockito.anyLong()); + Mockito.doReturn(UsageTypes.NETWORK).when(usageVoMock).getUsageType(); + + Value result = new Value(); + presetVariableHelperSpy.loadPresetVariableValueForNetwork(usageVoMock, result); + + assertPresetVariableIdAndName(expected, result); + Assert.assertEquals(expected.getState(), result.getState()); + Assert.assertEquals(expected.getResourceCounting(), result.getResourceCounting()); + } + + @Test + public void getPresetVariableValueNetworkResourceCountingTestSetValueAndReturnObject() { + VMInstanceVO vmInstanceVoMock1 = Mockito.spy(VMInstanceVO.class); + vmInstanceVoMock1.setState(VirtualMachine.State.Stopped); + + VMInstanceVO vmInstanceVoMock2 = Mockito.spy(VMInstanceVO.class); + vmInstanceVoMock2.setState(VirtualMachine.State.Running); + + Mockito.doReturn(List.of(vmInstanceVoMock1, vmInstanceVoMock2)).when(vmInstanceDaoMock).listNonRemovedVmsByTypeAndNetwork(Mockito.anyLong(), Mockito.any()); + + mockMethodValidateIfObjectIsNull(); + + ResourceCounting expected = getResourceCountingForTests(); + + ResourceCounting result = presetVariableHelperSpy.getPresetVariableValueNetworkResourceCounting(1L); + + Assert.assertEquals(expected.getRunningVms(), result.getRunningVms()); + Assert.assertEquals(expected.getStoppedVms(), result.getStoppedVms()); + } + + @Test + public void loadPresetVariableValueForVpcTestRecordIsNotAVpcDoNothing() { + getQuotaTypesForTests(UsageTypes.VPC).forEach(type -> { + Mockito.doReturn(type.getKey()).when(usageVoMock).getUsageType(); + presetVariableHelperSpy.loadPresetVariableValueForVpc(usageVoMock, null); + }); + + Mockito.verifyNoInteractions(vpcDaoMock); + } + + @Test + public void loadPresetVariableValueForVpcTestRecordIsVpcSetFields() { + Value expected = getValueForTests(); + + VpcVO networkVoMock = Mockito.mock(VpcVO.class); + Mockito.doReturn(networkVoMock).when(vpcDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + mockMethodValidateIfObjectIsNull(); + + Mockito.doReturn(expected.getId()).when(networkVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkVoMock).getName(); + Mockito.doReturn(expected.getVpcOffering()).when(presetVariableHelperSpy).getPresetVariableValueVpcOffering(Mockito.anyLong()); + + Mockito.doReturn(UsageTypes.VPC).when(usageVoMock).getUsageType(); + + Value result = new Value(); + presetVariableHelperSpy.loadPresetVariableValueForVpc(usageVoMock, result); + + assertPresetVariableIdAndName(expected, result); + Assert.assertEquals(expected.getVpcOffering(), result.getVpcOffering()); + } + + @Test + public void getPresetVariableValueNetworkOfferingTestSetValuesAndReturnObject() { + NetworkOfferingVO networkOfferingVoMock = Mockito.mock(NetworkOfferingVO.class); + Mockito.doReturn(networkOfferingVoMock).when(networkOfferingDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + mockMethodValidateIfObjectIsNull(); + + GenericPresetVariable expected = getGenericPresetVariableForTests(); + Mockito.doReturn(expected.getId()).when(networkOfferingVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(networkOfferingVoMock).getName(); + + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueNetworkOffering(1L); + + assertPresetVariableIdAndName(expected, result); + } + + @Test + public void getPresetVariableValueVpcOfferingTestSetValuesAndReturnObject() { + VpcOfferingVO vpcOfferingVoMock = Mockito.mock(VpcOfferingVO.class); + Mockito.doReturn(vpcOfferingVoMock).when(vpcOfferingDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + mockMethodValidateIfObjectIsNull(); + + GenericPresetVariable expected = getGenericPresetVariableForTests(); + Mockito.doReturn(expected.getId()).when(vpcOfferingVoMock).getUuid(); + Mockito.doReturn(expected.getName()).when(vpcOfferingVoMock).getName(); + + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueVpcOffering(1L); + + assertPresetVariableIdAndName(expected, result); + } } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java deleted file mode 100644 index bad33da88367..000000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java +++ /dev/null @@ -1,175 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class ValueTest { - - @Test - public void setIdTestAddFieldIdToCollection() { - Value variable = new Value(); - variable.setId(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("id")); - } - - @Test - public void setNameTestAddFieldNameToCollection() { - Value variable = new Value(); - variable.setName(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("name")); - } - - @Test - public void setHostTestAddFieldHostToCollection() { - Value variable = new Value(); - variable.setHost(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("host")); - } - - @Test - public void setOsNameTestAddFieldOsNameToCollection() { - Value variable = new Value(); - variable.setOsName(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("osName")); - } - - @Test - public void setAccountResourcesTestAddFieldAccountResourcesToCollection() { - Value variable = new Value(); - variable.setAccountResources(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("accountResources")); - } - - @Test - public void setTagsTestAddFieldTagsToCollection() { - Value variable = new Value(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); - } - - @Test - public void setTagTestAddFieldTagToCollection() { - Value variable = new Value(); - variable.setTag(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tag")); - } - - @Test - public void setSizeTestAddFieldSizeToCollection() { - Value variable = new Value(); - variable.setSize(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("size")); - } - - @Test - public void setProvisioningTypeTestAddFieldProvisioningTypeToCollection() { - Value variable = new Value(); - variable.setProvisioningType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("provisioningType")); - } - - @Test - public void setSnapshotTypeTestAddFieldSnapshotTypeToCollection() { - Value variable = new Value(); - variable.setSnapshotType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("snapshotType")); - } - - @Test - public void setVmSnapshotTypeTestAddFieldVmSnapshotTypeToCollection() { - Value variable = new Value(); - variable.setVmSnapshotType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("vmSnapshotType")); - } - - @Test - public void setComputeOfferingTestAddFieldComputeOfferingToCollection() { - Value variable = new Value(); - variable.setComputeOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("computeOffering")); - } - - @Test - public void setTemplateTestAddFieldTemplateToCollection() { - Value variable = new Value(); - variable.setTemplate(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("template")); - } - - @Test - public void setDiskOfferingTestAddFieldDiskOfferingToCollection() { - Value variable = new Value(); - variable.setDiskOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("diskOffering")); - } - - @Test - public void setStorageTestAddFieldStorageToCollection() { - Value variable = new Value(); - variable.setStorage(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("storage")); - } - - @Test - public void setComputingResourcesTestAddFieldComputingResourcesToCollection() { - Value variable = new Value(); - variable.setComputingResources(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("computingResources")); - } - - @Test - public void setVirtualSizeTestAddFieldVirtualSizeToCollection() { - Value variable = new Value(); - variable.setVirtualSize(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("virtualSize")); - } - - @Test - public void setBackupOfferingTestAddFieldBackupOfferingToCollection() { - Value variable = new Value(); - variable.setBackupOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("backupOffering")); - } - - @Test - public void setHypervisorTypeTestAddFieldHypervisorTypeToCollection() { - Value variable = new Value(); - variable.setHypervisorType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("hypervisorType")); - } - - @Test - public void setVolumeFormatTestAddFieldVolumeFormatToCollection() { - Value variable = new Value(); - variable.setVolumeFormat(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("volumeFormat")); - } - - @Test - public void setStateTestAddFieldStateToCollection() { - Value variable = new Value(); - variable.setState(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("state")); - } - -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java new file mode 100644 index 000000000000..afdd88f8d1da --- /dev/null +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java @@ -0,0 +1,91 @@ + +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.quota.dao; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class QuotaBalanceDaoImplTest { + QuotaBalanceDaoImpl quotaBalanceDaoImplSpy = Mockito.spy(QuotaBalanceDaoImpl.class); + + @Mock + QuotaBalanceVO quotaBalanceVoMock; + + @Test + public void getLastQuotaBalanceTestLastEntryIsNullAndNoCreditsReturnsZero() { + Mockito.doReturn(null).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(new ArrayList<>()).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any()); + + BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(1L, 2L); + + Assert.assertEquals(BigDecimal.ZERO, result); + } + + @Test + public void getLastQuotaBalanceTestReturnsLastEntryAndNoCredits() { + BigDecimal expected = BigDecimal.valueOf(-1542.46); + Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(expected).when(quotaBalanceVoMock).getCreditBalance(); + Mockito.doReturn(new ArrayList<>()).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any()); + + BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L); + + Assert.assertEquals(expected, result); + } + + @Test + public void getLastQuotaBalanceTestReturnsLastEntryPlusCredits() { + BigDecimal balance = BigDecimal.valueOf(-1542.46); + BigDecimal credit1 = new BigDecimal("150.14"); + BigDecimal credit2 = new BigDecimal("78.96"); + BigDecimal expected = balance.add(credit1).add(credit2); + + Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(balance, credit1, credit2).when(quotaBalanceVoMock).getCreditBalance(); + Mockito.doReturn(Arrays.asList(quotaBalanceVoMock, quotaBalanceVoMock)).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any()); + + BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L); + + Assert.assertEquals(expected, result); + } + + @Test + public void getLastQuotaBalanceTestReturnsLastEntryIsNullPlusCredits() { + BigDecimal credit1 = new BigDecimal("150.14"); + BigDecimal credit2 = new BigDecimal("78.96"); + BigDecimal expected = credit1.add(credit2); + + Mockito.doReturn(null).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(credit1, credit2).when(quotaBalanceVoMock).getCreditBalance(); + Mockito.doReturn(Arrays.asList(quotaBalanceVoMock, quotaBalanceVoMock)).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any()); + + BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L); + + Assert.assertEquals(expected, result); + } +} diff --git a/framework/spring/lifecycle/src/main/java/org/apache/cloudstack/spring/lifecycle/CloudStackExtendedLifeCycle.java b/framework/spring/lifecycle/src/main/java/org/apache/cloudstack/spring/lifecycle/CloudStackExtendedLifeCycle.java index 170e3b40e94c..bd3e424f7673 100644 --- a/framework/spring/lifecycle/src/main/java/org/apache/cloudstack/spring/lifecycle/CloudStackExtendedLifeCycle.java +++ b/framework/spring/lifecycle/src/main/java/org/apache/cloudstack/spring/lifecycle/CloudStackExtendedLifeCycle.java @@ -73,7 +73,8 @@ public void with(ComponentLifecycle lifecycle) { try { lifecycle.start(); } catch (Exception e) { - logger.error("Error on starting bean {} - {}", lifecycle.getName(), e.getMessage(), e); + logger.error("Error on starting bean [{}] due to: {}", lifecycle.getName(), e); + throw new CloudRuntimeException("Failed to start bean [" + lifecycle.getName() + "]"); } if (lifecycle instanceof ManagementBean) { diff --git a/framework/spring/module/src/main/java/org/apache/cloudstack/spring/module/model/impl/DefaultModuleDefinitionSet.java b/framework/spring/module/src/main/java/org/apache/cloudstack/spring/module/model/impl/DefaultModuleDefinitionSet.java index 2a6d0b63e5c2..78693f72140c 100644 --- a/framework/spring/module/src/main/java/org/apache/cloudstack/spring/module/model/impl/DefaultModuleDefinitionSet.java +++ b/framework/spring/module/src/main/java/org/apache/cloudstack/spring/module/model/impl/DefaultModuleDefinitionSet.java @@ -112,10 +112,8 @@ public void with(ModuleDefinition def, Stack parents) { logger.debug(String.format("Could not get module [%s] context bean.", moduleDefinitionName)); } } catch (BeansException e) { - logger.warn(String.format("Failed to start module [%s] due to: [%s].", moduleDefinitionName, e.getMessage())); - if (logger.isDebugEnabled()) { - logger.debug(String.format("module start failure of module [%s] was due to: ", moduleDefinitionName), e); - } + logger.error("Failed to start module [{}] due to: {}", def.getName(), e); + throw new RuntimeException(String.format("Failed to start module [%s]", def.getName())); } } catch (EmptyStackException e) { logger.warn(String.format("Failed to obtain module context due to [%s]. Using root context instead.", e.getMessage())); @@ -147,10 +145,8 @@ public void with(ModuleDefinition def, Stack parents) { logger.debug("Failed to obtain module context: ", e); } } catch (BeansException e) { - logger.warn(String.format("Failed to start module [%s] due to: [%s].", def.getName(), e.getMessage())); - if (logger.isDebugEnabled()) { - logger.debug(String.format("module start failure of module [%s] was due to: ", def.getName()), e); - } + logger.error("Failed to load module [{}] due to: {}", def.getName(), e); + throw new RuntimeException(String.format("Failed to load module [%s]", def.getName())); } } }); diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 705959336f15..3ba2e4d5789e 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -83,6 +83,8 @@ Requires: (iptables-services or iptables) Requires: rng-tools Requires: (qemu-img or qemu-tools) Requires: python3-pip +Requires: python3-six +Requires: python3-protobuf Requires: python3-setuptools Requires: (libgcrypt > 1.8.3 or libgcrypt20) Group: System Environment/Libraries @@ -115,7 +117,7 @@ Requires: ipset Requires: perl Requires: rsync Requires: cifs-utils -Requires: edk2-ovmf +Requires: (edk2-ovmf or qemu-ovmf-x86_64) Requires: swtpm Requires: (python3-libvirt or python3-libvirt-python) Requires: (qemu-img or qemu-tools) @@ -125,6 +127,8 @@ Requires: rng-tools Requires: (libgcrypt > 1.8.3 or libgcrypt20) Requires: (selinux-tools if selinux-tools) Requires: sysstat +Requires: python3-libnbd +Requires: socat Provides: cloud-agent Group: System Environment/Libraries %description agent @@ -334,11 +338,11 @@ cp -r ui/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ rm -f ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json ln -sf /etc/%{name}/ui/config.json ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json -# Package mysql-connector-python -wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/ee/ff/48bde5c0f013094d729fe4b0316ba2a24774b3ff1c52d924a8a4cb04078a/six-1.15.0-py2.py3-none-any.whl -wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/e9/93/4860cebd5ad3ff2664ad3c966490ccb46e3b88458b2095145bca11727ca4/setuptools-47.3.1-py3-none-any.whl -wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/32/27/1141a8232723dcb10a595cc0ce4321dcbbd5215300bf4acfc142343205bf/protobuf-3.19.6-py2.py3-none-any.whl +# Package mysql-connector-python (bundled to avoid dependency on external community repo) +# Version 8.0.31 is the last version supporting Python 3.6 (EL8) wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/08/1f/42d74bae9dd6dcfec67c9ed0f3fa482b1ae5ac5f117ca82ab589ecb3ca19/mysql_connector_python-8.0.31-py2.py3-none-any.whl +# Version 8.3.0 supports Python 3.8 to 3.12 (EL9, EL10) +wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/53/ed/26a4b8cacb8852c6fd97d2d58a7f2591c41989807ea82bd8d9725a4e6937/mysql_connector_python-8.3.0-py2.py3-none-any.whl chmod 440 ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d/%{name}-management chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/mnt @@ -455,8 +459,13 @@ then fi %post management -# Install mysql-connector-python -pip3 install %{_datadir}/%{name}-management/setup/wheel/six-1.15.0-py2.py3-none-any.whl %{_datadir}/%{name}-management/setup/wheel/setuptools-47.3.1-py3-none-any.whl %{_datadir}/%{name}-management/setup/wheel/protobuf-3.19.6-py2.py3-none-any.whl %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.0.31-py2.py3-none-any.whl +# Install mysql-connector-python wheel +# Detect Python version to install compatible wheel +if python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)'; then + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.3.0-py2.py3-none-any.whl +else + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.0.31-py2.py3-none-any.whl +fi /usr/bin/systemctl enable cloudstack-management > /dev/null 2>&1 || true /usr/bin/systemctl enable --now rngd > /dev/null 2>&1 || true diff --git a/packaging/suse15 b/packaging/suse15 deleted file mode 120000 index 4dad90d45e0c..000000000000 --- a/packaging/suse15 +++ /dev/null @@ -1 +0,0 @@ -el8 \ No newline at end of file diff --git a/packaging/suse15/cloud-ipallocator.rc b/packaging/suse15/cloud-ipallocator.rc new file mode 120000 index 000000000000..647598e6dc41 --- /dev/null +++ b/packaging/suse15/cloud-ipallocator.rc @@ -0,0 +1 @@ +../el8/cloud-ipallocator.rc \ No newline at end of file diff --git a/packaging/suse15/cloud.limits b/packaging/suse15/cloud.limits new file mode 120000 index 000000000000..37be77e3acf2 --- /dev/null +++ b/packaging/suse15/cloud.limits @@ -0,0 +1 @@ +../el8/cloud.limits \ No newline at end of file diff --git a/packaging/suse15/cloud.spec b/packaging/suse15/cloud.spec new file mode 100644 index 000000000000..cdfc5a72a34e --- /dev/null +++ b/packaging/suse15/cloud.spec @@ -0,0 +1,750 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +%define __os_install_post %{nil} +%global debug_package %{nil} +%global __requires_exclude libc\\.so\\..*|libc\\.so\\.6\\(GLIBC_.*\\) +%define _binaries_in_noarch_packages_terminate_build 0 + +# DISABLE the post-percentinstall java repacking and line number stripping +# we need to find a way to just disable the java repacking and line number stripping, but not the autodeps + +Name: cloudstack +Summary: CloudStack IaaS Platform +#http://fedoraproject.org/wiki/PackageNamingGuidelines#Pre-Release_packages +%define _maventag %{_fullver} +Release: %{_rel} + +Version: %{_ver} +License: ASL 2.0 +Vendor: Apache CloudStack +Packager: Apache CloudStack +Group: System Environment/Libraries +# FIXME do groups for every single one of the subpackages +Source0: %{name}-%{_maventag}.tgz +BuildRoot: %{_tmppath}/%{name}-%{_maventag}-%{release}-build +BuildArch: noarch + +BuildRequires: (java-11-openjdk-devel or java-17-openjdk-devel or java-21-openjdk-devel) +#BuildRequires: ws-commons-util +BuildRequires: jpackage-utils +BuildRequires: gcc +BuildRequires: glibc-devel +BuildRequires: /usr/bin/mkisofs +BuildRequires: python3-setuptools +BuildRequires: wget +BuildRequires: nodejs + +%description +CloudStack is a highly-scalable elastic, open source, +intelligent IaaS cloud implementation. + +%package management +Summary: CloudStack management server UI +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Requires: python3 +Requires: bash +Requires: gawk +Requires: which +Requires: file +Requires: tar +Requires: bzip2 +Requires: gzip +Requires: unzip +Requires: (/sbin/mount.nfs or /usr/sbin/mount.nfs) +Requires: (openssh-clients or openssh) +Requires: (nfs-utils or nfs-client) +Requires: iproute +Requires: wget +Requires: (mysql or mariadb or mysql8.4) +Requires: sudo +Requires: /sbin/service +Requires: /sbin/chkconfig +Requires: /usr/bin/ssh-keygen +Requires: (genisoimage or mkisofs or xorrisofs) +Requires: ipmitool +Requires: %{name}-common = %{_ver} +Requires: (iptables-services or iptables) +Requires: rng-tools +Requires: (qemu-img or qemu-tools) +Requires: python3-pip +Requires: python3-six +Requires: python3-protobuf +Requires: python3-setuptools +Requires: (libgcrypt > 1.8.3 or libgcrypt20) +Group: System Environment/Libraries +%description management +The CloudStack management server is the central point of coordination, +management, and intelligence in CloudStack. + +%package common +Summary: Apache CloudStack common files and scripts +Requires: python3 +Group: System Environment/Libraries +%description common +The Apache CloudStack files shared between agent and management server +%global __requires_exclude libc\\.so\\..*|libc\\.so\\.6\\(GLIBC_.*\\)|^(libuuid\\.so\\.1|/usr/bin/python)$ + +%package agent +Summary: CloudStack Agent for KVM hypervisors +Requires: (openssh-clients or openssh) +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Requires: %{name}-common = %{_ver} +Requires: libvirt +Requires: libvirt-daemon-driver-storage-rbd +Requires: ebtables +Requires: iptables +Requires: ethtool +Requires: (net-tools or net-tools-deprecated) +Requires: iproute +Requires: ipset +Requires: perl +Requires: rsync +Requires: cifs-utils +Requires: (edk2-ovmf or qemu-ovmf-x86_64) +Requires: swtpm +Requires: (python3-libvirt or python3-libvirt-python) +Requires: (qemu-img or qemu-tools) +Requires: qemu-kvm +Requires: cryptsetup +Requires: rng-tools +Requires: (libgcrypt > 1.8.3 or libgcrypt20) +Requires: (selinux-tools if selinux-tools) +Requires: sysstat +Provides: cloud-agent +Group: System Environment/Libraries +%description agent +The CloudStack agent for KVM hypervisors + +%package baremetal-agent +Summary: CloudStack baremetal agent +Requires: tftp-server +Requires: xinetd +Requires: syslinux +Requires: chkconfig +Requires: dhcp +Requires: httpd +Group: System Environment/Libraries +%description baremetal-agent +The CloudStack baremetal agent + +%package usage +Summary: CloudStack Usage calculation server +Requires: (java-17-openjdk or java-21-openjdk) +Requires: (tzdata-java or timezone-java) +Group: System Environment/Libraries +%description usage +The CloudStack usage calculation service + +%package ui +Summary: CloudStack UI +Group: System Environment/Libraries +%description ui +The CloudStack UI + +%package marvin +Summary: Apache CloudStack Marvin library +Requires: python3-pip +Requires: gcc +Requires: python3-devel +Requires: libffi-devel +Requires: openssl-devel +Group: System Environment/Libraries +%description marvin +Apache CloudStack Marvin library + +%package integration-tests +Summary: Apache CloudStack Marvin integration tests +Requires: %{name}-marvin = %{_ver} +Group: System Environment/Libraries +%description integration-tests +Apache CloudStack Marvin integration tests + +%if "%{_ossnoss}" == "noredist" +%package mysql-ha +Summary: Apache CloudStack Balancing Strategy for MySQL +Group: System Environmnet/Libraries +%description mysql-ha +Apache CloudStack Balancing Strategy for MySQL + +%endif + +%prep +echo Doing CloudStack build + +%setup -q -n %{name}-%{_maventag} + +%build + +cp packaging/suse15/replace.properties build/replace.properties +echo VERSION=%{_maventag} >> build/replace.properties +echo PACKAGE=%{name} >> build/replace.properties +touch build/gitrev.txt +echo $(git rev-parse HEAD) > build/gitrev.txt + +if [ "%{_ossnoss}" == "NOREDIST" -o "%{_ossnoss}" == "noredist" ] ; then + echo "Adding noredist flag to the maven build" + FLAGS="$FLAGS -Dnoredist" +fi + +if [ "%{_sim}" == "SIMULATOR" -o "%{_sim}" == "simulator" ] ; then + echo "Adding simulator flag to the maven build" + FLAGS="$FLAGS -Dsimulator" +fi + +if [ \"%{_temp}\" != "" ]; then + echo "Adding flags to package requested templates" + FLAGS="$FLAGS `rpm --eval %{?_temp}`" +fi + +mvn -Psystemvm,developer $FLAGS clean package +cd ui && npm install && npm run build && cd .. + +%install +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} +# Common directories +mkdir -p ${RPM_BUILD_ROOT}%{_bindir} +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/ipallocator +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/work +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/temp +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/mnt +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_initrddir} +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/default +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/profile.d +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d + +# Common +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/vms +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site +mkdir -p ${RPM_BUILD_ROOT}/usr/bin +cp -r scripts/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +install -D systemvm/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/vms/ +install python/lib/cloud_utils.py ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloud_utils.py +cp -r python/lib/cloudutils ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/ +python3 -m py_compile ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloud_utils.py +python3 -m compileall ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/python-site/cloudutils +cp build/gitrev.txt ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts +cp packaging/suse15/cloudstack-sccs ${RPM_BUILD_ROOT}/usr/bin + +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts/network/cisco +cp -r plugins/network-elements/cisco-vnmc/src/main/scripts/network/cisco/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/scripts/network/cisco + +# Management +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/ +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/systemd/system/%{name}-management.service.d +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/run +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel + +# Setup Jetty +ln -sf /etc/%{name}/management ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/conf +ln -sf /var/log/%{name}/management ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/logs + +install -D client/target/utilities/bin/cloud-migrate-databases ${RPM_BUILD_ROOT}%{_bindir}/%{name}-migrate-databases +install -D client/target/utilities/bin/cloud-set-guest-password ${RPM_BUILD_ROOT}%{_bindir}/%{name}-set-guest-password +install -D client/target/utilities/bin/cloud-set-guest-sshkey ${RPM_BUILD_ROOT}%{_bindir}/%{name}-set-guest-sshkey +install -D client/target/utilities/bin/cloud-setup-databases ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-databases +install -D client/target/utilities/bin/cloud-setup-encryption ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-encryption +install -D client/target/utilities/bin/cloud-setup-management ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-management +install -D client/target/utilities/bin/cloud-setup-baremetal ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-baremetal +install -D client/target/utilities/bin/cloud-sysvmadm ${RPM_BUILD_ROOT}%{_bindir}/%{name}-sysvmadm +install -D client/target/utilities/bin/cloud-update-xenserver-licenses ${RPM_BUILD_ROOT}%{_bindir}/%{name}-update-xenserver-licenses +# Bundle cmk in cloudstack-management +wget https://github.com/apache/cloudstack-cloudmonkey/releases/latest/download/cmk.linux.x86-64 -O ${RPM_BUILD_ROOT}%{_bindir}/cmk +chmod +x ${RPM_BUILD_ROOT}%{_bindir}/cmk + +cp -r client/target/utilities/scripts/db/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup +cp -r plugins/integrations/kubernetes-service/src/main/resources/conf/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf +cp -r client/target/cloud-client-ui-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/ +cp -r client/target/classes/META-INF/webapp ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp +cp ui/dist/config.json ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/ +cp -r ui/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/ +rm -f ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/config.json +ln -sf /etc/%{name}/management/config.json ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapp/config.json +mv ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cloud-client-ui-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib/cloudstack-%{_maventag}.jar +cp client/target/lib/*jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/lib/ + +# Don't package the scripts in the management webapp +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/scripts +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/vms + +for name in db.properties server.properties log4j-cloud.xml environment.properties java.security.ciphers +do + cp client/target/conf/$name ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/$name +done + +ln -sf log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/management/log4j2.xml + +install python/bindir/cloud-external-ipallocator.py ${RPM_BUILD_ROOT}%{_bindir}/%{name}-external-ipallocator.py +install -D client/target/pythonlibs/jasypt-1.9.3.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar +install -D utils/target/cloud-utils-%{_maventag}-bundled.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-common/lib/%{name}-utils.jar + +install -D packaging/suse15/cloud-ipallocator.rc ${RPM_BUILD_ROOT}%{_initrddir}/%{name}-ipallocator +install -D packaging/suse15/cloud.limits ${RPM_BUILD_ROOT}%{_sysconfdir}/security/limits.d/cloud +install -D packaging/suse15/filelimit.conf ${RPM_BUILD_ROOT}%{_sysconfdir}/systemd/system/%{name}-management.service.d +install -D packaging/systemd/cloudstack-management.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-management.service +install -D packaging/systemd/cloudstack-management.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-management +install -D server/target/conf/cloudstack-sudoers ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d/%{name}-management +touch ${RPM_BUILD_ROOT}%{_localstatedir}/run/%{name}-management.pid +#install -D server/target/conf/cloudstack-catalina.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-catalina +install -D server/target/conf/cloudstack-management.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-management + +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/etcd-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/etcd-node.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-control-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-control-node.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-control-node-add.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-control-node-add.yml +install -D plugins/integrations/kubernetes-service/src/main/resources/conf/k8s-node.yml ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/cks/conf/k8s-node.yml + +# SystemVM template +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm +cp -r engine/schema/dist/systemvm-templates/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm +rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm/sha512sum.txt + +# Sample Extensions +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +cp -r extensions/* ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +ln -sf %{_sysconfdir}/%{name}/extensions ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/extensions + +# UI +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/ui +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ +cp -r client/target/classes/META-INF/webapp/WEB-INF ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui +cp ui/dist/config.json ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/ui/ +cp -r ui/dist/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ +rm -f ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json +ln -sf /etc/%{name}/ui/config.json ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/config.json + +# Package mysql-connector-python (bundled to avoid dependency on external community repo) +# Version 8.0.31 is the last version supporting Python 3.6 (EL8) +wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/08/1f/42d74bae9dd6dcfec67c9ed0f3fa482b1ae5ac5f117ca82ab589ecb3ca19/mysql_connector_python-8.0.31-py2.py3-none-any.whl +# Version 8.3.0 supports Python 3.8 to 3.12 (EL9, EL10) +wget -P ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/setup/wheel https://files.pythonhosted.org/packages/53/ed/26a4b8cacb8852c6fd97d2d58a7f2591c41989807ea82bd8d9725a4e6937/mysql_connector_python-8.3.0-py2.py3-none-any.whl + +chmod 440 ${RPM_BUILD_ROOT}%{_sysconfdir}/sudoers.d/%{name}-management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/mnt +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/%{name}/management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/work +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/cache/%{name}/management/temp +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/management +chmod 770 ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent + +# KVM Agent +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/agent +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/plugins +install -D packaging/systemd/cloudstack-agent.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-agent.service +install -D packaging/systemd/cloudstack-rolling-maintenance@.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-rolling-maintenance@.service +install -D packaging/systemd/cloudstack-agent.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-agent +install -D agent/target/transformed/agent.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/agent.properties +install -D agent/target/transformed/uefi.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/uefi.properties +install -D agent/target/transformed/environment.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/environment.properties +install -D agent/target/transformed/log4j-cloud.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/agent/log4j-cloud.xml +install -D agent/target/transformed/cloud-setup-agent ${RPM_BUILD_ROOT}%{_bindir}/%{name}-setup-agent +install -D agent/target/transformed/cloudstack-agent-upgrade ${RPM_BUILD_ROOT}%{_bindir}/%{name}-agent-upgrade +install -D agent/target/transformed/cloud-guest-tool ${RPM_BUILD_ROOT}%{_bindir}/%{name}-guest-tool +install -D agent/target/transformed/libvirtqemuhook ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/libvirtqemuhook +install -D agent/target/transformed/rolling-maintenance ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/rolling-maintenance +install -D agent/target/transformed/cloud-ssh ${RPM_BUILD_ROOT}%{_bindir}/%{name}-ssh +install -D agent/target/transformed/cloudstack-agent-profile.sh ${RPM_BUILD_ROOT}%{_sysconfdir}/profile.d/%{name}-agent-profile.sh +install -D agent/target/transformed/cloudstack-agent.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-agent +install -D plugins/hypervisors/kvm/target/cloud-plugin-hypervisor-kvm-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%name-agent/lib/cloud-plugin-hypervisor-kvm-%{_maventag}.jar +cp plugins/hypervisors/kvm/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/storpool/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib +cp plugins/storage/volume/linstor/target/*.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib + +# Usage server +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib +install -D usage/target/cloud-usage-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/cloud-usage-%{_maventag}.jar +install -D usage/target/transformed/db.properties ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage/db.properties +install -D usage/target/transformed/log4j-cloud_usage.xml ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/usage/log4j-cloud.xml +cp usage/target/dependencies/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib/ +cp client/target/lib/mysql*jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-usage/lib/ +install -D packaging/systemd/cloudstack-usage.service ${RPM_BUILD_ROOT}%{_unitdir}/%{name}-usage.service +install -D packaging/systemd/cloudstack-usage.default ${RPM_BUILD_ROOT}%{_sysconfdir}/default/%{name}-usage +mkdir -p ${RPM_BUILD_ROOT}%{_localstatedir}/log/%{name}/usage/ +install -D usage/target/transformed/cloudstack-usage.logrotate ${RPM_BUILD_ROOT}%{_sysconfdir}/logrotate.d/%{name}-usage + +# Marvin +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-marvin +cp tools/marvin/dist/Marvin-*.tar.gz ${RPM_BUILD_ROOT}%{_datadir}/%{name}-marvin/ + +# integration-tests +mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-integration-tests +cp -r test/integration/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-integration-tests/ + +# MYSQL HA +if [ "x%{_ossnoss}" == "xnoredist" ] ; then + mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-mysql-ha/lib + cp -r plugins/database/mysql-ha/target/cloud-plugin-database-mysqlha-%{_maventag}.jar ${RPM_BUILD_ROOT}%{_datadir}/%{name}-mysql-ha/lib +fi + +#License files from whisker +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-management-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-management-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-common-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-common-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-agent-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-agent-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-usage-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-usage-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-ui-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-ui-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-marvin-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-marvin-%{version}/LICENSE +install -D tools/whisker/NOTICE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-integration-tests-%{version}/NOTICE +install -D tools/whisker/LICENSE ${RPM_BUILD_ROOT}%{_defaultdocdir}/%{name}-integration-tests-%{version}/LICENSE + +%clean +[ ${RPM_BUILD_ROOT} != "/" ] && rm -rf ${RPM_BUILD_ROOT} + +%posttrans common + +unalias cp +python_dir=$(python3 -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))") +if [ ! -z $python_dir ];then + cp -f -r /usr/share/cloudstack-common/python-site/* $python_dir/ +fi + +%preun management +/usr/bin/systemctl stop cloudstack-management || true +/usr/bin/systemctl disable cloudstack-management || true + +%pre management +id cloud > /dev/null 2>&1 || /usr/sbin/useradd -M -U -c "CloudStack unprivileged user" \ + -r -s /bin/sh -d %{_localstatedir}/cloudstack/management cloud || true + +rm -rf %{_localstatedir}/cache/cloudstack + +# in case of upgrade to 4.9+ copy commands.properties if not exists in /etc/cloudstack/management/ +if [ "$1" == "2" ] ; then + if [ -f "%{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/commands.properties" ] && [ ! -f "%{_sysconfdir}/%{name}/management/commands.properties" ] ; then + cp -p %{_datadir}/%{name}-management/webapps/client/WEB-INF/classes/commands.properties %{_sysconfdir}/%{name}/management/commands.properties + fi +fi + +# Remove old tomcat symlinks and env config file +if [ -L "%{_datadir}/%{name}-management/lib" ] +then + rm -f %{_datadir}/%{name}-management/bin + rm -f %{_datadir}/%{name}-management/lib + rm -f %{_datadir}/%{name}-management/temp + rm -f %{_datadir}/%{name}-management/work + rm -f %{_sysconfdir}/default/%{name}-management +fi + +%post management +# Install mysql-connector-python wheel +# Detect Python version to install compatible wheel +if python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)'; then + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.3.0-py2.py3-none-any.whl +else + pip3 install %{_datadir}/%{name}-management/setup/wheel/mysql_connector_python-8.0.31-py2.py3-none-any.whl +fi + +/usr/bin/systemctl enable cloudstack-management > /dev/null 2>&1 || true +/usr/bin/systemctl enable --now rngd > /dev/null 2>&1 || true + +grep -s -q "db.cloud.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.cloud.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "db.usage.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.usage.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "db.simulator.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" || sed -i -e "\$adb.simulator.driver=jdbc:mysql" "%{_sysconfdir}/%{name}/management/db.properties" + +# Update DB properties having master and slave(s), with source and replica(s) respectively (for inclusiveness) +grep -s -q "^db.cloud.slaves=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.slaves=/db.cloud.replicas=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.cloud.secondsBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.secondsBeforeRetryMaster=/db.cloud.secondsBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.cloud.queriesBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.cloud.queriesBeforeRetryMaster=/db.cloud.queriesBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.slaves=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.slaves=/db.usage.replicas=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.secondsBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.secondsBeforeRetryMaster=/db.usage.secondsBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" +grep -s -q "^db.usage.queriesBeforeRetryMaster=" "%{_sysconfdir}/%{name}/management/db.properties" && sed -i "s/^db.usage.queriesBeforeRetryMaster=/db.usage.queriesBeforeRetrySource=/g" "%{_sysconfdir}/%{name}/management/db.properties" + +if [ ! -f %{_datadir}/cloudstack-common/scripts/vm/hypervisor/xenserver/vhd-util ] ; then + echo Please download vhd-util from http://download.cloudstack.org/tools/vhd-util and put it in + echo %{_datadir}/cloudstack-common/scripts/vm/hypervisor/xenserver/ +fi + +if [ -f %{_sysconfdir}/sysconfig/%{name}-management ] ; then + rm -f %{_sysconfdir}/sysconfig/%{name}-management +fi + +chown -R cloud:cloud /var/log/cloudstack/management +chown -R cloud:cloud /usr/share/cloudstack-management/templates +find /usr/share/cloudstack-management/templates -type d -exec chmod 0770 {} \; + +systemctl daemon-reload + +%posttrans management +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text management +fi + +%preun agent +/sbin/service cloudstack-agent stop || true +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del cloudstack-agent > /dev/null 2>&1 || true +fi + +%pre agent + +# save old configs if they exist (for upgrade). Otherwise we may lose them +# when the old packages are erased. There are a lot of properties files here. +if [ -d "%{_sysconfdir}/cloud" ] ; then + mv %{_sysconfdir}/cloud %{_sysconfdir}/cloud.rpmsave +fi + +%posttrans agent + +if [ "$1" == "2" ] ; then + echo "Running %{_bindir}/%{name}-agent-upgrade to update bridge name for upgrade from CloudStack 4.0.x (and before) to CloudStack 4.1 (and later)" + %{_bindir}/%{name}-agent-upgrade +fi +if [ ! -d %{_sysconfdir}/libvirt/hooks ] ; then + mkdir %{_sysconfdir}/libvirt/hooks +fi +cp -a ${RPM_BUILD_ROOT}%{_datadir}/%{name}-agent/lib/libvirtqemuhook %{_sysconfdir}/libvirt/hooks/qemu +mkdir -m 0755 -p /usr/share/cloudstack-agent/tmp +/usr/bin/systemctl restart libvirtd +/usr/bin/systemctl enable cloudstack-agent > /dev/null 2>&1 || true +/usr/bin/systemctl enable cloudstack-rolling-maintenance@p > /dev/null 2>&1 || true +/usr/bin/systemctl enable --now rngd > /dev/null 2>&1 || true + +# if saved agent.properties from upgrade exist, copy them over +if [ -f "%{_sysconfdir}/cloud.rpmsave/agent/agent.properties" ]; then + mv %{_sysconfdir}/%{name}/agent/agent.properties %{_sysconfdir}/%{name}/agent/agent.properties.rpmnew + cp -p %{_sysconfdir}/cloud.rpmsave/agent/agent.properties %{_sysconfdir}/%{name}/agent + # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall + mv %{_sysconfdir}/cloud.rpmsave/agent/agent.properties %{_sysconfdir}/cloud.rpmsave/agent/agent.properties.rpmsave +fi + +# if saved uefi.properties from upgrade exist, copy them over +if [ -f "%{_sysconfdir}/cloud.rpmsave/agent/uefi.properties" ]; then + mv %{_sysconfdir}/%{name}/agent/uefi.properties %{_sysconfdir}/%{name}/agent/uefi.properties.rpmnew + cp -p %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties %{_sysconfdir}/%{name}/agent + # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall + mv %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties %{_sysconfdir}/cloud.rpmsave/agent/uefi.properties.rpmsave +fi + +systemctl daemon-reload + +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text agent +fi + +%pre usage +id cloud > /dev/null 2>&1 || /usr/sbin/useradd -M -U -c "CloudStack unprivileged user" \ + -r -s /bin/sh -d %{_localstatedir}/cloudstack/management cloud|| true + +%preun usage +/sbin/service cloudstack-usage stop || true +if [ "$1" == "0" ] ; then + /sbin/chkconfig --del cloudstack-usage > /dev/null 2>&1 || true +fi + +%post usage +if [ -f "%{_sysconfdir}/%{name}/management/db.properties" ]; then + echo "Replacing usage server's db.properties with a link to the management server's db.properties" + rm -f %{_sysconfdir}/%{name}/usage/db.properties + ln -s %{_sysconfdir}/%{name}/management/db.properties %{_sysconfdir}/%{name}/usage/db.properties + /usr/bin/systemctl enable cloudstack-usage > /dev/null 2>&1 || true +fi + +if [ -f "%{_sysconfdir}/%{name}/management/key" ]; then + echo "Replacing usage server's key with a link to the management server's key" + rm -f %{_sysconfdir}/%{name}/usage/key + ln -s %{_sysconfdir}/%{name}/management/key %{_sysconfdir}/%{name}/usage/key +fi + +if [ ! -f "%{_sysconfdir}/%{name}/usage/key" ]; then + ln -s %{_sysconfdir}/%{name}/management/key %{_sysconfdir}/%{name}/usage/key +fi + +mkdir -p /usr/local/libexec +if [ ! -f "/usr/local/libexec/sanity-check-last-id" ]; then + echo 1 > /usr/local/libexec/sanity-check-last-id +fi +chown cloud:cloud /usr/local/libexec/sanity-check-last-id + +%posttrans usage +# Print help message +if [ -f "/usr/share/cloudstack-common/scripts/installer/cloudstack-help-text" ];then + sed -i "s,^ACS_VERSION=.*,ACS_VERSION=%{_maventag},g" /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text + /usr/share/cloudstack-common/scripts/installer/cloudstack-help-text usage +fi + +%post marvin +pip3 install --upgrade https://files.pythonhosted.org/packages/08/1f/42d74bae9dd6dcfec67c9ed0f3fa482b1ae5ac5f117ca82ab589ecb3ca19/mysql_connector_python-8.0.31-py2.py3-none-any.whl +pip3 install --upgrade /usr/share/cloudstack-marvin/Marvin-*.tar.gz + +#No default permission as the permission setup is complex +%files management +%defattr(-,root,root,-) +%dir %{_datadir}/%{name}-management +%dir %attr(0770,root,cloud) %{_localstatedir}/%{name}/mnt +%dir %attr(0770,cloud,cloud) %{_localstatedir}/%{name}/management +%dir %attr(0770,root,cloud) %{_localstatedir}/cache/%{name}/management +%dir %attr(0770,root,cloud) %{_localstatedir}/log/%{name}/management +%config(noreplace) %{_sysconfdir}/default/%{name}-management +%config(noreplace) %{_sysconfdir}/sudoers.d/%{name}-management +%config(noreplace) %{_sysconfdir}/security/limits.d/cloud +%config(noreplace) %{_sysconfdir}/systemd/system/%{name}-management.service.d +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/db.properties +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/server.properties +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/management/config.json +%config(noreplace) %{_sysconfdir}/%{name}/management/log4j-cloud.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/log4j2.xml +%config(noreplace) %{_sysconfdir}/%{name}/management/environment.properties +%config(noreplace) %{_sysconfdir}/%{name}/management/java.security.ciphers +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-management +%attr(0644,root,root) %{_unitdir}/%{name}-management.service +%attr(0755,cloud,cloud) %{_localstatedir}/run/%{name}-management.pid +%attr(0755,root,root) %{_bindir}/%{name}-setup-management +%attr(0755,root,root) %{_bindir}/%{name}-update-xenserver-licenses +%{_datadir}/%{name}-management/conf +%{_datadir}/%{name}-management/lib/*.jar +%{_datadir}/%{name}-management/logs +%{_datadir}/%{name}-management/templates +%{_datadir}/%{name}-management/extensions +%attr(0755,root,root) %{_bindir}/%{name}-setup-databases +%attr(0755,root,root) %{_bindir}/%{name}-migrate-databases +%attr(0755,root,root) %{_bindir}/%{name}-set-guest-password +%attr(0755,root,root) %{_bindir}/%{name}-set-guest-sshkey +%attr(0755,root,root) %{_bindir}/%{name}-sysvmadm +%attr(0755,root,root) %{_bindir}/%{name}-setup-encryption +%attr(0755,root,root) %{_bindir}/cmk +%{_datadir}/%{name}-management/cks/conf/*.yml +%{_datadir}/%{name}-management/setup/*.sql +%{_datadir}/%{name}-management/setup/*.sh +%{_datadir}/%{name}-management/setup/server-setup.xml +%{_datadir}/%{name}-management/webapp/* +%dir %attr(0770, cloud, cloud) %{_datadir}/%{name}-management/templates +%dir %attr(0770, cloud, cloud) %{_datadir}/%{name}-management/templates/systemvm +%attr(0644, cloud, cloud) %{_datadir}/%{name}-management/templates/systemvm/* +%attr(0755,root,root) %{_bindir}/%{name}-external-ipallocator.py +%attr(0755,root,root) %{_initrddir}/%{name}-ipallocator +%dir %attr(0770,root,root) %{_localstatedir}/log/%{name}/ipallocator +%{_defaultdocdir}/%{name}-management-%{version}/LICENSE +%{_defaultdocdir}/%{name}-management-%{version}/NOTICE +%{_datadir}/%{name}-management/setup/wheel/*.whl +%dir %attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions +%attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions/* + +%files agent +%attr(0755,root,root) %{_bindir}/%{name}-setup-agent +%attr(0755,root,root) %{_bindir}/%{name}-agent-upgrade +%attr(0755,root,root) %{_bindir}/%{name}-guest-tool +%attr(0755,root,root) %{_bindir}/%{name}-ssh +%attr(0644,root,root) %{_unitdir}/%{name}-agent.service +%attr(0644,root,root) %{_unitdir}/%{name}-rolling-maintenance@.service +%config(noreplace) %{_sysconfdir}/default/%{name}-agent +%attr(0644,root,root) %{_sysconfdir}/profile.d/%{name}-agent-profile.sh +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-agent +%attr(0755,root,root) %{_datadir}/%{name}-common/scripts/network/cisco +%config(noreplace) %{_sysconfdir}/%{name}/agent +%dir %{_localstatedir}/log/%{name}/agent +%attr(0644,root,root) %{_datadir}/%{name}-agent/lib/*.jar +%attr(0755,root,root) %{_datadir}/%{name}-agent/lib/libvirtqemuhook +%attr(0755,root,root) %{_datadir}/%{name}-agent/lib/rolling-maintenance +%dir %{_datadir}/%{name}-agent/plugins +%{_defaultdocdir}/%{name}-agent-%{version}/LICENSE +%{_defaultdocdir}/%{name}-agent-%{version}/NOTICE + +%files common +%dir %attr(0755,root,root) %{_datadir}/%{name}-common/python-site/cloudutils +%dir %attr(0755,root,root) %{_datadir}/%{name}-common/vms +%attr(0755,root,root) %{_datadir}/%{name}-common/scripts +%attr(0755,root,root) /usr/bin/cloudstack-sccs +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/agent.zip +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/cloud-scripts.tgz +%attr(0644, root, root) %{_datadir}/%{name}-common/vms/patch-sysvms.sh +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/cloud_utils.py +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/__pycache__/* +%attr(0644,root,root) %{_datadir}/%{name}-common/python-site/cloudutils/* +%attr(0644, root, root) %{_datadir}/%{name}-common/lib/jasypt-1.9.3.jar +%attr(0644, root, root) %{_datadir}/%{name}-common/lib/%{name}-utils.jar +%{_defaultdocdir}/%{name}-common-%{version}/LICENSE +%{_defaultdocdir}/%{name}-common-%{version}/NOTICE + +%files ui +%config(noreplace) %attr(0640,root,cloud) %{_sysconfdir}/%{name}/ui/config.json +%{_datadir}/%{name}-ui/* +%{_defaultdocdir}/%{name}-ui-%{version}/LICENSE +%{_defaultdocdir}/%{name}-ui-%{version}/NOTICE + +%files usage +%attr(0644,root,root) %{_unitdir}/%{name}-usage.service +%config(noreplace) %{_sysconfdir}/default/%{name}-usage +%config(noreplace) %attr(0644,root,root) %{_sysconfdir}/logrotate.d/%{name}-usage +%attr(0644,root,root) %{_datadir}/%{name}-usage/*.jar +%attr(0644,root,root) %{_datadir}/%{name}-usage/lib/*.jar +%dir %attr(0770,root,cloud) %{_localstatedir}/log/%{name}/usage +%attr(0644,root,root) %{_sysconfdir}/%{name}/usage/db.properties +%attr(0644,root,root) %{_sysconfdir}/%{name}/usage/log4j-cloud.xml +%{_defaultdocdir}/%{name}-usage-%{version}/LICENSE +%{_defaultdocdir}/%{name}-usage-%{version}/NOTICE + +%files marvin +%attr(0644,root,root) %{_datadir}/%{name}-marvin/Marvin*.tar.gz +%{_defaultdocdir}/%{name}-marvin-%{version}/LICENSE +%{_defaultdocdir}/%{name}-marvin-%{version}/NOTICE + +%files integration-tests +%attr(0755,root,root) %{_datadir}/%{name}-integration-tests/* +%{_defaultdocdir}/%{name}-integration-tests-%{version}/LICENSE +%{_defaultdocdir}/%{name}-integration-tests-%{version}/NOTICE + +%if "%{_ossnoss}" == "noredist" +%files mysql-ha +%defattr(0644,cloud,cloud,0755) +%attr(0644,root,root) %{_datadir}/%{name}-mysql-ha/lib/* +%endif + +%files baremetal-agent +%attr(0755,root,root) %{_bindir}/cloudstack-setup-baremetal + +%changelog +* Thu Dec 22 2022 Rohit Yadav 4.18.0 +- Add support for EL9 + +* Fri Oct 14 2022 Daan Hoogland 4.18.0 +- initialising sanity check pointer file + +* Tue Jun 29 2021 David Jumani 4.16.0 +- Adding SUSE 15 support + +* Thu Apr 30 2015 Rohit Yadav 4.6.0 +- Remove awsapi package + +* Wed Nov 19 2014 Hugo Trippaers 4.6.0 +- Create a specific spec for CentOS 7 + +* Fri Jul 4 2014 Hugo Trippaers 4.5.0 +- Add a package for the mysql ha module + +* Fri Oct 5 2012 Hugo Trippaers 4.1.0 +- new style spec file diff --git a/packaging/suse15/cloudstack-agent.te b/packaging/suse15/cloudstack-agent.te new file mode 120000 index 000000000000..30e123f6cba5 --- /dev/null +++ b/packaging/suse15/cloudstack-agent.te @@ -0,0 +1 @@ +../el8/cloudstack-agent.te \ No newline at end of file diff --git a/packaging/suse15/cloudstack-sccs b/packaging/suse15/cloudstack-sccs new file mode 120000 index 000000000000..b9e6ed9dc085 --- /dev/null +++ b/packaging/suse15/cloudstack-sccs @@ -0,0 +1 @@ +../el8/cloudstack-sccs \ No newline at end of file diff --git a/packaging/suse15/filelimit.conf b/packaging/suse15/filelimit.conf new file mode 120000 index 000000000000..c71688ea640a --- /dev/null +++ b/packaging/suse15/filelimit.conf @@ -0,0 +1 @@ +../el8/filelimit.conf \ No newline at end of file diff --git a/packaging/suse15/replace.properties b/packaging/suse15/replace.properties new file mode 100644 index 000000000000..b1900af83406 --- /dev/null +++ b/packaging/suse15/replace.properties @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +DBUSER=cloud +DBPW=cloud +DBROOTPW= +MSLOG=vmops.log +APISERVERLOG=api.log +DBHOST=localhost +DBDRIVER=jdbc:mysql +COMPONENTS-SPEC=components-premium.xml +REMOTEHOST=localhost +AGENTCLASSPATH= +AGENTLOG=/var/log/cloudstack/agent/agent.log +AGENTLOGDIR=/var/log/cloudstack/agent/ +AGENTSYSCONFDIR=/etc/cloudstack/agent +APISERVERLOG=/var/log/cloudstack/management/apilog.log +BINDIR=/usr/bin +COMMONLIBDIR=/usr/share/cloudstack-common +CONFIGUREVARS= +DEPSCLASSPATH= +DOCDIR= +IPALOCATORLOG=/var/log/cloudstack/management/ipallocator.log +JAVADIR=/usr/share/java +LIBEXECDIR=/usr/libexec +LOCKDIR=/var/lock +MSCLASSPATH= +MSCONF=/etc/cloudstack/management +MSENVIRON=/usr/share/cloudstack-management +MSLOG=/var/log/cloudstack/management/management-server.log +MSLOGDIR=/var/log/cloudstack/management/ +MSMNTDIR=/var/cloudstack/mnt +MSUSER=cloud +PIDDIR=/var/run +PLUGINJAVADIR=/usr/share/cloudstack-management/plugin +PREMIUMJAVADIR=/usr/share/cloudstack-management/premium +PYTHONDIR=/usr/share/cloudstack-common/python-site/ +SERVERSYSCONFDIR=/etc/sysconfig +SETUPDATADIR=/usr/share/cloudstack-management/setup +SYSCONFDIR=/etc/sysconfig +SYSTEMCLASSPATH= +SYSTEMJARS= +USAGECLASSPATH= +USAGELOG=/var/log/cloudstack/usage/usage.log +USAGESYSCONFDIR=/etc/sysconfig +EXTENSIONSDEPLOYMENTMODE=production +GUESTNVRAMTEMPLATELEGACY=/usr/share/qemu/ovmf-x86_64-vars.bin +GUESTLOADERLEGACY=/usr/share/qemu/ovmf-x86_64-code.bin +GUESTNVRAMTEMPLATESECURE=/usr/share/qemu/ovmf-x86_64-ms-vars.bin +GUESTLOADERSECURE=/usr/share/qemu/ovmf-x86_64-ms-code.bin +GUESTNVRAMPATH=/var/lib/libvirt/qemu/nvram/ diff --git a/packaging/systemd/cloudstack-management.default b/packaging/systemd/cloudstack-management.default index 994a1ee86997..dbb7fa7d4bc5 100644 --- a/packaging/systemd/cloudstack-management.default +++ b/packaging/systemd/cloudstack-management.default @@ -15,9 +15,9 @@ # specific language governing permissions and limitations # under the License. -JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED" +JAVA_OPTS="-Djava.security.properties=/etc/cloudstack/management/java.security.ciphers -Djava.awt.headless=true -Xmx2G -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:ErrorFile=/var/log/cloudstack/management/cloudstack-management.err --add-opens=java.base/java.lang=ALL-UNNAMED --add-exports=java.base/sun.security.x509=ALL-UNNAMED -Djava.io.tmpdir=/var/tmp" -CLASSPATH="/usr/share/cloudstack-management/lib/*:/etc/cloudstack/management:/usr/share/cloudstack-common:/usr/share/cloudstack-management/setup:/usr/share/cloudstack-management:/usr/share/java/mysql-connector-java.jar:/usr/share/cloudstack-mysql-ha/lib/*" +CLASSPATH="/usr/share/cloudstack-management/lib/*:/etc/cloudstack/management:/usr/share/cloudstack-common:/usr/share/cloudstack-management/setup:/usr/share/cloudstack-management:/usr/share/cloudstack-mysql-ha/lib/*" BOOTSTRAP_CLASS=org.apache.cloudstack.ServerDaemon diff --git a/packaging/systemd/cloudstack-usage.default b/packaging/systemd/cloudstack-usage.default index 493f40c277a2..36b71ac3e0d6 100644 --- a/packaging/systemd/cloudstack-usage.default +++ b/packaging/systemd/cloudstack-usage.default @@ -17,7 +17,7 @@ JAVA_OPTS="-Xms256m -Xmx2048m --add-opens=java.base/java.lang=ALL-UNNAMED" -CLASSPATH="/usr/share/cloudstack-usage/*:/usr/share/cloudstack-usage/lib/*:/usr/share/cloudstack-mysql-ha/lib/*:/etc/cloudstack/usage:/usr/share/java/mysql-connector-java.jar" +CLASSPATH="/usr/share/cloudstack-usage/*:/usr/share/cloudstack-usage/lib/*:/usr/share/cloudstack-mysql-ha/lib/*:/etc/cloudstack/usage" JAVA_CLASS=com.cloud.usage.UsageServer diff --git a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java index 030e0bcf0141..f3e2335519a4 100644 --- a/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java +++ b/plugins/acl/dynamic-role-based/src/main/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessChecker.java @@ -17,15 +17,18 @@ package org.apache.cloudstack.acl; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.acl.RolePermissionEntity.Permission; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.utils.cache.LazyCache; @@ -47,7 +50,7 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API private RoleService roleService; private List services; - private Map> annotationRoleBasedApisMap = new HashMap>(); + private Map> annotationRoleBasedApisMap = new HashMap<>(); private LazyCache accountCache; private LazyCache>> rolePermissionsCache; @@ -56,7 +59,7 @@ public class DynamicRoleBasedAPIAccessChecker extends AdapterBase implements API protected DynamicRoleBasedAPIAccessChecker() { super(); for (RoleType roleType : RoleType.values()) { - annotationRoleBasedApisMap.put(roleType, new HashSet()); + annotationRoleBasedApisMap.put(roleType, new HashSet<>()); } } @@ -67,9 +70,12 @@ public List getApisAllowedToUser(Role role, User user, List apiN } List allPermissions = roleService.findAllPermissionsBy(role.getId()); + List allPermissionEntities = allPermissions.stream().map(permission -> (RolePermissionEntity) permission) + .collect(Collectors.toList()); + List allowedApis = new ArrayList<>(); for (String api : apiNames) { - if (checkApiPermissionByRole(role, api, allPermissions)) { + if (checkApiPermissionByRole(role, api, allPermissionEntities, false)) { allowedApis.add(api); } } @@ -84,8 +90,8 @@ public List getApisAllowedToUser(Role role, User user, List apiN * @param allPermissions list of role permissions for the given role * @return if the role has the permission for the API */ - public boolean checkApiPermissionByRole(Role role, String apiName, List allPermissions) { - for (final RolePermission permission : allPermissions) { + public boolean checkApiPermissionByRole(Role role, String apiName, List allPermissions, boolean keyPairOverride) { + for (RolePermissionEntity permission : allPermissions) { if (!permission.getRule().matches(apiName)) { continue; } @@ -94,13 +100,13 @@ public boolean checkApiPermissionByRole(Role role, String apiName, List> roleAndPermissions = getRolePermissionsUsingCache(account.getRoleId()); - final Role accountRole = roleAndPermissions.first(); - if (accountRole == null) { - throw new PermissionDeniedException(String.format("Account role for user id [%s] cannot be found.", user.getUuid())); - } - if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) { - logger.info("Account for user id {} is Root Admin or Domain Admin, all APIs are allowed.", user.getUuid()); - return true; + throw new PermissionDeniedException(String.format("Account for user with ID [%s] cannot be found", user.getUuid())); } - List allPermissions = roleAndPermissions.second(); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { - return true; - } - throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account for user id [%s].", commandName, user.getUuid())); + + return checkAccess(account, commandName, apiKeyPairPermissions); } - public boolean checkAccess(Account account, String commandName) { + @Override + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission ... apiKeyPairPermissions) { Pair> roleAndPermissions = getRolePermissionsUsingCache(account.getRoleId()); final Role accountRole = roleAndPermissions.first(); if (accountRole == null) { throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", account)); } - if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId()) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Account [%s] is Root Admin or Domain Admin, all APIs are allowed.", account)); - } + if (accountRole.getRoleType() == RoleType.Admin && accountRole.getId() == RoleType.Admin.getId() && apiKeyPairPermissions.length == 0) { + logger.info("Account [{}] is Root Admin and there aren't any API key pair permissions involved, thus, all APIs are allowed.", account); return true; } - List allPermissions = roleService.findAllPermissionsBy(accountRole.getId()); - if (checkApiPermissionByRole(accountRole, commandName, allPermissions)) { + boolean considerKeyPairPermissions = apiKeyPairPermissions.length > 0; + List allRules = considerKeyPairPermissions ? Arrays.asList(apiKeyPairPermissions) : new ArrayList<>(roleAndPermissions.second()); + if (checkApiPermissionByRole(accountRole, commandName, allRules, considerKeyPairPermissions)) { return true; } - throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account)); + + throw new UnavailableCommandException(String.format("The API [%s] does not exist or is not available for the account %s.", commandName, account.getAccountName())); + } + + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return annotationRoleBasedApisMap.get(roleType) + .stream() + .map(implicitApi -> new RolePermissionBaseVO(implicitApi, Permission.ALLOW)) + .collect(Collectors.toList()); } /** diff --git a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java index e58be3a75e79..71fbbb4a3650 100644 --- a/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java +++ b/plugins/acl/dynamic-role-based/src/test/java/org/apache/cloudstack/acl/DynamicRoleBasedAPIAccessCheckerTest.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; +import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -195,4 +197,68 @@ public void getApisAllowedToUserTestPermissionDenyForGivenApiShouldReturnEmptyLi List apisReceived = apiAccessCheckerSpy.getApisAllowedToUser(getTestRole(), getTestUser(), apiNames); Assert.assertEquals(0, apisReceived.size()); } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidApiKeyPairPermission() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null); + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestUnrelatedApiKeyPairPermission() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, "apiName", Permission.ALLOW, null); + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test + public void checkAccessTestValidApiKeyPairPermission() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission permission = new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null); + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permission)); + } + + @Test + public void checkAccessTestValidMultipleApiKeyPermissions() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{ + new ApiKeyPairPermissionVO(1L, "someDeniedApi", Permission.DENY, null), + new ApiKeyPairPermissionVO(1L, api, Permission.ALLOW, null) + }; + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions)); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidMultipleApiKeyPermissions() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission[] permissions = new ApiKeyPairPermission[]{ + new ApiKeyPairPermissionVO(1L, "someAllowedApi", Permission.ALLOW, null), + new ApiKeyPairPermissionVO(1L, api, Permission.DENY, null) + }; + assertFalse(apiAccessCheckerSpy.checkAccess(getTestUser(), api, permissions)); + } + + + @Test + public void checkAccessTestValidApiKeyPairPermissionWithNullOverride() { + final String api = "someAllowedApi"; + final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]); + final RolePermission permission = new RolePermissionVO(1L, api, Permission.ALLOW, null); + Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); + + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray)); + Mockito.verify(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); + } + + @Test(expected = UnavailableCommandException.class) + public void checkAccessTestInvalidApiKeyPairPermissionWithNullOverride() { + final String api = "someDeniedApi"; + final ApiKeyPairPermission[] emptyPermissionArray = List.of().toArray(new ApiKeyPairPermission[0]); + final RolePermission permission = new RolePermissionVO(1L, api, Permission.DENY, null); + Mockito.doReturn(Collections.singletonList(permission)).when(roleServiceMock).findAllPermissionsBy(Mockito.anyLong()); + + assertTrue(apiAccessCheckerSpy.checkAccess(getTestUser(), api, emptyPermissionArray)); + Mockito.verify(roleServiceMock, Mockito.times(1)).findAllPermissionsBy(Mockito.anyLong()); + } } diff --git a/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java b/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java index 2e7ae23d6f1b..8513f458660c 100644 --- a/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java +++ b/plugins/acl/project-role-based/src/main/java/org/apache/cloudstack/acl/ProjectRoleBasedApiAccessChecker.java @@ -23,6 +23,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.RolePermissionEntity.Permission; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.context.CallContext; import com.cloud.exception.PermissionDeniedException; @@ -105,7 +106,7 @@ public List getApisAllowedToUser(Role role, User user, List apiN } @Override - public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } @@ -150,7 +151,7 @@ public boolean checkAccess(User user, String apiCommandName) throws PermissionDe } @Override - public boolean checkAccess(Account account, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(Account account, String apiCommandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { return true; } @@ -182,6 +183,11 @@ public boolean configure(String name, Map params) throws Configu return true; } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return List.of(); + } + @Override public boolean start() { return super.start(); diff --git a/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java b/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java index 3444f967d784..6cf4da88f5c8 100644 --- a/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java +++ b/plugins/acl/static-role-based/src/main/java/org/apache/cloudstack/acl/StaticRoleBasedAPIAccessChecker.java @@ -21,11 +21,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; import com.cloud.exception.UnavailableCommandException; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.api.APICommand; @@ -90,7 +92,7 @@ public List getApisAllowedToUser(Role role, User user, List apiN } @Override - public boolean checkAccess(User user, String commandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String commandName, ApiKeyPairPermission... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } @@ -104,7 +106,7 @@ public boolean checkAccess(User user, String commandName) throws PermissionDenie } @Override - public boolean checkAccess(Account account, String commandName) { + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission... apiKeyPairPermissions) { if (!isEnabled()) { return true; } @@ -163,6 +165,14 @@ public boolean start() { return super.start(); } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return annotationRoleBasedApisMap.get(roleType) + .stream() + .map(implicitApi -> new RolePermissionBaseVO(implicitApi, RolePermissionEntity.Permission.ALLOW)) + .collect(Collectors.toList()); + } + private void processMapping(Map configMap) { for (Map.Entry entry : configMap.entrySet()) { String apiName = entry.getKey(); diff --git a/plugins/api/discovery/pom.xml b/plugins/api/discovery/pom.xml index b0f29612911c..1992da81e5fb 100644 --- a/plugins/api/discovery/pom.xml +++ b/plugins/api/discovery/pom.xml @@ -39,6 +39,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-api-limit-account-based + ${project.version} + compile + diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java index 9c01435fbf4f..8cd31939da01 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/api/command/user/discovery/ListApisCmd.java @@ -52,7 +52,7 @@ public class ListApisCmd extends BaseCmd { public void execute() throws ServerApiException { if (_apiDiscoveryService != null) { User user = CallContext.current().getCallingUser(); - ListResponse response = (ListResponse)_apiDiscoveryService.listApis(user, name); + ListResponse response = (ListResponse)_apiDiscoveryService.listApis(user, name, this); if (response == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Api Discovery plugin was unable to find an api by that name or process any apis"); } diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java index 5a6eab7389d1..073010a8eb60 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryService.java @@ -18,6 +18,7 @@ import com.cloud.user.Account; import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.command.user.discovery.ListApisCmd; import org.apache.cloudstack.api.response.ListResponse; import com.cloud.user.User; @@ -28,5 +29,5 @@ public interface ApiDiscoveryService extends PluggableService { List listApiNames(Account account); - ListResponse listApis(User user, String apiName); + ListResponse listApis(User user, String apiName, ListApisCmd cmd); } diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java index d6d235162efb..d412f12fce24 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -33,6 +34,8 @@ import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairService; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; @@ -44,8 +47,11 @@ import org.apache.cloudstack.api.response.ApiParameterResponse; import org.apache.cloudstack.api.response.ApiResponseResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.ratelimit.ApiRateLimitService; +import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.reflections.ReflectionUtils; import org.springframework.stereotype.Component; @@ -56,6 +62,7 @@ import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.User; +import com.cloud.user.UserAccount; import com.cloud.utils.ReflectUtil; import com.cloud.utils.component.ComponentLifecycleBase; import com.cloud.utils.component.PluggableService; @@ -67,6 +74,7 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A List _apiAccessCheckers = null; List _services = null; protected static Map s_apiNameDiscoveryResponseMap = null; + public static final List APIS_ALLOWED_FOR_PASSWORD_CHANGE = Arrays.asList("login", "logout", "updateUser", "listApis"); @Inject AccountService accountService; @@ -74,6 +82,9 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A @Inject RoleService roleService; + @Inject + ApiKeyPairService apiKeyPairService; + protected ApiDiscoveryServiceImpl() { super(); } @@ -253,16 +264,24 @@ public List listApiNames(Account account) { } @Override - public ListResponse listApis(User user, String name) { + public ListResponse listApis(User user, String name, ListApisCmd cmd) { ListResponse response = new ListResponse<>(); List responseList = new ArrayList<>(); List apisAllowed = new ArrayList<>(s_apiNameDiscoveryResponseMap.keySet()); + String apikey = accountService.getAccessingApiKey(cmd); if (user == null) return null; + Account account = accountService.getAccount(user.getAccountId()); + if (account == null) { + throw new PermissionDeniedException(String.format("The account with id [%s] for user [%s] is null.", user.getAccountId(), user)); + } - if (name != null) { + Role role = roleService.findRole(account.getRoleId()); + if (apikey != null) { + responseList = listApisForKeyPair(apikey, name, user, role, apisAllowed); + } else if (name != null) { if (!s_apiNameDiscoveryResponseMap.containsKey(name)) return null; @@ -277,22 +296,25 @@ public ListResponse listApis(User user, String name) { responseList.add(getApiDiscoveryResponseWithAccessibleParams(name, account)); } else { - if (account == null) { - throw new PermissionDeniedException(String.format("The account with id [%s] for user [%s] is null.", user.getAccountId(), user)); - } - - final Role role = roleService.findRole(account.getRoleId()); if (role == null || role.getId() < 1L) { throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); } - if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) { - logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.", - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); + // Limit APIs on first login requiring password change + UserAccount userAccount = accountService.getUserAccountById(user.getId()); + Map userAccDetails = userAccount.getDetails(); + if (MapUtils.isNotEmpty(userAccDetails) && !userAccDetails.containsKey(UserDetailVO.OauthLogin) && + "true".equalsIgnoreCase(userAccDetails.get(UserDetailVO.PasswordChangeRequired))) { + apisAllowed = APIS_ALLOWED_FOR_PASSWORD_CHANGE; } else { - for (APIChecker apiChecker : _apiAccessCheckers) { - apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed); + if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) { + logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); + } else { + for (APIChecker apiChecker : _apiAccessCheckers) { + apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed); + } } } @@ -331,6 +353,44 @@ public List> getCommands() { return cmdList; } + protected List listApisForKeyPair(String apiKey, String apiName, User user, Role role, List apisAllowed) { + List keyPairPermissions = accountService.getAllKeypairPermissions(apiKey); + + List filteredApis = new ArrayList<>(); + if (apiName != null && isApiAllowedForKey(keyPairPermissions, apiName)) { + filteredApis = List.of(apiName); + } else { + for (String api : apisAllowed) { + if (isApiAllowedForKey(keyPairPermissions, api)) { + filteredApis.add(api); + } + } + } + + checkRateLimit(user, role, filteredApis); + return filteredApis.stream().map(api -> s_apiNameDiscoveryResponseMap.get(api)).collect(Collectors.toList()); + } + + protected boolean isApiAllowedForKey(List rolePermissionEntities, String apiName) { + for (RolePermissionEntity rolePermissionEntity : rolePermissionEntities) { + if (!rolePermissionEntity.getRule().matches(apiName)) { + continue; + } + return rolePermissionEntity.getPermission().equals(RolePermissionEntity.Permission.ALLOW); + } + return false; + } + + private void checkRateLimit(User user, Role role, List apiNames) { + for (APIChecker apiChecker : _apiAccessCheckers) { + if (!(apiChecker instanceof ApiRateLimitService)) { + continue; + } + apiChecker.getApisAllowedToUser(role, user, apiNames); + return; + } + } + public List getApiAccessCheckers() { return _apiAccessCheckers; } diff --git a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java index eea78d8abb93..59415ccd1eb2 100644 --- a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java +++ b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java @@ -21,6 +21,8 @@ import com.cloud.user.AccountService; import com.cloud.user.AccountVO; import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; @@ -29,6 +31,8 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.api.response.ApiDiscoveryResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,11 +43,15 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.Setup2FADetail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; @RunWith(MockitoJUnitRunner.class) public class ApiDiscoveryTest { @@ -66,12 +74,17 @@ public class ApiDiscoveryTest { @InjectMocks ApiDiscoveryServiceImpl discoveryServiceSpy; + @Mock + UserAccount mockUserAccount; + @Before public void setup() { discoveryServiceSpy.s_apiNameDiscoveryResponseMap = apiNameDiscoveryResponseMapMock; discoveryServiceSpy._apiAccessCheckers = apiAccessCheckersMock; Mockito.when(discoveryServiceSpy._apiAccessCheckers.iterator()).thenReturn(Arrays.asList(apiCheckerMock).iterator()); + Mockito.when(mockUserAccount.getDetails()).thenReturn(null); + Mockito.when(accountServiceMock.getUserAccountById(anyLong())).thenReturn(mockUserAccount); } private User getTestUser() { @@ -86,7 +99,7 @@ private Account getNormalAccount() { @Test (expected = PermissionDeniedException.class) public void listApisTestThrowPermissionDeniedExceptionOnAccountNull() throws PermissionDeniedException { Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(null); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test (expected = PermissionDeniedException.class) @@ -94,7 +107,7 @@ public void listApisTestThrowPermissionDeniedExceptionOnRoleNull() throws Permis Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(null); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test (expected = PermissionDeniedException.class) @@ -104,7 +117,7 @@ public void listApisTestThrowPermissionDeniedExceptionOnRoleUnknown() throws Per Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(unknownRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); } @Test @@ -115,7 +128,7 @@ public void listApisTestDoesNotGetApisAllowedToUserOnAdminRole() throws Permissi Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(adminAccountVO); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(adminRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); Mockito.verify(apiCheckerMock, Mockito.times(0)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); } @@ -127,8 +140,33 @@ public void listApisTestGetsApisAllowedToUserOnUserRole() throws PermissionDenie Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); - discoveryServiceSpy.listApis(getTestUser(), null); + discoveryServiceSpy.listApis(getTestUser(), null, null); Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); } + + @Test + public void listApisForUserWithoutEnforcedPwdChange() throws PermissionDeniedException { + RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description"); + Map userDetails = new HashMap<>(); + userDetails.put(Setup2FADetail, UserAccountVO.Setup2FAstatus.ENABLED.name()); + Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails); + Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); + discoveryServiceSpy.listApis(getTestUser(), null, null); + Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); + } + + @Test + public void listApisForUserEnforcedPwdChange() throws PermissionDeniedException { + RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description"); + Map userDetails = new HashMap<>(); + userDetails.put(PasswordChangeRequired, "true"); + Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails); + Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); + Mockito.when(apiNameDiscoveryResponseMapMock.get(Mockito.anyString())).thenReturn(Mockito.mock(ApiDiscoveryResponse.class)); + ListResponse response = (ListResponse) discoveryServiceSpy.listApis(getTestUser(), null, null); + Assert.assertEquals(4, response.getResponses().size()); + } } diff --git a/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java b/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java index 917cd7bb2b46..afa2b6155de6 100644 --- a/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java +++ b/plugins/api/rate-limit/src/main/java/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java @@ -27,6 +27,9 @@ import net.sf.ehcache.CacheManager; import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermissionEntity; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.springframework.stereotype.Component; @@ -161,17 +164,17 @@ public void throwExceptionDueToApiRateLimitReached(Long accountId) throws Reques } @Override - public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException { + public boolean checkAccess(User user, String apiCommandName, ApiKeyPairPermission ... apiKeyPairPermissions) throws PermissionDeniedException { if (!isEnabled()) { return true; } Account account = _accountService.getAccount(user.getAccountId()); - return checkAccess(account, apiCommandName); + return checkAccess(account, apiCommandName, apiKeyPairPermissions); } @Override - public boolean checkAccess(Account account, String commandName) { + public boolean checkAccess(Account account, String commandName, ApiKeyPairPermission ... apiKeyPairPermissions) { Long accountId = account.getAccountId(); if (_accountService.isRootAdmin(accountId)) { logger.info(String.format("Account [%s] is Root Admin, in this case, API limit does not apply.", @@ -207,6 +210,11 @@ public boolean hasApiRateLimitBeenExceeded(Long accountId) { return true; } + @Override + public List getImplicitRolePermissions(RoleType roleType) { + return List.of(); + } + @Override public boolean isEnabled() { if (!enabled) { diff --git a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java index b228a9f8ce05..00bb353788f9 100644 --- a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java +++ b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java @@ -90,13 +90,14 @@ public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backup } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { logger.debug("Restoring vm {} from backup {} on the Dummy Backup Provider", vm, backup); return true; } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final StoragePoolHostVO dataStore = storagePoolHostDao.findByUuid(dataStoreUuid); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); @@ -153,7 +154,7 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated) { logger.debug("Starting backup for VM {} on Dummy provider", vm); BackupVO backup = new BackupVO(); @@ -204,7 +205,7 @@ public void syncBackupStorageStats(Long zoneId) { } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return new Pair<>(true, null); } } diff --git a/plugins/backup/kboss/pom.xml b/plugins/backup/kboss/pom.xml new file mode 100644 index 000000000000..eb8cbd03efa1 --- /dev/null +++ b/plugins/backup/kboss/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + cloud-plugin-backup-kvm-backup-on-secondary-storage + Apache CloudStack Plugin - KVM Backup On Secondary Storage + + cloudstack-plugins + org.apache.cloudstack + 4.23.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-plugin-hypervisor-kvm + ${project.version} + + + org.apache.cloudstack + cloud-engine-components-api + ${project.version} + compile + + + org.apache.cloudstack + cloud-engine-orchestration + ${project.version} + compile + + + diff --git a/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java b/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java new file mode 100644 index 000000000000..596dc62b7206 --- /dev/null +++ b/plugins/backup/kboss/src/main/java/org/apache/cloudstack/backup/KbossBackupProvider.java @@ -0,0 +1,2921 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.backup; + +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.BACKUP_HASH; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.CURRENT; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.END_OF_CHAIN; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.IMAGE_STORE_ID; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.ISOLATED; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.PARENT_ID; +import static org.apache.cloudstack.backup.dao.BackupDetailsDao.SCREENSHOT_PATH; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.alert.AlertService; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.jobs.AsyncJob; +import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.Outcome; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.framework.jobs.impl.OutcomeImpl; +import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.secstorage.heuristics.HeuristicType; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.manager.Commands; +import com.cloud.alert.AlertManager; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.resource.ResourceState; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.uservm.UserVm; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Predicate; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.exception.BackupProviderException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmManager; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VirtualMachineManagerImpl; +import com.cloud.vm.VirtualMachineProfileImpl; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.VmWork; +import com.cloud.vm.VmWorkConstants; +import com.cloud.vm.VmWorkDeleteBackup; +import com.cloud.vm.VmWorkRestoreBackup; +import com.cloud.vm.VmWorkRestoreVolumeBackupAndAttach; +import com.cloud.vm.VmWorkSerializer; +import com.cloud.vm.VmWorkTakeBackup; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +public class KbossBackupProvider extends AdapterBase implements InternalBackupProvider, Configurable { + protected ConfigKey backupChainSize = new ConfigKey<>("Advanced", Integer.class, "backup.chain.size", "8", "Determines the max size of a backup chain." + + " Currently only used by the KBOSS provider. If cloud admins set it to 1 , all the backups will be full backups. With values lower than 1, the backup chain will be " + + "unlimited, unless it is stopped by another process. Please note that unlimited backup chains have a higher chance of getting corrupted, as new backups will be" + + " dependant on all of the older ones.", true, ConfigKey.Scope.Zone); + + protected ConfigKey backupTimeout = new ConfigKey<>("Advanced", Integer.class, "kboss.timeout", "43200", "Timeout, in seconds, to execute KBOSS commands. After the " + + "command times out, the Management Server will still wait for another kboss.timeout seconds to receive a response from the Agent.", true, ConfigKey.Scope.Zone); + + @Inject + private AsyncJobManager jobManager; + @Inject + private EntityManager entityManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private UserVmDao userVmDao; + + @Inject + private VMInstanceDetailsDao vmInstanceDetailsDao; + + @Inject + private VMSnapshotHelper vmSnapshotHelper; + + @Inject + private SnapshotDataStoreDao snapshotDataStoreDao; + + @Inject + private VMSnapshotDao vmSnapshotDao; + + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + + @Inject + private BackupDao backupDao; + + @Inject + private InternalBackupJoinDao internalBackupJoinDao; + + @Inject + private BackupDetailsDao backupDetailDao; + + @Inject + private InternalBackupStoragePoolDao internalBackupStoragePoolDao; + + @Inject + private InternalBackupDataStoreDao internalBackupDataStoreDao; + + @Inject + private BackupOfferingDao backupOfferingDao; + + @Inject + private BackupOfferingDetailsDao backupOfferingDetailsDao; + + @Inject + private HeuristicRuleHelper heuristicRuleHelper; + + @Inject + private DataStoreManager dataStoreManager; + + @Inject + private AgentManager agentManager; + + @Inject + private EndPointSelector endPointSelector; + + @Inject + private VolumeDao volumeDao; + + @Inject + private ImageStoreDao imageStoreDao; + + @Inject + private VolumeApiService volumeApiService; + + @Inject + private PrimaryDataStoreDao storagePoolDao; + + @Inject + private HostDao hostDao; + + @Inject + private UserVmManager userVmManager; + + @Inject + private VolumeOrchestrationService volumeOrchestrationService; + + @Inject + private VolumeDataFactory volumeDataFactory; + @Inject + private InternalBackupServiceJobDao internalBackupServiceJobDao; + + @Inject + private BackupManager backupManager; + + @Inject + private DiskOfferingDao diskOfferingDao; + + @Inject + private HypervisorGuruManager hypervisorGuruManager; + + @Inject + private NicDao nicDao; + + @Inject + private AlertManager alertManager; + + protected final List validChildStatesToRemoveBackup = List.of(Backup.Status.Expunged, Backup.Status.Error, Backup.Status.Failed); + + private final List supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, + Storage.StoragePoolType.SharedMountPoint); + + private final List allowedBackupStatesToRemove = List.of(Backup.Status.BackedUp, Backup.Status.Failed, Backup.Status.Error); + + private final List allowedBackupStatesToCompress = List.of(Backup.Status.BackedUp, Backup.Status.Restoring); + + private final List allowedBackupStatesToValidate = List.of(Backup.Status.BackedUp, Backup.Status.Restoring); + + private final List allowedVmStates = Arrays.asList(VirtualMachine.State.Running, VirtualMachine.State.Stopped); + + @Override + public String getName() { + return "kboss"; + } + + @Override + public String getDescription() { + return "KVM Backup on Secondary Storage"; + } + + @Override + public List listBackupOfferings(Long zoneId) { + return List.of(); + } + + @Override + public boolean isValidProviderOffering(Long zoneId, String uuid) { + return true; + } + + @Override + public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { + logger.debug("Assigning VM [{}] to KBOSS backup offering with name:[{}], uuid: [{}].", vm.getUuid(), backupOffering.getName(), backupOffering.getUuid()); + if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) { + logger.error("KVM Backup on Secondary Storage provider is only supported for KVM."); + return false; + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (!vmSnapshotDetails.stream().allMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + logger.error("KBOSS is only supported with disk-only VM snapshots using [{}] strategy. Found a disk-only VM snapshot using another strategy for the VM.", + VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT); + logger.debug("Found VM snapshot details [{}].", () -> vmSnapshotDetails.stream().map(VMSnapshotDetailsVO::getName).collect(Collectors.toList())); + return false; + } + } + + return CollectionUtils.isEmpty(vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.DiskAndMemory)); + } + + @Override + public boolean removeVMFromBackupOffering(VirtualMachine vm) { + logger.info("Removing VM [{}] from KBOSS backup offering.", vm.getUuid()); + + validateVmState(vm, "remove backup offering", VirtualMachine.State.Expunging, VirtualMachine.State.Destroyed); + if (endBackupChain(vm)) { + return true; + } + UserVmVO vmVO = userVmDao.findById(vm.getId()); + logger.error("Failed to merge deltas for VM [{}] during backup offering removal process. Changing its state to [{}].", vm, VirtualMachine.State.BackupError); + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, vmVO.getState().name(), false); + vmVO.setState(VirtualMachine.State.BackupError); + userVmDao.update(vmVO.getId(), vmVO); + + return false; + } + + @Override + public boolean willDeleteBackupsOnOfferingRemoval() { + return false; + } + + @Override + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVm, boolean isolated) { + logger.debug("Queueing backup on VM [{}].", vm.getUuid()); + Outcome outcome = createBackupThroughJobQueue(vm, ObjectUtils.defaultIfNull(quiesceVm, false), isolated); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job takeBackup due to [%s]. VM [%s].", e.getMessage(), vm.getUuid()), e); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } else if (jobResult instanceof Throwable) { + throw new CloudRuntimeException(String.format("Exception while taking KBOSS backup for VM [%s]. Check the logs for more information.", vm.getUuid())); + } + + Pair result = (Pair)jobResult; + Pair returnValue = new Pair<>(result.first(), null); + if (result.first()) { + returnValue.second(backupDao.findById(result.second())); + } + return returnValue; + } + + @Override + public Pair orchestrateTakeBackup(Backup backup, boolean quiesceVm, boolean isolated) { + BackupVO backupVO = (BackupVO) backup; + long vmId = backup.getVmId(); + VirtualMachine userVm = virtualMachineManager.findById(vmId); + Long hostId = vmSnapshotHelper.pickRunningHost(vmId); + HostVO hostVO = hostDao.findById(hostId); + + if (hostVO.getStatus() != Status.Up || hostVO.getResourceState() != ResourceState.Enabled) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + + logger.error("No available host found to create backup [{}] of VM [{}]. Setting the backup as Failed.", backupVO.getUuid(), userVm.getUuid()); + return new Pair<>(Boolean.FALSE, backup.getId()); + } + + List volumeTOs; + try { + validateVmState(userVm, "take backup"); + volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + validateStorages(volumeTOs, userVm.getUuid()); + } catch (Exception e) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + throw e; + } + + logger.info("Starting VM backup process for VM [{}].", userVm.getUuid()); + + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + + backupVO.setDate(new Date()); + List backupChain = getBackupJoinParents(backupVO, true); + InternalBackupJoinVO parentBackup = null; + if (isolated) { + setBackupAsIsolated(backupVO); + } else { + parentBackup = getParentAndSetEndOfChain(backupVO, backupChain, backupOfferingVO); + } + InternalBackupJoinVO newBackupJoin = internalBackupJoinDao.findById(backup.getId()); + boolean fullBackup = parentBackup == null; + List parentBackupDeltasOnPrimary = new ArrayList<>(); + List parentBackupDeltasOnSecondary = new ArrayList<>(); + List chainImageStoreUrls = null; + List kbossTOs = new ArrayList<>(); + HashMap volumeUuidToDeltaPrimaryRef = new HashMap<>(); + HashMap volumeUuidToDeltaSecondaryRef = new HashMap<>(); + + if (!fullBackup) { + parentBackupDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parentBackup.getId()); + parentBackupDeltasOnSecondary = internalBackupDataStoreDao.listByBackupId(parentBackup.getId()); + + chainImageStoreUrls = getChainImageStoreUrls(backupChain); + } + + boolean runningVm = userVm.getState() == VirtualMachine.State.Running; + transitVmState(userVm, VirtualMachine.Event.BackupRequested, hostId); + updateBackupStatusToBackingUp(volumeTOs, backupVO); + + DataStore imageStore = getImageStoreForBackup(userVm.getDataCenterId(), backupVO); + createBasicBackupDetails(imageStore.getId(), fullBackup ? 0L : parentBackup.getId(), backupVO); + + List succeedingVmSnapshotList = getSucceedingVmSnapshotList(parentBackup); + VMSnapshotVO succeedingVmSnapshot = succeedingVmSnapshotList.isEmpty() ? null : succeedingVmSnapshotList.get(0); + + Map> volumeIdToSnapshotDataStoreList = mapVolumesToVmSnapshotReferences(volumeTOs, succeedingVmSnapshotList); + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + KbossTO kbossTO = new KbossTO(volumeObjectTO, volumeIdToSnapshotDataStoreList.getOrDefault(volumeObjectTO.getId(), new ArrayList<>())); + kbossTOs.add(kbossTO); + createDeltaReferences(fullBackup, !succeedingVmSnapshotList.isEmpty(), runningVm, backup, parentBackupDeltasOnSecondary, + parentBackupDeltasOnPrimary, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, succeedingVmSnapshot, kbossTO); + } + + TakeKbossBackupCommand command = new TakeKbossBackupCommand(quiesceVm, runningVm, newBackupJoin.getEndOfChain(), userVm.getInstanceName(), imageStore.getUri(), + chainImageStoreUrls, kbossTOs, isolated); + + Answer answer = sendBackupCommand(hostId, command); + + if (answer == null || !answer.getResult()) { + processBackupFailure(answer, userVm, hostId, runningVm, backupVO); + return new Pair<>(Boolean.FALSE, null); + } + + processBackupSuccess(runningVm, volumeTOs, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, (TakeKbossBackupAnswer)answer, parentBackupDeltasOnPrimary, + succeedingVmSnapshotList, backupVO, fullBackup, userVm, hostId, newBackupJoin.getEndOfChain(), isolated); + + if (!isolated) { + updateCurrentBackup(newBackupJoin); + } + + if (offeringSupportsCompression(newBackupJoin)) { + compressBackupAsync(newBackupJoin, backup.getZoneId(), userVm.getAccountId()); + } else { + validateBackupAsyncIfHasOfferingSupport(newBackupJoin, backup.getZoneId(), userVm.getAccountId()); + } + return new Pair<>(Boolean.TRUE, backupVO.getId()); + } + + @Override + public boolean deleteBackup(Backup backup, boolean forced) { + logger.debug("Queueing backup [{}] deletion.", backup.getUuid()); + Outcome outcome = deleteBackupThroughJobQueue(backup, forced); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job deleteBackup due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + if (jobResult instanceof Throwable) { + if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } + throw new CloudRuntimeException(String.format("Exception while deleting KBOSS backup [%s]. Check the logs for more information.", backup.getUuid())); + } + + return BooleanUtils.isTrue((Boolean) jobResult); + } + + @Override + public Boolean orchestrateDeleteBackup(Backup backup, boolean forced) { + BackupVO backupVO = (BackupVO) backup; + + VirtualMachine virtualMachine = virtualMachineManager.findById(backup.getVmId()); + + if (virtualMachine != null) { + validateVmState(virtualMachine, "delete backup", VirtualMachine.State.Destroyed); + } + + logger.info("Starting delete process for backup [{}].", backupVO); + + if (!validateBackupStateForRemoval(backupVO.getId())) { + return false; + } + + checkErrorBackup(backupVO, virtualMachine); + if (deleteFailedBackup(backupVO)) { + return true; + } + + InternalBackupJoinVO childBackup = internalBackupJoinDao.findByParentId(backup.getId()); + + if (childBackup != null && !validChildStatesToRemoveBackup.contains(childBackup.getStatus())) { + logger.debug("Backup [{}] has children that are not in one of the following states [{}]; will mark it as removed on the database but the files will not be deleted " + + "from secondary storage until the children are also expunged.", backup.getUuid(), validChildStatesToRemoveBackup); + backupVO.setStatus(Backup.Status.Removed); + backupDao.update(backupVO.getId(), backupVO); + return true; + } + + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backup.getId()); + if (backupJoinVO.getCurrent()) { + if (!mergeCurrentBackupDeltas(backupJoinVO)) { + return false; + } + InternalBackupJoinVO parent = internalBackupJoinDao.findById(backupJoinVO.getParentId()); + if (parent != null && parent.getStatus() == Backup.Status.BackedUp) { + backupDetailDao.persist(new BackupDetailVO(parent.getId(), END_OF_CHAIN, Boolean.TRUE.toString(), false)); + } + } + + Commands deleteCommands = new Commands(Command.OnError.Continue); + + DataStore dataStore = addBackupDeltasToDeleteCommand(backup.getId(), deleteCommands); + Pair, InternalBackupJoinVO> backupParentsToBeRemovedAndLastAliveBackup = getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVO, + deleteCommands); + + EndPoint endPoint = endPointSelector.select(dataStore); + if (endPoint == null) { + logger.error("Unable to find SSVM to delete backup [{}]. Check if SSVM is up for the zone.", backup); + throw new CloudRuntimeException(String.format("Unable to delete backup [%s]. Please check the logs.", backup.getUuid())); + } + Answer[] deleteAnswers; + try { + deleteAnswers = sendBackupCommands(endPoint.getId(), deleteCommands); + } catch (AgentUnavailableException | OperationTimedoutException e) { + throw new CloudRuntimeException(e); + } + + List removedBackupIds = backupParentsToBeRemovedAndLastAliveBackup.first().stream().map(InternalBackupJoinVO::getId).collect(Collectors.toList()); + removedBackupIds.add(backup.getId()); + + boolean isFailedSetEmpty = processRemoveBackupFailures(forced, deleteAnswers, removedBackupIds, backupJoinVO); + + processRemovedBackups(removedBackupIds); + + if (backupParentsToBeRemovedAndLastAliveBackup.second() != null) { + backupDetailDao.persist(new BackupDetailVO(backupParentsToBeRemovedAndLastAliveBackup.second().getId(), END_OF_CHAIN, Boolean.TRUE.toString(), false)); + } + + return isFailedSetEmpty; + } + + @Override + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { + logger.debug("Queueing backup [{}] restore for VM [{}].", backup.getUuid(), vm.getUuid()); + validateQuickRestore(backup, quickRestore); + + Outcome outcome = restoreVMFromBackupThroughJobQueue(vm, backup, quickRestore, hostId); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job restoreVMFromBackup due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } finally { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(Backup.Status.BackedUp); + backupDao.update(backupVO.getId(), backupVO); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + handleRestoreException(backup, vm, jobResult); + + return BooleanUtils.isTrue((Boolean) jobResult); + } + + @Override + public Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, boolean quickRestore, Long hostId, boolean sameVmAsBackup) { + logger.info("Starting restore backup process for VM [{}] and backup [{}].", vm.getUuid(), backup); + validateNoVmSnapshots(vm); + validateQuickRestore(backup, quickRestore); + long backupId = backup.getId(); + Pair isValidStateAndBackupVo = validateCompressionStateForRestoreAndGetBackup(backupId); + + if (!isValidStateAndBackupVo.first()) { + return false; + } + + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + InternalBackupJoinVO currentBackup = sameVmAsBackup ? internalBackupJoinDao.findCurrent(vm.getId()) : null; + List deltasOnPrimary = new ArrayList<>(); + if (currentBackup != null) { + deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(currentBackup.getId()); + } + List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(backupId); + List volumeTOs = vmSnapshotHelper.getVolumeTOList(vm.getId()); + + Set deltasToRemove = new HashSet<>(); + + List backupsWithoutVolumes = sameVmAsBackup ? getBackupsWithoutVolumes(deltasOnSecondary, volumeTOs) : List.of(); + + HostVO host; + try { + host = getHostToRestore(vm, quickRestore, hostId); + } catch (AgentUnavailableException e) { + throw new CloudRuntimeException(e); + } + + BackupVO backupVO = isValidStateAndBackupVo.second(); + List volumeInfos = backupVO.getBackedUpVolumes(); + if (sameVmAsBackup) { + createAndAttachVolumes(volumeInfos, backupsWithoutVolumes, vm, host); + // Get new volume references + volumeTOs = vmSnapshotHelper.getVolumeTOList(vm.getId()); + } + + Set> backupAndVolumePairs = generateBackupAndVolumePairsToRestore(deltasOnSecondary, volumeTOs, backupJoinVO, sameVmAsBackup); + + List deltasToBeMerged = List.of(); + if (sameVmAsBackup) { + List volumesNotPartOfTheBackup = getVolumesThatAreNotPartOfTheBackup(volumeTOs, deltasOnSecondary); + deltasToBeMerged = populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(deltasOnPrimary, deltasToRemove, volumeTOs, volumesNotPartOfTheBackup, + vm.getUuid()); + } + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + + Commands commands = new Commands(Command.OnError.Stop); + commands.addCommand(new RestoreKbossBackupCommand(deltasToRemove, backupAndVolumePairs, secondaryStorageUrls, quickRestore)); + commands.addCommand(new MergeDiskOnlyVmSnapshotCommand(deltasToBeMerged, vm.getState().equals(VirtualMachine.State.Running), vm.getInstanceName())); + + Answer[] answers; + + try { + answers = sendBackupCommands(host.getId(), commands); + } catch (OperationTimedoutException | AgentUnavailableException e) { + throw new CloudRuntimeException(e); + } + + if (answers == null) { + logger.error("Failed to restore backup [{}] due to no answer from host.", backup); + return false; + } + + if (!processRestoreAnswers(vm, answers, quickRestore)) { + return false; + } + + updateVolumePathsAndSizeIfNeeded(vm, volumeTOs, volumeInfos, deltasToBeMerged, sameVmAsBackup); + + if (currentBackup != null) { + internalBackupStoragePoolDao.expungeByBackupId(currentBackup.getId()); + setEndOfChainAndRemoveCurrentForBackup(currentBackup); + } + + if (quickRestore) { + List volumesToConsolidate = getVolumesToConsolidate(vm, deltasOnSecondary, volumeTOs, host.getId(), sameVmAsBackup); + return finalizeQuickRestore(vm, volumesToConsolidate, host.getId()); + } + + return true; + } + + @Override + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { + logger.debug("Queueing backup [{}] volume [{}] restore for VM [{}].", backup.getUuid(), backupVolumeInfo, vm.getUuid()); + validateQuickRestore(backup, quickRestore); + Outcome outcome = restoreBackedUpVolumeThroughJobQueue(vm, backup, backupVolumeInfo, hostIp, quickRestore); + + try { + outcome.get(); + } catch (InterruptedException | ExecutionException e) { + throw new CloudRuntimeException(String.format("Unable to retrieve result from job restoreBackedUpVolume due to [%s]. Backup [%s].", e.getMessage(), backup.getUuid()), e); + } finally { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(Backup.Status.BackedUp); + backupDao.update(backupVO.getId(), backupVO); + } + + Object jobResult = jobManager.unmarshallResultObject(outcome.getJob()); + + handleRestoreException(backup, vm, jobResult); + + if (!(jobResult instanceof Pair)) { + throw new CloudRuntimeException(String.format("Unexpected answer from restoreBackupVolume job. Got [%s].", jobResult)); + } + return (Pair) jobResult; + } + + @Override + public Pair orchestrateRestoreBackedUpVolume(Backup backup, VirtualMachine vm, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + BackupVO backupVO = (BackupVO) backup; + Pair isValidStateAndBackupVo = validateCompressionStateForRestoreAndGetBackup(backup.getId()); + + if (!isValidStateAndBackupVo.first()) { + return new Pair<>(false, null); + } + + VolumeVO backedUpVolume = volumeDao.findByUuidIncludingRemoved(backupVolumeInfo.getUuid()); + HostVO hostVo = hostDao.findByIp(hostIp); + VolumeInfo volumeInfo = duplicateAndCreateVolume(vm, hostVo, backupVolumeInfo); + + VolumeObjectTO volumeObjectTO = (VolumeObjectTO) volumeInfo.getTO(); + InternalBackupDataStoreVO deltaOnSecondary = internalBackupDataStoreDao.findByBackupIdAndVolumeId(backup.getId(), backedUpVolume.getId()); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backup.getId()); + Pair backupAndVolumePair = generateBackupAndVolumePairForSingleNewVolume(deltaOnSecondary, volumeObjectTO, internalBackupJoinVO); + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + + RestoreKbossBackupCommand cmd = new RestoreKbossBackupCommand(Set.of(), Set.of(backupAndVolumePair), secondaryStorageUrls, quickRestore); + + Answer answer = sendBackupCommand(hostVo.getId(), cmd); + + if (!processRestoreAnswers(vm, new Answer[] {answer}, quickRestore)) { + throw new CloudRuntimeException("Bad answer from agent"); + } + + VolumeVO newVolume = (VolumeVO)volumeInfo.getVolume(); + volumeDao.update(newVolume.getId(), newVolume); + + Volume attachedVolume = volumeApiService.attachVolumeToVM(vm.getId(), newVolume.getId(), null, false, false); + + if (quickRestore) { + ArrayList volumeToConsolidate = new ArrayList<>(); + volumeToConsolidate.add(volumeDataFactory.getVolume(attachedVolume.getId())); + return new Pair<>(finalizeQuickRestore(vm, volumeToConsolidate, hostVo.getId()), attachedVolume.getUuid()); + } + + return new Pair<>(true, attachedVolume.getUuid()); + } + + @Override + public boolean startBackupCompression(long backupId, long hostId) { + Pair validCompressAndBackupVO = validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + if (!validCompressAndBackupVO.first()) { + return false; + } + + InternalBackupJoinVO backup = internalBackupJoinDao.findById(backupId); + InternalBackupJoinVO parentBackup = internalBackupJoinDao.findById(backup.getParentId()); + + List backupDeltas = internalBackupDataStoreDao.listByBackupId(backupId); + List parentBackupDeltas = parentBackup != null ? internalBackupDataStoreDao.listByBackupId(backup.getParentId()) : List.of(); + + DataStoreTO imageStoreTo = dataStoreManager.getDataStore(backup.getImageStoreId(), DataStoreRole.Image).getTO(); + DataStoreTO parentStoreTo = parentBackup != null ? dataStoreManager.getDataStore(parentBackup.getImageStoreId(), DataStoreRole.Image).getTO() : null; + + List deltasToCompressAndParents = new ArrayList<>(); + for (InternalBackupDataStoreVO delta : backupDeltas) { + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(imageStoreTo, Hypervisor.HypervisorType.KVM, delta.getBackupPath()); + InternalBackupDataStoreVO parentDataStore = parentBackupDeltas.stream().filter(parent -> parent.getVolumeId() == delta.getVolumeId()).findFirst().orElse(null); + BackupDeltaTO parentDeltaTO = parentDataStore != null ? new BackupDeltaTO(parentStoreTo, Hypervisor.HypervisorType.KVM, parentDataStore.getBackupPath()) : null; + deltasToCompressAndParents.add(new DeltaMergeTreeTO(null, parentDeltaTO, backupDeltaTO, null)); + } + + HostVO hostVO = hostDao.findById(hostId); + BackupVO backupVO = validCompressAndBackupVO.second(); + + long minFreeStorage = Math.round(backupVO.getSize() * backupCompressionMinimumFreeStorage.valueIn(hostVO.getDataCenterId())); + + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backupVO.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.COMPRESSION_LIBRARY); + List backupChain = getBackupJoinParents(backupVO, true); + List chainImageStoreUrls = getChainImageStoreUrls(backupChain); + CompressBackupCommand cmd = new CompressBackupCommand(deltasToCompressAndParents, chainImageStoreUrls, minFreeStorage, detail == null ? null : + Backup.CompressionLibrary.valueOf(detail.getValue()), backupCompressionCoroutines.valueIn(hostVO.getClusterId()), + backupCompressionRateLimit.valueIn(hostVO.getClusterId())); + cmd.setWait(backupCompressionTimeout.valueIn(hostVO.getClusterId())); + Answer answer = agentManager.easySend(hostId, cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Failed to compress backup [{}] due to {}.", backup.getUuid(), answer == null ? "no answer" : answer.getDetails()); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + return false; + } + + logger.info("Successfully completed the first step of the backup compression process for backup [{}]. Will launch a new compression job to finalize the compression.", + backup.getUuid()); + + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupVO.getId(), backupVO.getZoneId(), backupVO.getVmId(), backupVO.getAccountId(), + InternalBackupServiceJobType.FinalizeCompression)); + + return true; + } + + @Override + public boolean finalizeBackupCompression(long backupId, long hostId) { + Pair shouldContinueProcessAndBackupVo = validateBackupStateForFinalizeCompression(backupId); + if (!shouldContinueProcessAndBackupVo.first()) { + return false; + } + BackupVO backupVO = shouldContinueProcessAndBackupVo.second(); + + List deltaTOs = getBackupDeltaTOList(backupId); + + FinalizeBackupCompressionCommand cmd = new FinalizeBackupCompressionCommand(backupVO.getStatus() != Backup.Status.BackedUp, deltaTOs); + HostVO hostVO = hostDao.findById(hostId); + cmd.setWait(backupCompressionTimeout.valueIn(hostVO.getClusterId())); + Answer answer = agentManager.easySend(hostId, cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Failed to finish compression of backup [{}] due to {}.", backupVO.getUuid(), answer == null ? "no answer" : answer.getDetails()); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + return false; + } + + if (cmd.isCleanup()) { + logger.info("Successfully cleaned up backup compression of backup [{}].", backupVO); + return true; + } + + backupVO.setCompressionStatus(Backup.CompressionStatus.Compressed); + backupVO.setUncompressedSize(backupVO.getSize()); + backupVO.setSize(Long.parseLong(answer.getDetails())); + backupDao.update(backupVO.getId(), backupVO); + + logger.info("Finalized compression for backup [{}], old size was [{}], compressed size is [{}].", backupVO.getUuid(), backupVO.getUncompressedSize(), backupVO.getSize()); + + validateBackupAsyncIfHasOfferingSupport(internalBackupJoinDao.findById(backupId), backupVO.getZoneId(), backupVO.getAccountId()); + return true; + } + + @Override + public boolean validateBackup(long backupId, long hostId) { + if (!validateBackupStateForValidation(backupId)) { + return false; + } + BackupVO backupVO = backupDao.findById(backupId); + backupVO.setValidationStatus(Backup.ValidationStatus.Validating); + backupDao.update(backupId, backupVO); + BackupDetailVO hashDetail = backupDetailDao.findDetail(backupId, BACKUP_HASH); + if (hashDetail != null) { + return validateWithHash(backupId, backupVO, hashDetail); + } else { + return validateWithValidationVm(backupId, hostId, backupVO); + } + } + + @Override + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickRestore) { + Pair shouldRestoreAndOldStatus = validateBackupStateForRestoreBackupToVM(backup.getId()); + if (!shouldRestoreAndOldStatus.first()) { + return new Pair<>(false, "Backup is not in the right state."); + } + + boolean result = false; + try { + result = orchestrateRestoreVMFromBackup(backup, vm, quickRestore, null, false); + } catch (Exception exception) { + handleRestoreException(backup, vm, exception); + } finally { + Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) transactionStatus -> { + BackupVO backupVO = backupDao.findById(backup.getId()); + backupVO.setStatus(shouldRestoreAndOldStatus.second()); + backupDao.update(backupVO.getId(), backupVO); + return true; + }); + } + + return new Pair<>(result, null); + } + + @Override + public boolean finishBackupChain(VirtualMachine virtualMachine) { + UserVmVO userVmVO = userVmDao.findById(virtualMachine.getId()); + if (allowedVmStates.contains(userVmVO.getState())) { + return endBackupChain(userVmVO); + } + if (userVmVO.getState() != VirtualMachine.State.BackupError) { + logger.error("VM [{}] is not in the right state to finish backup chain. It can only be in states [Running, Stopped and BackupError].", userVmVO.getUuid()); + + return false; + } + return normalizeBackupErrorAndFinishChain(userVmVO); + } + + @Override + public void syncBackupMetrics(Long zoneId) { + } + + @Override + public Backup createNewBackupEntryForRestorePoint(Backup.RestorePoint rp, VirtualMachine vm) { + return null; + } + + @Override + public Pair getBackupStorageStats(Long zoneId) { + return new Pair<>(0L, 0L); + } + + @Override + public void syncBackupStorageStats(Long zoneId) { + } + + @Override + public boolean supportsInstanceFromBackup() { + return true; + } + + @Override + public boolean supportsMemoryVmSnapshot() { + return false; + } + + @Override + public void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { + logger.info("Preparing volume [{}] for detach.", volume.getUuid()); + mergeCurrentDeltaIntoVolume(volume, virtualMachine, "detach", virtualMachine.getState().equals(VirtualMachine.State.Running)); + } + + @Override + public void prepareVolumeForMigration(Volume volume, VirtualMachine vm) { + if (VirtualMachine.State.Migrating.equals(vm.getState())) { + logger.info("Preparing volume [{}] for live migration.", volume.getUuid()); + mergeCurrentDeltaIntoVolume(volume, vm, "live migration", true); + } + } + + @Override + public void updateVolumeId(VirtualMachine virtualMachine, long oldVolumeId, long newVolumeId) { + internalBackupDataStoreDao.updateVolumeId(oldVolumeId, newVolumeId); + } + + @Override + public void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { + InternalBackupJoinVO currentBackup = internalBackupJoinDao.findCurrent(virtualMachine.getId()); + + if (currentBackup == null) { + logger.debug("There is no current backup delta, the VM [{}] is already prepared for VM snapshot revert.", virtualMachine.getUuid()); + return; + } + if (currentBackup.getDate().before(vmSnapshot.getCreated())) { + logger.debug("The current backup delta was taken before [{}] the VM snapshot being reverted [{}], no need to prepare the VM.", currentBackup.getDate(), + vmSnapshot.getCreated()); + + return; + } + + logger.debug("Preparing VM [{}] for VM snapshot reversion.", virtualMachine.getUuid()); + + List volumeObjectTOs = vmSnapshotHelper.getVolumeTOList(virtualMachine.getId()); + VMSnapshotVO vmSnapshotSucceedingCurrentBackup = getSucceedingVmSnapshot(currentBackup); + + List deltaMergeTreeTOList = new ArrayList<>(); + Commands commands = new Commands(Command.OnError.Stop); + List deletedDeltas = new ArrayList<>(); + + createDeleteCommandsAndMergeTrees(volumeObjectTOs, commands, deletedDeltas, vmSnapshotSucceedingCurrentBackup, deltaMergeTreeTOList); + + if (!deltaMergeTreeTOList.isEmpty()) { + commands.addCommand(new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOList, false, virtualMachine.getInstanceName())); + } + + Long hostId = vmSnapshotHelper.pickRunningHost(virtualMachine.getId()); + + Answer[] answers; + try { + answers = sendBackupCommands(hostId, commands); + } catch (AgentUnavailableException | OperationTimedoutException e) { + throw new CloudRuntimeException(e); + } + + if (answers == null || Arrays.stream(answers).anyMatch(answer -> !answer.getResult())) { + logger.error("Error while trying to prepare VM [{}] for VM snapshot reversion. Got [{}] as answers from host.", virtualMachine.getUuid(), + answers != null ? Arrays.stream(answers).filter(answer -> !answer.getResult()).map(Answer::getDetails) : null); + throw new CloudRuntimeException(String.format("Unable to prepare VM [%s] for VM snapshot reversion.", virtualMachine.getUuid())); + } + + List snapRefsSucceedingCurrentBackup = new ArrayList<>(); + if (vmSnapshotSucceedingCurrentBackup != null) { + snapRefsSucceedingCurrentBackup = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotSucceedingCurrentBackup.getId()); + } + + updateReferencesAfterPrepareForSnapshotRevert(deltaMergeTreeTOList, snapRefsSucceedingCurrentBackup, deletedDeltas, currentBackup); + } + + /** + * Get the secondary storage URLs of the backups that are backing files of this VM. This is only useful for Validation VMs currently, which are created with backing files on + * the secondary storage. + * */ + @Override + public Set getSecondaryStorageUrls(UserVm userVm) { + VMInstanceDetailVO detailVO = vmInstanceDetailsDao.findDetail(userVm.getId(), ApiConstants.BACKUP_ID); + if (detailVO == null) { + return Set.of(); + } + BackupVO backupVO = backupDao.findByUuid(detailVO.getValue()); + Set secondaryStorageUrls = getParentSecondaryStorageUrls(backupVO); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + secondaryStorageUrls.add(imageStoreDao.findById(internalBackupJoinVO.getImageStoreId()).getUrl()); + return secondaryStorageUrls; + } + + @Override + public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) { + return false; + } + + @Override + public List listRestorePoints(VirtualMachine vm) { + return null; + } + + @Override + public String getConfigComponentName() { + return BackupService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {backupChainSize, backupTimeout, backupCompressionTimeout, backupCompressionMinimumFreeStorage, backupCompressionRateLimit, + backupCompressionCoroutines}; + } + + protected Outcome createBackupThroughJobQueue(VirtualMachine vm, boolean quiesceVm, boolean isolated) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + BackupVO backup = new BackupVO(String.format("%s-%s", vm.getHostName(), DateUtil.getDateInSystemTimeZone()), vmId, vm.getBackupOfferingId(), accountId, + vm.getDomainId(), vm.getDataCenterId(), 0, Backup.Status.Queued, null, + Backup.CompressionStatus.Uncompressed, Backup.ValidationStatus.NotValidated); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkTakeBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkTakeBackup workInfo = new VmWorkTakeBackup(userId, accountId, vmId, backupDao.persist(backup).getId(), VM_WORK_JOB_HANDLER, quiesceVm, isolated); + + workJob.setDispatcher(VmWorkConstants.VM_WORK_JOB_DISPATCHER); + workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); + + jobManager.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vmId); + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new OutcomeImpl<>(Pair.class, workJob, VirtualMachineManagerImpl.VmJobCheckInterval.value(), new Predicate() { + @Override + public boolean checkCondition() { + AsyncJobVO jobVo = entityManager.findById(AsyncJobVO.class, workJob.getId()); + return jobVo == null || jobVo.getStatus() != JobInfo.Status.IN_PROGRESS; + } + }, AsyncJob.Topics.JOB_STATE); + } + + protected Outcome deleteBackupThroughJobQueue(Backup backup, boolean forced) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + VirtualMachine userVm = userVmDao.findByIdIncludingRemoved(backup.getVmId()); + long vmId = userVm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkDeleteBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkDeleteBackup workInfo = new VmWorkDeleteBackup(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), forced); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected Outcome restoreVMFromBackupThroughJobQueue(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkRestoreBackup.class.getName(), vmId, VirtualMachine.Type.Instance, + VmWorkJobVO.Step.Starting); + VmWorkRestoreBackup workInfo = new VmWorkRestoreBackup(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), quickRestore, hostId); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected Outcome restoreBackedUpVolumeThroughJobQueue(VirtualMachine vm, Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, boolean quickRestore) { + final CallContext context = CallContext.current(); + long userId = context.getCallingUser().getId(); + long accountId = context.getCallingAccount().getAccountId(); + long vmId = vm.getId(); + + VmWorkJobVO workJob = new VmWorkJobVO(AsyncJobExecutionContext.getOriginJobId(), userId, accountId, VmWorkRestoreVolumeBackupAndAttach.class.getName(), vmId, + VirtualMachine.Type.Instance, VmWorkJobVO.Step.Starting); + VmWorkRestoreVolumeBackupAndAttach workInfo = new VmWorkRestoreVolumeBackupAndAttach(userId, accountId, vmId, VM_WORK_JOB_HANDLER, backup.getId(), + backupVolumeInfo, hostIp, quickRestore); + + return submitWorkJob(workJob, workInfo, vmId); + } + + protected OutcomeImpl submitWorkJob(VmWorkJobVO workJob, VmWork workInfo, long vmId) { + workJob.setDispatcher(VmWorkConstants.VM_WORK_JOB_DISPATCHER); + workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo)); + + jobManager.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vmId); + AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId()); + + return new OutcomeImpl<>(Boolean.class, workJob, VirtualMachineManagerImpl.VmJobCheckInterval.value(), new Predicate() { + @Override + public boolean checkCondition() { + AsyncJobVO jobVo = entityManager.findById(AsyncJobVO.class, workJob.getId()); + return jobVo == null || jobVo.getStatus() != JobInfo.Status.IN_PROGRESS; + } + }, AsyncJob.Topics.JOB_STATE); + } + + protected void validateBackupAsyncIfHasOfferingSupport(InternalBackupJoinVO backupJoinVO, long zoneId, long accountId) { + if (!offeringSupportsValidation(backupJoinVO)) { + return; + } + + logger.info("Queuing backup validation job for backup [{}].", backupJoinVO.getUuid()); + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupJoinVO.getId(), zoneId, backupJoinVO.getVmId(), accountId, InternalBackupServiceJobType.BackupValidation)); + } + + protected void compressBackupAsync(InternalBackupJoinVO backupJoinVO, long zoneId, long accountId) { + logger.info("Queuing backup compression job for backup [{}].", backupJoinVO.getUuid()); + internalBackupServiceJobDao.persist(new InternalBackupServiceJobVO(backupJoinVO.getId(), zoneId, backupJoinVO.getVmId(), accountId, InternalBackupServiceJobType.StartCompression)); + } + + protected boolean finalizeQuickRestore(VirtualMachine vm, List volumesToConsolidate, long hostId) { + logger.info("Finalizing quick restore for VM [{}].", vm.getUuid()); + + UserVmVO userVmVO = userVmDao.findById(vm.getId()); + if (userVmVO.getState() == VirtualMachine.State.Stopped) { + try { + logger.info("Starting VM [{}] as part of the quick restore process.", vm.getName()); + userVmManager.startVirtualMachine(userVmVO.getId(), hostId, new HashMap<>(), null, true); + } catch (Exception e) { + logger.error("Caught [{}] while trying to quick restore VM [{}]. Throwing BackupException.", e, vm); + throw new BackupException(String.format("Exception while trying to start VM [%s] as part of the quick restore process.", userVmVO.getUuid()), e, false); + } + } + + return consolidateVolumes(vm, hostId, volumesToConsolidate); + } + + protected boolean validateWithHash(long backupId, BackupVO backupVO, BackupDetailVO hashDetail) { + List backupDeltaTOList = getBackupDeltaTOList(backupId); + TakeBackupHashCommand hashCommand = new TakeBackupHashCommand(backupDeltaTOList, backupVO.getUuid()); + List hosts = hostDao.listAllHostsUpByZoneAndHypervisor(backupVO.getZoneId(), Hypervisor.HypervisorType.KVM); + String message; + if (CollectionUtils.isEmpty(hosts)) { + message = String.format("No Up and Enabled host found in zone [%s]. Cannot validate backup [%s]. Will try again later.", backupVO.getZoneId(), backupVO.getUuid()); + logger.error(message); + setBackupUnableToValidateAndSendAlert(backupVO, message); + return false; + } + Collections.shuffle(hosts); + Answer answer = sendBackupCommand(hosts.get(0).getId(), hashCommand); + if (!answer.getResult()) { + message = String.format("Unable to get hash of backup [%s] due to [%s]. Will try again later.", backupVO.getUuid(), answer.getDetails()); + logger.warn(message); + setBackupUnableToValidateAndSendAlert(backupVO, message); + return false; + } + + if (!hashDetail.getValue().equals(answer.getDetails())) { + message = String.format("Current xxHash128 of backup [%s] is different from previous validated hash. This backup has changed and might be corrupt." + + "The old hash is [%s]; the new hash is [%s].", backupVO.getUuid(), hashDetail.getValue(), answer.getDetails()); + logger.error(message); + setBackupAsInvalidAndSendAlert(backupVO, message); + return false; + } + + logger.info("xxHash128 of backup [{}] is the same as when it was validated. This backup is still valid.", backupVO.getUuid()); + backupVO.setValidationStatus(Backup.ValidationStatus.Valid); + backupDao.update(backupId, backupVO); + return true; + } + + protected boolean validateWithValidationVm(long backupId, long hostId, BackupVO backupVO) { + boolean startedVm = false; + UserVmVO validationVm = null; + List volumeVOs = List.of(); + try { + validationVm = allocateValidationVm(backupId, backupVO); + if (validationVm == null) { + return false; + } + + HostVO hostVo = hostDao.findById(hostId); + List volumeToList = new ArrayList<>(); + volumeVOs = volumeDao.findByInstance(validationVm.getId()); + createValidationVolumesOnPrimaryStorage(volumeVOs, validationVm, backupVO, hostVo, volumeToList); + + List backupDeltas = internalBackupDataStoreDao.listByBackupId(backupId); + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + Set> backupDeltaAndVolumePairs = generateBackupAndVolumePairsToRestore(backupDeltas, volumeToList, backupJoinVO, false); + if (!prepareForValidation(hostId, backupDeltaAndVolumePairs, backupVO, validationVm)) { + return false; + } + + userVmManager.startVirtualMachine(validationVm, null); + startedVm = true; + //refresh info + validationVm = userVmDao.findById(validationVm.getId()); + hostVo = hostDao.findById(validationVm.getHostId()); + + HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(validationVm.getHypervisorType()); + VirtualMachineProfileImpl profile = new VirtualMachineProfileImpl(validationVm); + VirtualMachineTO vmTO = hvGuru.implement(profile); + + if (!validateBackup(backupId, vmTO, backupDeltaAndVolumePairs, backupVO, validationVm, hostVo)) { + endBackupChainIfConfigured(backupVO); + return false; + } + calculateAndSaveHash(backupDeltaAndVolumePairs, backupVO, hostVo.getId()); + return true; + } catch (Exception ex) { + logger.error("Encountered an exception during the validation process of backup [{}]. Will cleanup now.", backupVO.getUuid(), ex); + setBackupUnableToValidateAndSendAlert(backupVO, "Failed to validate due to unexpected exception: " + ex.getMessage()); + return false; + } finally { + cleanupValidation(startedVm, validationVm, backupVO, volumeVOs); + } + } + + /** + * If backupValidationEndChainOnFail is true for the account, and the backup being validated is part of the current chain, we end the current chain. + * */ + protected void endBackupChainIfConfigured(BackupVO backupVO) { + if (!getValidationEndChainOnFail(backupVO)) { + return; + } + VirtualMachine vm = userVmDao.findByIdIncludingRemoved(backupVO.getVmId()); + validateVmState(vm, "end backup chain", VirtualMachine.State.Expunging, VirtualMachine.State.Destroyed); + List backupChildren = getBackupJoinChildren(backupVO); + + // Get updated record + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + if (backupJoinVO.getCurrent() || (!backupChildren.isEmpty() && backupChildren.get(backupChildren.size() - 1).getCurrent())) { + logger.info("As [{}] is true, we are ending the backup chain for VM [{}]. The next backup will be a full backup.", + BackupValidationServiceJobController.backupValidationEndChainOnFail.toString()); + endBackupChain(vm); + } + } + + /** + * This method was created to facilitate testing + * */ + protected Boolean getValidationEndChainOnFail(BackupVO backupVO) { + return BackupValidationServiceJobController.backupValidationEndChainOnFail.valueIn(backupVO.getAccountId()); + } + + protected boolean normalizeBackupErrorAndFinishChain(UserVmVO userVmVO) { + VMInstanceDetailVO detail = vmInstanceDetailsDao.findDetail(userVmVO.getId(), VmDetailConstants.LAST_KNOWN_STATE); + boolean runningVM = detail == null || VirtualMachine.State.valueOf(detail.getValue()) == VirtualMachine.State.Running; + + BackupVO backupVO = backupDao.findLatestByStatusAndVmId(Backup.Status.Error, userVmVO.getId()); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + ImageStoreVO imageStoreVO = imageStoreDao.findById(internalBackupJoinVO.getImageStoreId()); + + List kbossTOS = new ArrayList<>(); + List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(internalBackupJoinVO.getId()); + InternalBackupJoinVO parent = internalBackupJoinDao.findById(internalBackupJoinVO.getParentId()); + + // There is a possibility that the cleanup step of the backup creation was executed, and thus we would have to merge with the old parent's parent + List parentDeltasOnPrimary = new ArrayList<>(); + if (parent != null) { + parentDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parent.getId()); + } + + List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(internalBackupJoinVO.getId()); + configureKbossTosForCleanup(userVmVO, deltasOnPrimary, deltasOnSecondary, runningVM, parentDeltasOnPrimary, kbossTOS); + CleanupKbossBackupErrorCommand command = new CleanupKbossBackupErrorCommand(runningVM, userVmVO.getInstanceName(), imageStoreVO.getUrl(), kbossTOS); + + long hostId = userVmVO.getHostId() != null ? userVmVO.getHostId() : vmSnapshotHelper.pickRunningHost(userVmVO.getId()); + Answer answer = sendBackupCommand(hostId, command); + if (answer == null || !answer.getResult()) { + logger.error("Unable to finish backup chain for VM [{}]. The host [{}] logs will have more information on why this happened.", userVmVO.getUuid(), hostId); + return false; + } + + boolean chainAlreadyEnded = processCleanupBackupErrorAnswer(userVmVO, answer); + + if (!chainAlreadyEnded) { + return endBackupChain(userVmVO); + } + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(userVmVO.getId()); + internalBackupStoragePoolDao.expungeByBackupId(current.getId()); + setEndOfChainAndRemoveCurrentForBackup(current); + + return true; + } + + protected boolean processCleanupBackupErrorAnswer(UserVmVO userVmVO, Answer answer) { + boolean runningVM; + CleanupKbossBackupErrorAnswer cleanAnswer = (CleanupKbossBackupErrorAnswer) answer; + logger.info("Successfully finished chain for VM [{}] and normalizing the BackupError state. Cleaning up metadata.", userVmVO.getUuid()); + + boolean chainAlreadyEnded = false; + for (VolumeObjectTO volumeObjectTO : cleanAnswer.getVolumeObjectTos()) { + VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); + if (!volumeObjectTO.getPath().equals(volumeVO.getPath())) { + volumeVO.setPath(volumeObjectTO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + chainAlreadyEnded = true; + } + } + + runningVM = cleanAnswer.isVmRunning(); + userVmVO.setState(runningVM ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); + userVmDao.update(userVmVO.getId(), userVmVO); + vmInstanceDetailsDao.removeDetail(userVmVO.getId(), VmDetailConstants.LAST_KNOWN_STATE); + return chainAlreadyEnded; + } + + protected void calculateAndSaveHash(Set> backupDeltaAndVolumePairs, BackupVO backupVO, long hostId) { + TakeBackupHashCommand cmd = new TakeBackupHashCommand(backupDeltaAndVolumePairs.stream().map(Pair::first).collect(Collectors.toList()), backupVO.getUuid()); + Answer answer = sendBackupCommand(hostId, cmd); + + if (answer.getResult() && answer.getDetails() != null) { + logger.debug("Got xxHash128 [{}] of backup [{}].", answer.getDetails(), backupVO.getUuid()); + backupDetailDao.addDetail(backupVO.getId(), BackupDetailsDao.BACKUP_HASH, answer.getDetails(), false); + return; + } + logger.warn("Unable to get hash of backup [{}] due to [{}].", backupVO.getUuid(), answer.getDetails()); + } + + + /** + * Return a list of BackupDeltaTO of the given backup. + * */ + protected List getBackupDeltaTOList(long backupId) { + InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); + DataStoreTO imageStoreTo = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image).getTO(); + List deltas = internalBackupDataStoreDao.listByBackupId(backupId); + + return deltas.stream() + .map(delta -> new BackupDeltaTO(imageStoreTo, Hypervisor.HypervisorType.KVM, delta.getBackupPath())) + .collect(Collectors.toList()); + } + + protected void cleanupValidation(boolean startedVm, UserVmVO validationVm, BackupVO backupVO, List volumeVOs) { + if (validationVm == null) { + return; + } + if (startedVm) { + userVmManager.stopVirtualMachine(validationVm.getId(), true); + } + DestroyVMCmd destroyVMCmd = new DestroyVMCmd(validationVm.getId(), true); + StringBuilder errorMessage = new StringBuilder("Cleanup failed due to:"); + boolean sendMail = false; + try { + userVmManager.destroyVm(destroyVMCmd, false); + } catch (Exception e) { + errorMessage.append("\nGot an unexpected exception while trying to destroy validation VM."); + sendMail = true; + logger.error("Got an error while trying to cleanup validation of backup [{}].", backupVO.getUuid(), e); + } + for (VolumeVO volume : volumeVOs) { + if (volume.getVolumeType() == Volume.Type.ROOT) { + continue; + } + Volume vol = volumeApiService.destroyVolume(volume.getId(), CallContext.current().getCallingAccount(), true, true, null); + if (vol == null) { + sendMail = true; + errorMessage.append(String.format("\nWe were unable to destroy volume [%s].", volume.getUuid())); + } + } + + if (startedVm) { + CleanupKbossValidationCommand cleanupKbossValidationCommand = new CleanupKbossValidationCommand(validationVm.getName(), getSecondaryStorageUrls(validationVm)); + Answer answer = agentManager.easySend(validationVm.getHostId(), cleanupKbossValidationCommand); + if (answer == null || !answer.getResult()) { + logger.error("Failed to cleanup post validation of backup [{}]. Got answer [{}]", backupVO.getUuid(), answer == null ? null : answer.getDetails()); + HostVO host = hostDao.findById(validationVm.getHostId()); + sendMail = true; + errorMessage.append(String.format("\nFailed to cleanup secondary storage mount at host [%s].", host != null ? host.getUuid() : "null")); + } + } + + if (sendMail) { + sendCleanupFailedEmail(backupVO, errorMessage.toString()); + } + } + + protected boolean validateBackup(long backupId, VirtualMachineTO vmTO, Set> backupDeltaAndVolumePairs, BackupVO backupVO, UserVmVO validationVm, + HostVO hostVo) { + Answer answer; + ValidateKbossVmCommand validateKbossVmCommand = new ValidateKbossVmCommand(vmTO, backupDeltaAndVolumePairs.stream().findFirst().get().first()); + configureValidationSteps(validateKbossVmCommand, backupVO); + answer = agentManager.easySend(validationVm.getHostId(), validateKbossVmCommand); + + boolean result = processValidationAnswer(answer, backupVO, validationVm, hostVo, validateKbossVmCommand); + if (result) { + backupVO.setValidationStatus(Backup.ValidationStatus.Valid); + backupDao.update(backupId, backupVO); + } + return result; + } + + protected boolean prepareForValidation(long hostId, Set> backupDeltaAndVolumePairs, BackupVO backupVO, UserVmVO validationVm) { + PrepareValidationCommand prepareCommand = new PrepareValidationCommand(new ArrayList<>(backupDeltaAndVolumePairs), getParentSecondaryStorageUrls(backupVO)); + + Answer answer = agentManager.easySend(hostId, prepareCommand); + + if (answer == null || !answer.getResult()) { + String msg = String.format("Failed to prepare dummy VM [%s] for validation of %s. %s", validationVm.getName(), backupVO.getUuid(), answer != null ? + "Details: "+ answer.getDetails(): ""); + logger.error(msg); + setBackupUnableToValidateAndSendAlert(backupVO, msg); + return false; + } + return true; + } + + protected void createValidationVolumesOnPrimaryStorage(List volumeVOs, UserVmVO validationVm, BackupVO backupVO, HostVO hostVo, List volumeToList) throws NoTransitionException { + for (VolumeVO volume : volumeVOs) { + logger.debug("Creating validation volume [{}] for validation VM [{}].", volume.getUuid(), validationVm.getUuid()); + VolumeInfo volumeInfo = volumeDataFactory.getVolume(volume.getId()); + volumeInfo = volumeOrchestrationService.createVolumeOnPrimaryStorage(validationVm, volumeInfo, Hypervisor.HypervisorType.KVM, null, hostVo.getClusterId(), + hostVo.getPodId()); + validateCorrectStorageType(backupVO, volume, volumeInfo); + volumeToList.add((VolumeObjectTO)volumeInfo.getTO()); + } + } + + protected UserVmVO allocateValidationVm(long backupId, BackupVO backupVO) { + UserVmVO validationVm; + try { + validationVm = (UserVmVO) userVmManager.allocateVMForValidation(backupId, Hypervisor.HypervisorType.KVM); + NicVO nic = nicDao.findDefaultNicForVM(validationVm.getId()); + virtualMachineManager.updateVmNic(validationVm, nic, false); + validationVm.setDataCenterId(backupVO.getZoneId()); + } catch (InsufficientCapacityException | ResourceAllocationException | ResourceUnavailableException e) { + String msg = String.format("Unable to allocate dummy VM to validate %s due to %s.", backupVO.getUuid(), e.getMessage()); + logger.error(msg, e); + setBackupUnableToValidateAndSendAlert(backupVO, msg); + return null; + } + return validationVm; + } + + protected List getVolumesToConsolidate(VirtualMachine vm, List deltasOnSecondary, List volumeObjectTOS, long hostId, + boolean sameVmAsBackup) { + List volumesToConsolidate = new ArrayList<>(); + + transitVmState(vm, VirtualMachine.Event.RestoringSuccess, hostId); + for (VolumeObjectTO volume : volumeObjectTOS) { + VolumeInfo volumeInfo = volumeDataFactory.getVolume(volume.getVolumeId()); + transitVolumeState(volumeInfo.getVolume(), Volume.Event.RestoreSucceeded); + + if (!sameVmAsBackup || deltasOnSecondary.stream().anyMatch(delta -> delta.getVolumeId() == volume.getVolumeId())) { + volumesToConsolidate.add(volumeInfo); + } + } + return volumesToConsolidate; + } + + protected boolean consolidateVolumes(VirtualMachine vm, long hostId, List volumesToConsolidate) { + for (VolumeInfo volumeInfo : volumesToConsolidate) { + transitVolumeState(volumeInfo.getVolume(), Volume.Event.ConsolidationRequested); + } + + VMInstanceDetailVO uuids = vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS); + List secondaryStorageUuids = uuids != null ? List.of(uuids.getValue().split(",")) : List.of(); + ConsolidateVolumesCommand cmd = new ConsolidateVolumesCommand(volumesToConsolidate, secondaryStorageUuids, vm.getInstanceName()); + Answer answer = sendBackupCommand(hostId, cmd); + + String logError = String.format("Failed to consolidate volumes [%s] of VM [%s]. Answer details: [%s].", + volumesToConsolidate, vm.getName(), answer != null ? answer.getDetails() : "null"); + if (!(answer instanceof ConsolidateVolumesAnswer)) { + logger.error(logError); + throw new BackupException(logError, false); + } + ConsolidateVolumesAnswer cAnswer = (ConsolidateVolumesAnswer)answer; + processConsolidateAnswer(cAnswer, volumesToConsolidate, vm); + + logger.info("Volume consolidation answer: [{}].", cAnswer.getResult()); + return cAnswer.getResult(); + } + + /** + * Validates the Backup status:
    + * - If it is Error and The VM is in BackupError, will throw an exception;
    + * - If it is in Error but the VM is not in BackupError, will set the backup as Failed so that it may be removed with {@code deleteFailedBackup(BackupVO backupVO)};
    + * - If it is not in Error, does nothing. + * */ + protected void checkErrorBackup(BackupVO backupVO, VirtualMachine virtualMachine) { + if (backupVO.getStatus() != Backup.Status.Error) { + return; + } + if (virtualMachine != null && virtualMachine.getState() == VirtualMachine.State.BackupError) { + logger.error("Unable to delete backup [{}] as it is in Error state and the associated VM [{}] is in BackupError state. You must read the backup creation logs," + + " normalize the VM's volumes in the hypervisor/storage and update the VM state in the database before trying to delete the backup. Try again when the VM is not " + + "in this state.", backupVO, virtualMachine.getUuid()); + throw new InvalidParameterValueException(String.format("Unable to delete backup [%s]. Please check the logs.", backupVO.getUuid())); + } + logger.debug("Assuming VM and storage are normalized and setting backup [{}] as failed so its metadata is deleted."); + backupVO.setStatus(Backup.Status.Failed); + } + + /** + * Deletes a Failed backup metadata and sets the backup as Expunged. + * */ + protected boolean deleteFailedBackup(BackupVO backupVO) { + if (backupVO.getStatus() != Backup.Status.Failed) { + return false; + } + long backupId = backupVO.getId(); + + backupVO.setStatus(Backup.Status.Expunged); + backupDao.update(backupId, backupVO); + internalBackupStoragePoolDao.expungeByBackupId(backupId); + internalBackupDataStoreDao.expungeByBackupId(backupId); + backupDetailDao.removeDetails(backupId); + return true; + } + + /** + * Merges the current delta on primary storage, if any, into the given volume. If the backup has no more deltas on primary storage, will set the backup as end_of_chain. + * */ + protected void mergeCurrentDeltaIntoVolume(Volume volume, VirtualMachine virtualMachine, String operation, boolean isVmRunning) { + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeId(volume.getId()); + if (delta == null) { + logger.debug("Volume [{}] has no deltas to merge, doing nothing.", volume.getUuid()); + return; + } + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(delta.getBackupId()); + VMSnapshotVO succeedingVmSnapshotVO = getSucceedingVmSnapshot(internalBackupJoinVO); + + DataStore store = dataStoreManager.getDataStore(volume.getPoolId(), DataStoreRole.Primary); + VolumeObject volumeObject = VolumeObject.getVolumeObject(store, (VolumeVO)volume); + + DeltaMergeTreeTO deltaMergeTreeTO = createDeltaMergeTree(succeedingVmSnapshotVO == null, isVmRunning, delta, (VolumeObjectTO)volumeObject.getTO(), succeedingVmSnapshotVO); + MergeDiskOnlyVmSnapshotCommand cmd = new MergeDiskOnlyVmSnapshotCommand(List.of(deltaMergeTreeTO), isVmRunning, virtualMachine.getInstanceName()); + + Answer answer = sendBackupCommand(vmSnapshotHelper.pickRunningHost(virtualMachine.getId()), cmd); + + if (answer == null || !answer.getResult()) { + logger.error("Error while trying to prepare volume [{}] for {}. Got [{}] as answer from host.", volume.getUuid(), operation, answer != null ? answer.getDetails() : null); + throw new CloudRuntimeException(String.format("Unable to prepare volume [%s] for [%s].", volume.getUuid(), operation)); + } + + if (succeedingVmSnapshotVO == null) { + VolumeVO volumeVO = volumeDao.findById(volumeObject.getId()); + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + } + + expungeOldDeltasAndUpdateVmSnapshotIfNeeded(List.of(delta), succeedingVmSnapshotVO); + + List backupDeltas = internalBackupStoragePoolDao.listByBackupId(delta.getBackupId()); + if (backupDeltas.isEmpty()) { + logger.debug("Backup [{}] has no more deltas on primary storage due to prepare volume [{}] for {} operation. Will set it as end of chain and not current.", + internalBackupJoinVO.getUuid(), volume.getUuid(), operation); + setEndOfChainAndRemoveCurrentForBackup(internalBackupJoinVO); + } + } + + /** + * Creates the necessary delta references on both primary and secondary storage. Also maps the volume to the parent delta backup and create the delta merge tree. + * */ + protected void createDeltaReferences(boolean fullBackup, boolean hasVmSnapshotSucceedingLastBackup, boolean runningVm, Backup backup, + List parentBackupDeltasOnSecondary, List parentBackupDeltasOnPrimary, + HashMap volumeUuidToDeltaPrimaryRef, HashMap volumeUuidToDeltaSecondaryRef, + VMSnapshotVO succeedingVmSnapshot, KbossTO kbossTO) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + logger.debug("Creating delta references for backup [{}] of volume [{}].", backup.getUuid(), volumeObjectTO.getUuid()); + + String filename = UUID.randomUUID().toString(); + String relativePathOnSecondary = String.format("%s%s%s%s%s%s%s", "backups", File.separator, volumeObjectTO.getAccountId(), File.separator, volumeObjectTO.getId(), + File.separator, filename); + kbossTO.setDeltaPathOnPrimary(filename); + kbossTO.setDeltaPathOnSecondary(relativePathOnSecondary); + + InternalBackupDataStoreVO deltaSecondaryRef = new InternalBackupDataStoreVO(backup.getId(), volumeObjectTO.getVolumeId(), volumeObjectTO.getDeviceId(), relativePathOnSecondary); + if (!fullBackup) { + InternalBackupStoragePoolVO parentDeltaOnPrimary = createDeltaMergeTreeForVolume(false, runningVm, parentBackupDeltasOnPrimary, succeedingVmSnapshot, kbossTO); + findAndSetParentBackupPath(parentBackupDeltasOnSecondary, parentDeltaOnPrimary, kbossTO); + } + + InternalBackupDataStoreVO referenceOnSecondary = internalBackupDataStoreDao.persist(deltaSecondaryRef); + logger.trace("Created reference [{}] for backup [{}] of volume [{}].", referenceOnSecondary, backup, volumeObjectTO); + volumeUuidToDeltaSecondaryRef.put(volumeObjectTO.getUuid(), referenceOnSecondary); + + InternalBackupStoragePoolVO deltaPrimaryRef = new InternalBackupStoragePoolVO(backup.getId(), volumeObjectTO.getPoolId(), volumeObjectTO.getVolumeId(), filename, + volumeObjectTO.getPath()); + + if (kbossTO.getDeltaMergeTreeTO() != null && !hasVmSnapshotSucceedingLastBackup) { + deltaPrimaryRef.setBackupDeltaParentPath(kbossTO.getDeltaMergeTreeTO().getParent().getPath()); + } else if (hasVmSnapshotSucceedingLastBackup) { + deltaPrimaryRef.setBackupDeltaParentPath(volumeObjectTO.getPath()); + } + + InternalBackupStoragePoolVO referenceOnPrimary = internalBackupStoragePoolDao.persist(deltaPrimaryRef); + logger.trace("Created reference [{}] for backup [{}] of volume [{}].", referenceOnPrimary, backup, volumeObjectTO); + volumeUuidToDeltaPrimaryRef.put(volumeObjectTO.getUuid(), referenceOnPrimary); + } + + protected HostVO getHostToRestore(VirtualMachine vm, boolean quickRestore, Long hostId) throws AgentUnavailableException { + HostVO host; + if (quickRestore) { + if (hostId == null) { + hostId = vm.getLastHostId(); + } + if (hostId == null) { + logger.error("Cannot quick restore if the VM has no last host and no hostId was informed. You may try to start it in an available host and stop it before quick" + + " restoring. Otherwise, use the normal restore."); + throw new AgentUnavailableException(String.format("No host found to quick restore VM [%s]. Please check the logs.", vm.getUuid()), -1); + } + host = hostDao.findByIdIncludingRemoved(hostId); + if (host.getStatus() != Status.Up || host.isInMaintenanceStates() || host.getResourceState() != ResourceState.Enabled) { + logger.error("Cannot quick restore if the VM's last host is in maintenance, not Up, or disabled. You may try to start it in an available host and stop it before quick" + + " restoring. Otherwise, use the normal restore."); + throw new AgentUnavailableException(String.format("No host found to quick restore VM [%s]. Please check the logs.", vm.getUuid()), -1); + } + } else { + hostId = vmSnapshotHelper.pickRunningHost(vm.getId()); + host = hostDao.findByIdIncludingRemoved(hostId); + } + return host; + } + + /** + * Returns ordered list of disk-only VM snapshots taken after the last backup. The list is ordered from oldest to newest. + * */ + protected List getSucceedingVmSnapshotList(InternalBackupJoinVO backup) { + List vmSnapshotVOs = new ArrayList<>(); + if (backup == null) { + return vmSnapshotVOs; + } + + VMSnapshotVO currentSnapshotVO = vmSnapshotDao.findCurrentSnapshotByVmId(backup.getVmId()); + if (currentSnapshotVO == null || currentSnapshotVO.getCreated().before(backup.getDate())) { + return vmSnapshotVOs; + } + vmSnapshotVOs.add(0, currentSnapshotVO); + + while (currentSnapshotVO.getParent() != null && currentSnapshotVO.getParent() != 0) { + VMSnapshotVO parentSnap = vmSnapshotDao.findById(currentSnapshotVO.getParent()); + if (parentSnap.getCreated().before(backup.getDate())){ + break; + } + currentSnapshotVO = parentSnap; + vmSnapshotVOs.add(0, currentSnapshotVO); + } + + logger.debug("Found the following VM snapshots that succeed the backup [{}]: [{}].", backup.getUuid(), vmSnapshotVOs); + + return vmSnapshotVOs; + } + + /** + * Returns the disk-only VM snapshot taken after the last backup, if any. + * */ + protected VMSnapshotVO getSucceedingVmSnapshot(InternalBackupJoinVO backup) { + List snaps = getSucceedingVmSnapshotList(backup); + if (snaps.isEmpty()) { + return null; + } + return snaps.get(0); + } + + /** + * Given a VM snapshot, returns a map of volume id to list of snapshot references of the children of the VM snapshot. + * */ + protected Map> gatherSnapshotReferencesOfChildrenSnapshot(List volumeObjectTOs, VMSnapshot vmSnapshotVO) { + Map> volumeToSnapshotRefs = new HashMap<>(); + if (vmSnapshotVO == null) { + return volumeToSnapshotRefs; + } + + List snapshotChildren = vmSnapshotDao.listByParent(vmSnapshotVO.getId()); + if (CollectionUtils.isEmpty(snapshotChildren)) { + return volumeToSnapshotRefs; + } + + List snapshotDataStoreVOS = new ArrayList<>(); + snapshotChildren.stream() + .map(snapshotVo -> vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(snapshotVo.getId())) + .forEach(snapshotDataStoreVOS::addAll); + mapVolumesToSnapshotReferences(volumeObjectTOs, snapshotDataStoreVOS, volumeToSnapshotRefs); + if (logger.isDebugEnabled()) { + StringBuilder log = new StringBuilder(String.format("Found the following snapshot references that succeed the VM snapshot [%s].", vmSnapshotVO.getUuid())); + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + log.append(String.format(" Volume [%s]; Snapshot references [%s].", volumeObjectTO.getUuid(), volumeToSnapshotRefs.get(volumeObjectTO.getId()))); + } + logger.debug(log.toString()); + } + + return volumeToSnapshotRefs; + } + + /** + * Given a list of volumes and VM snapshots, maps the volumes to the snapshot references of the VM snapshots. + * */ + protected Map> mapVolumesToVmSnapshotReferences(List volumeObjectTOs, List vmSnapshotVOList) { + Map> volumeToSnapshotRefs = new HashMap<>(); + if (vmSnapshotVOList.isEmpty()) { + logger.trace("No VM snapshot to map to any volume, returning."); + return volumeToSnapshotRefs; + } + + ArrayList allRefs = new ArrayList<>(); + for (VMSnapshotVO vmSnapshotVO : vmSnapshotVOList) { + allRefs.addAll(vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVO.getId())); + } + mapVolumesToSnapshotReferences(volumeObjectTOs, allRefs, volumeToSnapshotRefs); + logger.trace("Given volume objects [{}] and VM snapshots [{}], created the following map [{}].", volumeObjectTOs, vmSnapshotVOList, volumeToSnapshotRefs); + return volumeToSnapshotRefs; + } + + protected void mapVolumesToSnapshotReferences(List volumeObjectTOs, List snapshotDataStoreVOS, Map> volumeToSnapshotRefs) { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + List associatedSnapshots = snapshotDataStoreVOS.stream() + .filter(snapRef -> Objects.equals(snapRef.getVolumeId(), volumeObjectTO.getVolumeId())) + .collect(Collectors.toList()); + volumeToSnapshotRefs.put(volumeObjectTO.getId(), associatedSnapshots); + } + } + + /** + * Updates the necessary references on the database. Also calculates the backup's physical size. + * */ + protected long updateDeltaReferencesAndCalculateBackupPhysicalSize(VolumeObjectTO volumeObjectTO, HashMap volumeUuidToDeltaPrimaryRef, + HashMap volumeUuidToDeltaSecondaryRef, TakeKbossBackupAnswer answer, long physicalBackupSize, boolean endChain, boolean isolated, + BackupVO backupVO) { + String volumeUuid = volumeObjectTO.getUuid(); + InternalBackupStoragePoolVO deltaPrimaryRef = volumeUuidToDeltaPrimaryRef.get(volumeUuid); + if (endChain || isolated) { + logger.trace("Since backup [{}] is [{}]. We will delete the delta reference on primary at [{}] as it does not exist anymore.", backupVO.getUuid(), endChain ? + "end of chain" : "isolated", deltaPrimaryRef.getBackupDeltaPath()); + internalBackupStoragePoolDao.expunge(deltaPrimaryRef.getId()); + } + + InternalBackupDataStoreVO deltaSecondaryRef = volumeUuidToDeltaSecondaryRef.get(volumeUuid); + + String newVolumePath = answer.getMapVolumeUuidToNewVolumePath().get(volumeUuid); + + VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); + volumeVO.setPath(newVolumePath); + logger.trace("Updating volume [{}] path to [{}].", volumeVO.getUuid(), newVolumePath); + volumeDao.update(volumeVO.getId(), volumeVO); + + Pair deltaPathOnSecondaryAndSize = answer.getMapVolumeUuidToDeltaPathOnSecondaryAndSize().get(volumeUuid); + logger.trace("Updating delta reference on secondary [{}] path to [{}].", deltaSecondaryRef, deltaPathOnSecondaryAndSize.first()); + deltaSecondaryRef.setBackupPath(deltaPathOnSecondaryAndSize.first()); + internalBackupDataStoreDao.update(deltaSecondaryRef.getId(), deltaSecondaryRef); + + physicalBackupSize += deltaPathOnSecondaryAndSize.second(); + return physicalBackupSize; + } + + /** + * Expunge the old backup deltas and if there were disk-only VM snapshot deltas after the last backup, update their paths. + * */ + protected void expungeOldDeltasAndUpdateVmSnapshotIfNeeded(List oldDeltasOnPrimary, VMSnapshot vmSnapshot) { + List snapshotRefs = vmSnapshot == null ? List.of() : vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshot.getId()); + for (InternalBackupStoragePoolVO oldBackupDelta : oldDeltasOnPrimary) { + logger.trace("Expunging old backup delta [{}].", oldBackupDelta); + internalBackupStoragePoolDao.expunge(oldBackupDelta.getId()); + SnapshotDataStoreVO snapshotDataStoreVO = snapshotRefs.stream().filter(ref -> ref.getVolumeId() == oldBackupDelta.getVolumeId()).findFirst().orElse(null); + if (snapshotDataStoreVO == null) { + continue; + } + snapshotDataStoreVO.setInstallPath(oldBackupDelta.getBackupDeltaParentPath()); + logger.debug("Updating snapshot delta [{}] path to [{}].", snapshotDataStoreVO.getId(), oldBackupDelta.getBackupDeltaParentPath()); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); + } + } + + /** + * Create a {@link DeltaMergeTreeTO} for the volume if it has a delta on primary and add it to the list. + * + * @return the delta on primary of the volume. Null if no delta. + * */ + protected InternalBackupStoragePoolVO createDeltaMergeTreeForVolume(boolean childIsVolume, boolean runningVm, List deltasOnPrimary, VMSnapshotVO succeedingVmSnapshot, + KbossTO kbossTO) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + + InternalBackupStoragePoolVO deltaOnPrimary = deltasOnPrimary.stream() + .filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()) + .findFirst() + .orElse(null); + if (deltaOnPrimary == null) { + logger.debug("Volume [{}] has no delta on primary storage.", volumeObjectTO); + return null; + } + + logger.debug("Volume [{}] has a backup delta on primary storage [{}].", volumeObjectTO.getUuid(), deltaOnPrimary); + + kbossTO.setDeltaMergeTreeTO(createDeltaMergeTree(childIsVolume, runningVm, deltaOnPrimary, volumeObjectTO, succeedingVmSnapshot)); + return deltaOnPrimary; + } + + protected DeltaMergeTreeTO createDeltaMergeTree(boolean childIsVolume, boolean runningVm, InternalBackupStoragePoolVO deltaOnPrimary, + VolumeObjectTO volumeObjectTO, VMSnapshotVO succeedingVmSnapshot) { + DataStore store = dataStoreManager.getDataStore(deltaOnPrimary.getStoragePoolId(), DataStoreRole.Primary); + DataTO deltaChild; + if (childIsVolume) { + deltaChild = volumeObjectTO; + } else { + deltaChild = new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaPath()); + } + + BackupDeltaTO deltaParent = new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaParentPath()); + + List succeedingDeltaPaths = new ArrayList<>(); + if (succeedingVmSnapshot != null) { + succeedingDeltaPaths = gatherSnapshotReferencesOfChildrenSnapshot(List.of(volumeObjectTO), succeedingVmSnapshot).getOrDefault(volumeObjectTO.getVolumeId(), List.of()) + .stream().map(SnapshotDataStoreVO::getInstallPath).collect(Collectors.toList()); + + if (!childIsVolume && !runningVm && succeedingDeltaPaths.isEmpty()) { + succeedingDeltaPaths = List.of(volumeObjectTO.getPath()); + logger.debug("Since the last backup delta of volume [{}] is succeeded by a snapshot and the delta created by this snapshot is also the volume, it will have to be" + + " rebased. Setting it as the grand-child.", volumeObjectTO.getUuid()); + } + } + + List deltaGrandchildren = succeedingDeltaPaths.stream() + .map(deltaPath -> new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaPath)) + .collect(Collectors.toList()); + + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, deltaParent, deltaChild, deltaGrandchildren); + + logger.debug("Mapped the following delta merge tree for volume [{}]: [{}].", volumeObjectTO.getUuid(), deltaMergeTreeTO); + return deltaMergeTreeTO; + } + + /** + * Sets on the {@code kbossTO} the backupParentOnSecondary path based on the list of InternalBackupDataStoreVO. + * + * @param parentBackupDeltasOnSecondary + * List of deltas on secondary; + * @param parentDeltaOnPrimary + * @param kbossTO + * KbossTO to be configured; + */ + protected void findAndSetParentBackupPath(List parentBackupDeltasOnSecondary, InternalBackupStoragePoolVO parentDeltaOnPrimary, KbossTO kbossTO) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + if (parentDeltaOnPrimary == null) { + logger.debug("Volume [{}] has no parent on primary, thus its backup cannot be incremental.", volumeObjectTO); + return; + } + + InternalBackupDataStoreVO parentOnSecondary = parentBackupDeltasOnSecondary.stream() + .filter(backupDataStoreVo -> volumeObjectTO.getVolumeId() == backupDataStoreVo.getVolumeId()) + .findFirst() + .orElse(null); + + if (parentOnSecondary == null) { + return; + } + + logger.debug("Volume [{}] already has a backup [{}].", volumeObjectTO.getUuid(), parentOnSecondary.getBackupId()); + + kbossTO.setPathBackupParentOnSecondary(parentOnSecondary.getBackupPath()); + } + + /** + * Verify if the data center has heuristic rules for allocating backups; if there is then returns the {@link DataStore} returned by the JS script. + * Otherwise, returns a {@link DataStore} with free capacity. + */ + protected DataStore getImageStoreForBackup(Long dataCenterId, BackupVO backupVO) { + DataStore imageStore = heuristicRuleHelper.getImageStoreIfThereIsHeuristicRule(dataCenterId, HeuristicType.BACKUP, backupVO); + + if (imageStore == null) { + imageStore = dataStoreManager.getImageStoreWithFreeCapacity(dataCenterId); + } + + if (imageStore == null) { + backupVO.setStatus(Backup.Status.Failed); + backupDao.update(backupVO.getId(), backupVO); + throw new CloudRuntimeException(String.format("Unable to find secondary storage for backup [%s].", backupVO)); + } + + logger.debug("Backup [{}] will use secondary storage [{}].", backupVO.getUuid(), imageStore.getUuid()); + return imageStore; + } + + protected void setBackupAsIsolated(BackupVO backup) { + logger.debug("Setting backup [{}] as isolated.", backup.getUuid()); + backupDetailDao.persist(new BackupDetailVO(backup.getId(), ISOLATED, Boolean.TRUE.toString(), true)); + } + + /** + * Gets the parent for newBackup. Will set the newBackup as the end of chain if needed.
    + * - If no backups are found, returns null.
    + * - If the last backup was the end of the chain, returns null.
    + * + * @param newBackup the new backup being created. + * @param backupChain newBackup's ancestors. + * */ + protected InternalBackupJoinVO getParentAndSetEndOfChain(BackupVO newBackup, List backupChain, BackupOfferingVO offering) { + int chainSize = getChainSizeForBackup(offering, newBackup.getZoneId()); + if (CollectionUtils.isEmpty(backupChain)) { + setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(chainSize, chainSize, newBackup.getId(), newBackup.getUuid()); + return null; + } + + int remainingChainSize = chainSize - backupChain.size(); + setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(remainingChainSize, chainSize, newBackup.getId(), newBackup.getUuid()); + + InternalBackupJoinVO parent = backupChain.get(0); + return parent.getStatus().equals(Backup.Status.BackedUp) ? parent : null; + } + + /** + * For every restore point, maps a volume to it. + * @throws CloudRuntimeException If cannot map restore point to any volume. + * */ + protected Set> generateBackupAndVolumePairsToRestore(List backupDeltas, List volumeTOs, + InternalBackupJoinVO backupJoinVO, boolean sameVmAsBackup) { + Set> backupAndVolumePairs = new HashSet<>(); + DataStore dataStore = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image); + for (InternalBackupDataStoreVO backupDataStoreVO : backupDeltas) { + VolumeObjectTO volumeObjectTO = volumeTOs.stream() + .filter(volumeTO -> sameVmAsBackup ? volumeTO.getVolumeId() == backupDataStoreVO.getVolumeId() : volumeTO.getDeviceId() == backupDataStoreVO.getDeviceId()) + .findFirst() + .orElse(null); + + if (volumeObjectTO == null) { + logger.error("All backups should have a corresponding volume at this point, however, backup delta [{}] does not.", backupDataStoreVO.getId()); + throw new CloudRuntimeException("Error while restoring backup. Please check the logs."); + } + + backupAndVolumePairs.add(new Pair<>(new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, backupDataStoreVO.getBackupPath()), volumeObjectTO)); + } + logger.debug("Generated the following list of pairs of backup deltas and volumes: [{}].", backupAndVolumePairs); + return backupAndVolumePairs; + } + + protected Pair generateBackupAndVolumePairForSingleNewVolume(InternalBackupDataStoreVO backupDeltaVo, VolumeObjectTO volumeTO, + InternalBackupJoinVO backupJoinVO) { + DataStore dataStore = dataStoreManager.getDataStore(backupJoinVO.getImageStoreId(), DataStoreRole.Image); + Pair backupAndVolumePair = new Pair<>(new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, backupDeltaVo.getBackupPath()), volumeTO); + + logger.debug("Paired volume [{}] with backup delta [{}].", volumeTO, backupAndVolumePair.first()); + return backupAndVolumePair; + } + + /** + * For every volume, maps deltas that should be deleted, if there are any. If a volume has a delta but is not part of backup being restored, it will be mapped to be merged. + * + * @return List of deltas to be merged. + * */ + protected List populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List deltasOnPrimary, Set deltasToRemove, List volumeTOs, + List volumesNotPartOfTheBackupBeingRestored, String vmUuid) { + List deltasToBeMerged = new ArrayList<>(); + for (InternalBackupStoragePoolVO deltaOnPrimary : deltasOnPrimary) { + Optional optional = volumeTOs.stream().filter(volumeTO -> volumeTO.getVolumeId() == deltaOnPrimary.getVolumeId()).findFirst(); + if (optional.isEmpty()) { + logger.error("Failed to find volume that matches delta [{}] with path [{}]. Please check for inconsistencies on the database or if there are leftover" + + " deltas on storage.", deltaOnPrimary.getId(), deltaOnPrimary.getBackupDeltaPath()); + throw new CloudRuntimeException(String.format("Failed to restore VM [%s]. Please check the logs.", vmUuid)); + } + VolumeObjectTO volumeObjectTO = optional.get(); + + if (volumesNotPartOfTheBackupBeingRestored.contains(volumeObjectTO)) { + deltasToBeMerged.add(createDeltaMergeTree(true, false, deltaOnPrimary, volumeObjectTO, null)); + continue; + } + + DataStore dataStore = dataStoreManager.getDataStore(deltaOnPrimary.getStoragePoolId(), DataStoreRole.Primary); + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(dataStore.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaPath()); + logger.debug("Mapped the following backup delta on primary to be removed since the volume [{}] is not part of the backup being restored [{}].", + volumeObjectTO.getUuid(), backupDeltaTO); + deltasToRemove.add(backupDeltaTO); + volumeObjectTO.setPath(deltaOnPrimary.getBackupDeltaParentPath()); + } + if (!deltasToBeMerged.isEmpty()) { + logger.debug("The following deltaMergeTrees [{}] were created to merge volumes [{}] that have no backups.", deltasToBeMerged, volumesNotPartOfTheBackupBeingRestored); + } + return deltasToBeMerged; + } + + protected void updateVolumePathsAndSizeIfNeeded(VirtualMachine vm, List volumeTOs, List volumeInfos, + List deltaMergeTreeTOList, boolean sameVmAsBackup) { + List volumeVOs = volumeDao.findByInstance(vm.getId()); + + for (VolumeVO volumeVO : volumeVOs) { + VolumeObjectTO volumeTO = volumeTOs.stream().filter(volumeObjectTO -> volumeObjectTO.getVolumeId() == volumeVO.getId()).findFirst().get(); + + String log = "Volume [%s] path was updated as part of the backup restore process. New path: [%s]."; + DeltaMergeTreeTO deltaMergeTreeTO = deltaMergeTreeTOList.stream().filter(delta -> delta.getChild().getId() == volumeTO.getId()).findFirst().orElse(null); + if (!volumeVO.getPath().equals(volumeTO.getPath())) { + volumeVO.setPath(volumeTO.getPath()); + logger.debug(() -> String.format(log, volumeVO.getUuid(), volumeVO.getPath())); + } else if (deltaMergeTreeTO != null) { + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug(() -> String.format(log, volumeVO.getUuid(), volumeVO.getPath())); + } + + Backup.VolumeInfo volumeInfo = volumeInfos.stream() + .filter(info -> sameVmAsBackup ? volumeVO.getUuid().equals(info.getUuid()) : volumeVO.getDeviceId().equals(info.getDeviceId())) + .findFirst().orElse(null); + if (volumeInfo != null && !Objects.equals(volumeInfo.getSize(), volumeVO.getSize())) { + logger.debug("Volume [{}] size was restored as part of the backup restore process. Old size is [{}] new size is [{}].", volumeVO.getUuid(), + volumeVO.getSize(), volumeInfo.getSize()); + volumeVO.setSize(volumeInfo.getSize()); + } + + volumeDao.update(volumeVO.getId(), volumeVO); + } + } + + protected void createAndAttachVolumes(List volumeInfos, List backupDeltas, VirtualMachine vm, HostVO host) { + logger.info("Found the following backup deltas that have no volume correspondence [{}]. Will create new volumes and attach them to VM [{}].", backupDeltas.stream() + .map(InternalBackupDataStoreVO::getId).collect(Collectors.toList()), vm.getUuid()); + for (InternalBackupDataStoreVO delta : backupDeltas) { + VolumeVO volumeVO = volumeDao.findByIdIncludingRemoved(delta.getVolumeId()); + Backup.VolumeInfo backupVolumeInfo = volumeInfos.stream().filter(info -> volumeVO.getUuid().equals(info.getUuid())).findFirst().orElseThrow(); + VolumeInfo volumeInfo = duplicateAndCreateVolume(vm, host, backupVolumeInfo); + Volume volume = volumeApiService.attachVolumeToVM(vm.getId(), volumeInfo.getId(), null, false, true); + transitVolumeState(volume, Volume.Event.RestoreRequested); + delta.setVolumeId(volume.getId()); + } + } + + protected VolumeInfo duplicateAndCreateVolume(VirtualMachine vm, HostVO hostVo, Backup.VolumeInfo backupVolumeInfo) { + VolumeVO newVolume = duplicateVolume(backupVolumeInfo); + VolumeInfo volumeInfo = volumeDataFactory.getVolume(newVolume.getId()); + + try { + volumeInfo = volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeInfo, Hypervisor.HypervisorType.KVM, null, hostVo.getClusterId(), hostVo.getPodId()); + validateCorrectStorageType(null, newVolume, volumeInfo); + } catch (NoTransitionException ex) { + logger.error("Exception while creating volume to restore.", ex); + throw new CloudRuntimeException(ex); + } + + return volumeInfo; + } + + protected VolumeVO duplicateVolume(Backup.VolumeInfo backupVolumeInfo) { + VolumeVO volumeVO = volumeDao.findByUuidIncludingRemoved(backupVolumeInfo.getUuid()); + VolumeVO duplicateVO = new VolumeVO(volumeVO); + DiskOfferingVO diskOfferingVO = diskOfferingDao.findByUuidIncludingRemoved(backupVolumeInfo.getDiskOfferingId()); + duplicateVO.setDiskOfferingId(diskOfferingVO.getId()); + duplicateVO.setSize(backupVolumeInfo.getSize()); + duplicateVO.setMinIops(backupVolumeInfo.getMinIops()); + duplicateVO.setMaxIops(backupVolumeInfo.getMaxIops()); + duplicateVO.setAttached(null); + duplicateVO.setVolumeType(Volume.Type.DATADISK); + duplicateVO.setInstanceId(null); + duplicateVO.setPoolId(null); + duplicateVO.setPath(null); + return volumeDao.persist(duplicateVO); + } + + protected List getBackupsWithoutVolumes(List backups, List volumes) { + List deltasOnSecondaryWithNoVolumes = new ArrayList<>(); + for (InternalBackupDataStoreVO backup : backups) { + VolumeObjectTO volumeObjectTO = volumes.stream().filter(volumeTO -> volumeTO.getVolumeId() == backup.getVolumeId()) + .findFirst() + .orElse(null); + + if (volumeObjectTO == null) { + deltasOnSecondaryWithNoVolumes.add(backup); + } + } + return deltasOnSecondaryWithNoVolumes; + } + + protected List getVolumesThatAreNotPartOfTheBackup(List volumeObjectTOS, List deltasOnSecondary) { + List volumesWithNoBackups = new ArrayList<>(); + for (VolumeObjectTO volume : volumeObjectTOS) { + if (deltasOnSecondary.stream().noneMatch(delta -> delta.getVolumeId() == volume.getVolumeId())) { + volumesWithNoBackups.add(volume); + } + } + logger.debug("Found the following volumes that are not part of the backup being restored [{}].", volumesWithNoBackups); + return volumesWithNoBackups; + } + + protected void processBackupSuccess(boolean runningVm, List volumeTOs, HashMap volumeUuidToDeltaPrimaryRef, + HashMap volumeUuidToDeltaSecondaryRef, TakeKbossBackupAnswer answer, List parentBackupDeltasOnPrimary, + List succeedingVmSnapshots, BackupVO backupVO, boolean fullBackup, VirtualMachine userVm, Long hostId, boolean endChain, boolean isolated) { + long physicalBackupSize = 0; + logger.debug("Processing backup [{}] success.", backupVO.getUuid()); + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + physicalBackupSize = updateDeltaReferencesAndCalculateBackupPhysicalSize(volumeObjectTO, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, answer, + physicalBackupSize, endChain, isolated, backupVO); + } + + expungeOldDeltasAndUpdateVmSnapshotIfNeeded(parentBackupDeltasOnPrimary, succeedingVmSnapshots.isEmpty() ? null : succeedingVmSnapshots.get(0)); + + backupVO.setSize(physicalBackupSize); + backupVO.setStatus(Backup.Status.BackedUp); + backupVO.setBackedUpVolumes(backupManager.createVolumeInfoFromVolumes(new ArrayList<>(volumeDao.findByInstance(userVm.getId())))); + backupDao.loadDetails(backupVO); + backupVO.getDetails().putAll(backupManager.getBackupDetailsFromVM(userVm)); + backupVO.setType(fullBackup ? "FULL" : "INCREMENTAL"); + backupDao.update(backupVO.getId(), backupVO); + + transitVmState(userVm, runningVm ? VirtualMachine.Event.BackupSucceededRunning : VirtualMachine.Event.BackupSucceededStopped, hostId); + } + + protected void processBackupFailure(Answer answer, VirtualMachine vm, long hostId, boolean runningVm, BackupVO backupVO) { + if (answer instanceof TakeKbossBackupAnswer && ((TakeKbossBackupAnswer) answer).isVmConsistent()) { + logger.info("Backup [{}] of VM [{}] failed. However, the VM is still consistent, so we will roll back its state.", backupVO.getUuid(), vm.getUuid()); + backupVO.setStatus(Backup.Status.Failed); + + transitVmState(vm, runningVm ? VirtualMachine.Event.OperationFailedToRunning : VirtualMachine.Event.OperationFailedToStopped, hostId); + } else { + logger.info("Backup [{}] of VM [{}] ended in error. We are not sure if the VM is consistent; thus, we will set it as BackupError.", backupVO.getUuid(), vm.getUuid()); + transitVmState(vm, VirtualMachine.Event.OperationFailedToError, hostId); + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, runningVm ? VirtualMachine.State.Running.name() : VirtualMachine.State.Stopped.name(), false); + backupVO.setStatus(Backup.Status.Error); + } + + backupDao.update(backupVO.getId(), backupVO); + } + + protected void processRemovedBackups(List removedBackupIds) { + for (Long removedBackupId : removedBackupIds) { + BackupVO removedBackupVO = backupDao.findByIdIncludingRemoved(removedBackupId); + removedBackupVO.setStatus(Backup.Status.Expunged); + backupDao.update(removedBackupId, removedBackupVO); + internalBackupDataStoreDao.expungeByBackupId(removedBackupId); + backupDetailDao.removeDetailsExcept(removedBackupId, END_OF_CHAIN); + } + } + + /** + * For every backup, except for the one which the command was issued, will set them as Expunged regardless and hope operators will look + * at the logs. For the current one, if forced=false, will set it as error, otherwise, will set it as Expunged as well. + * */ + protected boolean processRemoveBackupFailures(boolean forced, Answer[] deleteAnswers, List removedBackupIds, InternalBackupJoinVO backupJoinVO) { + List failures = Arrays.stream(deleteAnswers).filter(answer -> !answer.getResult()).collect(Collectors.toList()); + Set failedToRemoveBackupIdSet = new HashSet<>(); + if (CollectionUtils.isNotEmpty(failures)) { + StringBuilder failureStringBuilder = new StringBuilder("Encountered the following failures during backup removal, all will be marked as Expunged and need to be" + + " manually deleted from storage. "); + for (Answer answer : failures) { + failedToRemoveBackupIdSet.add(((BackupDeleteAnswer)answer).getBackupId()); + failureStringBuilder.append(answer.getDetails()); + } + logger.error(failureStringBuilder.toString()); + } + + removedBackupIds.removeAll(failedToRemoveBackupIdSet); + + boolean result = failedToRemoveBackupIdSet.isEmpty(); + if (!forced && failedToRemoveBackupIdSet.remove(backupJoinVO.getId())) { + BackupVO failedVO = backupDao.findByIdIncludingRemoved(backupJoinVO.getId()); + logger.info("Since backup delete command was not forced, will not set the main backup [{}] as Expunged, will set it as error instead.", failedVO.getUuid()); + failedVO.setStatus(Backup.Status.Error); + backupDao.update(failedVO.getId(), failedVO); + } + + for (Long failedToRemove : failedToRemoveBackupIdSet) { + BackupVO failedVO = backupDao.findByIdIncludingRemoved(failedToRemove); + failedVO.setStatus(Backup.Status.Expunged); + logger.error("Setting backup [{}] as expunged, even though there was an error when deleting it from storage. Please look at the logs and check if it was deleted from" + + " storage.", failedVO.getUuid()); + backupDao.update(failedToRemove, failedVO); + } + + return result; + } + + protected void processConsolidateAnswer(ConsolidateVolumesAnswer cAnswer, List volumesToConsolidate, VirtualMachine vm) { + for (VolumeObjectTO volumeObjectTO : cAnswer.getSuccessfullyConsolidatedVolumes()) { + VolumeInfo volumeInfo = volumesToConsolidate.stream().filter(vol -> vol.getId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + transitVolumeState(volumeInfo.getVolume(), Volume.Event.OperationSucceeded); + volumesToConsolidate.remove(volumeInfo); + } + volumesToConsolidate.forEach(volumeInfo -> transitVolumeState(volumeInfo, Volume.Event.OperationFailed)); + if (cAnswer.getResult()) { + vmInstanceDetailsDao.removeDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS); + } else { + throw new BackupException(String.format("Failed to consolidate all volumes necessary of VM [%s]. Missing volumes are [%s].", vm.getUuid(), volumesToConsolidate), false); + } + } + + protected boolean processRestoreAnswers(VirtualMachine vm, Answer[] answers, boolean quickRestore) { + boolean cmdSucceeded = true; + for (Answer answer : answers) { + if (answer == null || !answer.getResult()) { + cmdSucceeded = false; + logger.error("Failed to restore backup due to: [{}].", answer == null ? "null answer" : answer.getDetails()); + } + if (answer instanceof RestoreKbossBackupAnswer && quickRestore) { + RestoreKbossBackupAnswer restoreAnswer = (RestoreKbossBackupAnswer) answer; + vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LINKED_VOLUMES_SECONDARY_STORAGE_UUIDS, StringUtils.join(restoreAnswer.getSecondaryStorageUuids(), ","), false); + } + } + return cmdSucceeded; + } + + protected boolean processValidationAnswer(Answer answer, BackupVO backupVO, UserVmVO validationVm, HostVO hostVo, ValidateKbossVmCommand validateKbossVmCommand) { + if (answer == null) { + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: Null answer from host [%s]", backupVO.getUuid(), + validationVm.getName(), hostVo.getName()); + logger.error(msg); + setBackupAsInvalidAndSendAlert(backupVO, msg); + return false; + } + if (!answer.getResult()) { + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: %s", backupVO.getUuid(), + validationVm.getName(), answer.getDetails()); + logger.error(msg); + setBackupAsInvalidAndSendAlert(backupVO, msg); + return false; + } + if (!(answer instanceof ValidateKbossVmAnswer)) { + return false; + } + ValidateKbossVmAnswer validateKbossVmAnswer = (ValidateKbossVmAnswer)answer; + boolean result = true; + String msg = String.format("Backup [%s] was validated using dummy VM [%s]. The backup was deemed invalid due to: ", backupVO.getUuid(), validationVm.getName()); + if (validateKbossVmCommand.isWaitForBoot() && !validateKbossVmAnswer.isBootValidated()) { + result = false; + msg += "\n - The VM did not boot within the expected time."; + } + if (validateKbossVmCommand.isExecuteScript() && validateKbossVmAnswer.getScriptResult() != null) { + result = false; + msg += "\n - The script did not output the expected output. Captured output: " + validateKbossVmAnswer.getScriptResult(); + } + if (validateKbossVmCommand.isTakeScreenshot() && validateKbossVmAnswer.getScreenshotPath() == null) { + result = false; + msg += "\n - We were unable to take a screenshot of the VM."; + } else if (validateKbossVmCommand.isTakeScreenshot()) { + logger.debug("Saving validation screenshot path [{}] to the backup details of backup [{}].", validateKbossVmAnswer.getScreenshotPath(), backupVO.getUuid()); + backupDetailDao.addDetail(backupVO.getId(), SCREENSHOT_PATH, validateKbossVmAnswer.getScreenshotPath(), false); + } + if (!result) { + setBackupAsInvalidAndSendAlert(backupVO, msg); + } + + return result; + } + + protected void handleBackupExceptionInRestore(VirtualMachine vm, BackupException jobResult) { + if (!jobResult.isVmConsistent()) { + UserVmVO vmVO = userVmDao.findById(vm.getId()); + vmVO.setState(VirtualMachine.State.RestoreError); + userVmDao.update(vmVO.getId(), vmVO); + for (VolumeVO vol : volumeDao.findByInstance(vmVO.getId())) { + vol.setState(Volume.State.RestoreError); + volumeDao.update(vol.getId(), vol); + } + } + } + + protected void handleRestoreException(Backup backup, VirtualMachine vm, Object jobResult) { + if (!(jobResult instanceof Throwable)) { + return; + } + if (jobResult instanceof BackupException) { + handleBackupExceptionInRestore(vm, (BackupException)jobResult); + } else if (jobResult instanceof BackupProviderException) { + throw (BackupProviderException) jobResult; + } + throw new CloudRuntimeException(String.format("Exception while restoring KVM internal incremental backup [%s]. Check the logs for more information.", backup.getUuid()), + ((Throwable)jobResult).getCause()); + } + + protected boolean endBackupChain(VirtualMachine vm) { + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(vm.getId()); + if (current == null) { + logger.debug("There is no current active chain, no need to do anything."); + return true; + } + + if (mergeCurrentBackupDeltas(current)) { + setEndOfChainAndRemoveCurrentForBackup(current); + return true; + } + return false; + } + + /** + * Merges the backup deltas related to the passed {@code InternalBackupJoinVO}. + * + * @return true if the merge was successful and false otherwise. + * */ + protected boolean mergeCurrentBackupDeltas(InternalBackupJoinVO backupJoinVO) { + VirtualMachine userVm = userVmDao.findById(backupJoinVO.getVmId()); + VMSnapshotVO succeedingVmSnapshot = getSucceedingVmSnapshot(backupJoinVO); + MergeDiskOnlyVmSnapshotCommand cmd = buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(backupJoinVO, userVm, succeedingVmSnapshot); + Long hostId = vmSnapshotHelper.pickRunningHost(backupJoinVO.getVmId()); + + Answer answer = sendBackupCommand(hostId, cmd); + if (answer == null || !answer.getResult()) { + logger.error("Failed to remove backup [{}]. Tried to merge the current deltas to cleanup the VM but failed due to [{}].", + backupJoinVO.getUuid(), answer != null ? answer.getDetails() : "no answer"); + return false; + } + + expungeOldDeltasAndUpdateVmSnapshotIfNeeded(internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()), succeedingVmSnapshot); + + if (succeedingVmSnapshot != null) { + return true; + } + + for (DeltaMergeTreeTO deltaMergeTreeTO : cmd.getDeltaMergeTreeToList()) { + VolumeVO volumeVO = volumeDao.findById(deltaMergeTreeTO.getVolumeObjectTO().getVolumeId()); + volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug("Updating volume [{}] path to [{}] as part of the backup delete cleanup process.", volumeVO.getUuid(), volumeVO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + } + + return true; + } + + protected void createDeleteCommandsAndMergeTrees(List volumeObjectTOs, Commands commands, List deletedDeltas, + VMSnapshotVO vmSnapshotSucceedingCurrentBackup, List deltaMergeTreeTOList) { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeId(volumeObjectTO.getVolumeId()); + if (delta == null) { + continue; + } + if (delta.getBackupDeltaPath().equals(volumeObjectTO.getPath())) { + commands.addCommand(new DeleteCommand(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, delta.getBackupDeltaParentPath()))); + deletedDeltas.add(delta); + logger.debug("Volume [{}] has a backup delta that will be deleted as part of the preparation to revert a VM snapshot.", volumeObjectTO.getUuid()); + } else { + deltaMergeTreeTOList.add(createDeltaMergeTree(false, false, delta, volumeObjectTO, vmSnapshotSucceedingCurrentBackup)); + } + } + } + + /*** + * Gets the list of parents that should be expunged. Will also create delete commands for them and add them to the list deleteCommands object. + * + * @param backupVO backup being expunged + * @param deleteCommands Commands object that will be appended with the delete commands for the parent backups. + * @return A pair which contains the list of backups that will be expunged, and the reference to the last backup of the chain that is still alive, if it exists. + */ + protected Pair, InternalBackupJoinVO> getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(BackupVO backupVO, Commands deleteCommands) { + logger.debug("Searching for removed parents of [{}] that should be expunged.", backupVO); + List backupParents = getBackupJoinParents(backupVO, true); + List backupParentsToBeExpunged = null; + InternalBackupJoinVO lastAliveBackup = null; + for (int i = 0; i < backupParents.size(); i++) { + InternalBackupJoinVO backupParent = backupParents.get(i); + if (Backup.Status.Removed.equals(backupParent.getStatus())) { + addBackupDeltasToDeleteCommand(backupParent.getId(), deleteCommands); + } else { + backupParentsToBeExpunged = backupParents.subList(0, i); + lastAliveBackup = backupParents.get(i); + break; + } + } + if (backupParentsToBeExpunged == null) { + backupParentsToBeExpunged = backupParents; + } + logger.debug("Found [{}] removed parents of [{}] that should be expunged: [{}].", backupParentsToBeExpunged.size(), backupVO, backupParentsToBeExpunged); + return new Pair<>(backupParentsToBeExpunged, lastAliveBackup); + } + + protected MergeDiskOnlyVmSnapshotCommand buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(InternalBackupJoinVO backupJoinVO, VirtualMachine userVm, VMSnapshotVO vmSnapshot) { + List deltaMergeTreeTOs = new ArrayList<>(); + + List volumeTOs = vmSnapshotHelper.getVolumeTOList(backupJoinVO.getVmId()); + Map> volumeIdToSnapshotDataStoreList = gatherSnapshotReferencesOfChildrenSnapshot(volumeTOs, vmSnapshot); + List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()); + + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + KbossTO kbossTO = new KbossTO(volumeObjectTO, volumeIdToSnapshotDataStoreList.getOrDefault(volumeObjectTO.getId(), new ArrayList<>())); + createDeltaMergeTreeForVolume(vmSnapshot == null, userVm.getState() == VirtualMachine.State.Running, deltasOnPrimary, vmSnapshot, kbossTO); + if (kbossTO.getDeltaMergeTreeTO() != null) { + deltaMergeTreeTOs.add(kbossTO.getDeltaMergeTreeTO()); + } else { + logger.debug("Volume [{}] does not have any deltas to merge as part of the backup delete process.", volumeObjectTO.getUuid()); + } + } + + return new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOs, userVm.getState().equals(VirtualMachine.State.Running), userVm.getInstanceName()); + } + + protected DataStore addBackupDeltasToDeleteCommand(long backupId, Commands deleteCommands) { + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupId); + List internalBackupDataStoreVOS = internalBackupDataStoreDao.listByBackupId(backupId); + DataStore dataStore = dataStoreManager.getDataStore(internalBackupJoinVO.getImageStoreId(), DataStoreRole.Image); + DataStoreTO dataStoreTO = dataStore.getTO(); + BackupDetailVO screenshotPath = backupDetailDao.findDetail(backupId, SCREENSHOT_PATH); + for (InternalBackupDataStoreVO internalBackupDataStoreVO : internalBackupDataStoreVOS) { + BackupDeltaTO backupDeltaTO = new BackupDeltaTO(dataStoreTO, Hypervisor.HypervisorType.KVM, internalBackupDataStoreVO.getBackupPath()); + backupDeltaTO.setId(backupId); + if (screenshotPath != null) { + backupDeltaTO.setScreenshotPath(screenshotPath.getValue()); + screenshotPath = null; + } + DeleteCommand deleteCommand = new DeleteCommand(backupDeltaTO); + deleteCommands.addCommand(deleteCommand); + } + return dataStore; + } + + protected Set getParentSecondaryStorageUrls(BackupVO backupVO) { + List parentBackups = getBackupJoinParents(backupVO, true); + Set secondaryStorageIds = parentBackups.stream().map(InternalBackupJoinVO::getImageStoreId).collect(Collectors.toSet()); + return secondaryStorageIds.stream().map(id -> imageStoreDao.findById(id).getUrl()).collect(Collectors.toSet()); + } + + protected List getChainImageStoreUrls(List backupChain) { + List chainImageStoreUrls; + LinkedHashSet imageStoreIdSet = backupChain.stream().map(InternalBackupJoinVO::getImageStoreId).collect(Collectors.toCollection(LinkedHashSet::new)); + chainImageStoreUrls = imageStoreIdSet.stream().map(id -> imageStoreDao.findById(id).getUrl()).collect(Collectors.toList()); + return chainImageStoreUrls; + } + + /** + * Gets the list of backup parents of a given BackupVO. + * @param backupVO the backup in question. + * @param includeRemoved whether to include removed (but not expunged) parents or not. + * @return list of parents, or an empty list if no parents found. + * */ + protected List getBackupJoinParents(BackupVO backupVO, boolean includeRemoved) { + List ancestorBackups; + + if (includeRemoved) { + ancestorBackups = internalBackupJoinDao.listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(backupVO.getVmId(), backupVO.getDate()); + } else { + ancestorBackups = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getDate(), true, false); + } + + for (int i = 0; i < ancestorBackups.size(); i++) { + if (ancestorBackups.get(i).getEndOfChain()) { + return ancestorBackups.subList(0, i); + } + } + + logger.debug("Found the following backup chain ancestors of backup [{}]: [{}].", backupVO, ancestorBackups); + return ancestorBackups; + } + + protected int getChainSizeForBackup(BackupOfferingVO offering, long zoneId) { + BackupOfferingDetailsVO detailsVO = backupOfferingDetailsDao.findDetail(offering.getId(), ApiConstants.BACKUP_CHAIN_SIZE); + if (detailsVO != null) { + return Integer.parseInt(detailsVO.getValue()); + } + return backupChainSize.valueIn(zoneId); + } + + /** + * Gets the list of backup children of a given backupVO. In ascending created order. + * + * @return list of children, or and empty list if no children found. + * */ + protected List getBackupJoinChildren(BackupVO backupVO) { + List children = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getDate(), false, true); + + long parentId = backupVO.getId(); + for (int i = 0; i < children.size(); i++) { + if (children.get(i).getParentId() != parentId) { + return children.subList(0, i); + } + parentId = children.get(i).getId(); + } + + return children; + } + + /** + * Creates a detail for the given BackupVO if the remaining chain size is one or less and the value of backupChainSize is greater than 0. + * */ + protected void setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(int remainingChainSize, int chainSize, long backupId, String backupUuid) { + if (remainingChainSize <= 1 && chainSize > 0) { + logger.debug("Setting backup [{}] as end of chain.", backupUuid); + backupDetailDao.persist(new BackupDetailVO(backupId, END_OF_CHAIN, Boolean.TRUE.toString(), true)); + } + } + + protected void setBackupVirtualSize(List volumeTOs, BackupVO backupVO) { + long virtualSize = 0; + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + virtualSize += volumeObjectTO.getSize(); + } + + backupVO.setProtectedSize(virtualSize); + } + + protected void updateBackupStatusToBackingUp(List volumeTOs, BackupVO backupVO) { + setBackupVirtualSize(volumeTOs, backupVO); + backupVO.setStatus(Backup.Status.BackingUp); + backupDao.update(backupVO.getId(), backupVO); + } + + /** + * Retrieves the current backup and removes the CURRENT detail. If the informed backup is not the end of chain, sets it as the new CURRENT + * */ + protected void updateCurrentBackup(InternalBackupJoinVO backup) { + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(backup.getVmId()); + + if (current != null) { + backupDetailDao.removeDetail(current.getId(), CURRENT); + } + + if (!backup.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(backup.getId(), CURRENT, Boolean.TRUE.toString(), true)); + } + } + + /** + * Given a backup, removes the CURRENT detail, and if the snapshot is not set as END_OF_CHAIN, sets it as END_OF_CHAIN. + * */ + protected void setEndOfChainAndRemoveCurrentForBackup(InternalBackupJoinVO currentBackup) { + backupDetailDao.removeDetail(currentBackup.getId(), CURRENT); + if (!currentBackup.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(currentBackup.getId(), END_OF_CHAIN, Boolean.TRUE.toString(), true)); + } + } + + protected void setBackupUnableToValidateAndSendAlert(BackupVO backupVO, String msg) { + backupVO.setValidationStatus(Backup.ValidationStatus.UnableToValidate); + backupDao.update(backupVO.getId(), backupVO); + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_UNABLE_TO_VALIDATE, backupVO.getZoneId(), null, String.format("Unable to validate backup [%s]", + backupVO.getName()), msg); + } + + protected void sendCleanupFailedEmail(BackupVO backupVO, String msg) { + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_CLEANUP_FAILED, backupVO.getZoneId(), null, String.format("Cleanup of validation of backup " + + "[%s] failed", + backupVO.getName()), msg); + } + + protected void setBackupAsInvalidAndSendAlert(BackupVO backupVO, String msg) { + backupVO.setValidationStatus(Backup.ValidationStatus.NotValid); + backupDao.update(backupVO.getId(), backupVO); + alertManager.sendAlert(AlertService.AlertType.ALERT_TYPE_BACKUP_VALIDATION_FAILED, backupVO.getZoneId(), null, String.format("Backup [%s] is not valid", + backupVO.getName()), msg); + } + + protected void configureKbossTosForCleanup(UserVmVO userVmVO, List deltasOnPrimary, List deltasOnSecondary, boolean runningVM, + List parentDeltasOnPrimary, List kbossTOS) { + for (VolumeObjectTO volumeObjectTO : vmSnapshotHelper.getVolumeTOList(userVmVO.getId())) { + InternalBackupStoragePoolVO deltaOnPrimary = deltasOnPrimary.stream() + .filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + volumeObjectTO.setPath(deltaOnPrimary.getBackupDeltaPath()); + + InternalBackupDataStoreVO deltaOnSecondary = + deltasOnSecondary.stream().filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElseThrow(); + KbossTO kbossTO = new KbossTO(volumeObjectTO, deltaOnPrimary.getBackupDeltaParentPath(), deltaOnSecondary.getBackupPath()); + + parentDeltasOnPrimary.stream() + .filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst() + .ifPresent(parentDelta -> kbossTO.setParentDeltaPathOnPrimary(parentDelta.getBackupDeltaParentPath())); + kbossTOS.add(kbossTO); + } + } + + protected void configureValidationSteps(ValidateKbossVmCommand cmd, BackupVO backup) { + BackupOfferingVO offeringVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detailsVO = backupOfferingDetailsDao.findDetail(offeringVO.getId(), ApiConstants.VALIDATION_STEPS); + String validationSteps = detailsVO.getValue(); + for (String step : validationSteps.split(",")) { + Backup.ValidationSteps enumStep = Backup.ValidationSteps.valueOf(step); + switch (enumStep) { + case screenshot: + cmd.setTakeScreenshot(true); + VMInstanceDetailVO screenshotWait = vmInstanceDetailsDao.findDetail(backup.getVmId(), VmDetailConstants.VALIDATION_SCREENSHOT_WAIT); + cmd.setScreenshotWait(screenshotWait != null ? Integer.valueOf(screenshotWait.getValue()) : + BackupValidationServiceJobController.backupValidationScreenshotDefaultWait.valueIn(backup.getAccountId())); + break; + case wait_for_boot: + cmd.setWaitForBoot(true); + VMInstanceDetailVO bootTimeout = vmInstanceDetailsDao.findDetail(backup.getVmId(), VmDetailConstants.VALIDATION_BOOT_TIMEOUT); + cmd.setBootTimeout(bootTimeout != null ? Integer.valueOf(bootTimeout.getValue()) : + BackupValidationServiceJobController.backupValidationBootDefaultTimeout.valueIn(backup.getAccountId())); + break; + case execute_command: + configureValidationScript(cmd, backup); + break; + } + } + } + + protected void configureValidationScript(ValidateKbossVmCommand cmd, BackupVO backupVO) { + long vmId = backupVO.getVmId(); + VMInstanceDetailVO script = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + if (script == null) { + logger.warn("Execute command step was configured but no script given. Ignoring this step for backup [{}].", backupVO.getUuid()); + return; + } + cmd.setExecuteScript(true); + cmd.setScriptToExecute(script.getValue()); + VMInstanceDetailVO scriptArguments = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_ARGUMENTS); + cmd.setScriptArguments(scriptArguments != null ? scriptArguments.getValue() : null); + VMInstanceDetailVO scriptExpectedResult = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_EXPECTED_RESULT); + cmd.setExpectedResult(scriptExpectedResult != null ? scriptExpectedResult.getValue() : "0"); + VMInstanceDetailVO scriptTimeout = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND_TIMEOUT); + cmd.setScriptTimeout(scriptTimeout != null ? Integer.valueOf(scriptTimeout.getValue()) : + BackupValidationServiceJobController.backupValidationScriptDefaultTimeout.valueIn(backupVO.getAccountId())); + } + + protected void createBasicBackupDetails(Long imageStoreId, Long parentId, BackupVO backupVO) { + backupDetailDao.persist(new BackupDetailVO(backupVO.getId(), IMAGE_STORE_ID, imageStoreId.toString(), false)); + backupDetailDao.persist(new BackupDetailVO(backupVO.getId(), PARENT_ID, parentId.toString(), false)); + } + + protected void updateReferencesAfterPrepareForSnapshotRevert(List deltaMergeTreeTOList, List snapRefsSucceedingCurrentBackup, + List deletedDeltas, InternalBackupJoinVO backupVO) { + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOList) { + SnapshotDataStoreVO snapshotRef = snapRefsSucceedingCurrentBackup.stream() + .filter(ref -> Objects.equals(ref.getVolumeId(), deltaMergeTreeTO.getVolumeObjectTO().getVolumeId())) + .findFirst() + .orElse(null); + if (snapshotRef != null) { + snapshotRef.setInstallPath(deltaMergeTreeTO.getParent().getPath()); + logger.debug("Updating snapshot reference [{}] path to [{}] as part of the preparation to restore a VM snapshot.", snapshotRef.getId(), snapshotRef.getInstallPath()); + snapshotDataStoreDao.update(snapshotRef.getId(), snapshotRef); + } + internalBackupStoragePoolDao.expungeByVolumeId(deltaMergeTreeTO.getVolumeObjectTO().getVolumeId()); + } + + for (InternalBackupStoragePoolVO delta : deletedDeltas) { + internalBackupStoragePoolDao.expungeByVolumeId(delta.getVolumeId()); + } + + setEndOfChainAndRemoveCurrentForBackup(backupVO); + } + + protected Answer sendBackupCommand(long hostId, Command cmd) { + cmd.setWait(backupTimeout.value()); + return agentManager.easySend(hostId, cmd); + } + + protected Answer[] sendBackupCommands(Long hostId, Commands cmds) throws OperationTimedoutException, AgentUnavailableException { + for (Command cmd : cmds) { + cmd.setWait(backupTimeout.value()); + } + return agentManager.send(hostId, cmds); + } + + protected void validateCorrectStorageType(BackupVO backupVO, VolumeVO volume, VolumeInfo volumeInfo) { + StoragePoolVO storagePoolVO = storagePoolDao.findById(volumeInfo.getDataStore().getId()); + if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { + logger.error("Error while trying to create volume [{}]. It was created in a storage that is not supported. Make sure that the disk offerings of VMs with backup " + + "offerings can only be allocated to file-based storages ({}).", backupVO, volume, supportedStoragePoolTypes); + throw new CloudRuntimeException(String.format("Unable to create volume [%s] due to a failure to allocate the volume. Please check the logs.", backupVO.getUuid())); + } + } + + protected void validateQuickRestore(Backup backup, boolean quickRestore) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.ALLOW_QUICK_RESTORE); + if (quickRestore && (detail == null || !Boolean.parseBoolean(detail.getValue()))) { + throw new BackupProviderException(String.format("Unable to quick restore backup [%s] using offering [%s] as the offering does not support quick restoration.", + backup.getUuid(), backupOfferingVO.getUuid())); + } + } + + protected boolean offeringSupportsValidation(InternalBackupJoinVO backup) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.VALIDATE); + if (detail == null || !Boolean.parseBoolean(detail.getValue())) { + logger.debug("Backup [{}] will not be validated as offering [{}] does not support it.", backup, backupOfferingVO.getUuid()); + return false; + } + return true; + } + + protected boolean offeringSupportsCompression(InternalBackupJoinVO backup) { + BackupOfferingVO backupOfferingVO = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + BackupOfferingDetailsVO detail = backupOfferingDetailsDao.findDetail(backupOfferingVO.getId(), ApiConstants.COMPRESS); + if (detail == null || !Boolean.parseBoolean(detail.getValue())) { + logger.debug("Backup [{}] will not be compressed as offering [{}] does not support it.", backup, backupOfferingVO.getUuid()); + return false; + } + return true; + } + + protected void validateVmState(VirtualMachine vm, String operation, VirtualMachine.State... additionalStates) { + List allowedStates = new ArrayList<>(this.allowedVmStates); + allowedStates.addAll(Arrays.asList(additionalStates)); + if (!allowedStates.contains(vm.getState())) { + throw new BackupProviderException(String.format("VM [%s] is not in the right state to %s. It must be in one of these states: %s", vm.getUuid(), operation, + allowedStates)); + } + } + + protected Pair validateCompressionStateForRestoreAndGetBackup(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Cannot restore it.", backupId); + return new Pair<>(false, null); + } + + if (backupVO.getCompressionStatus() == Backup.CompressionStatus.FinalizingCompression) { + logger.error("We cannot restore backups that are finalizing the compression process. Please wait for the process to end and try again later.", + allowedBackupStatesToCompress, backupVO.getStatus()); + return new Pair<>(false, null); + } + backupVO.setStatus(Backup.Status.Restoring); + backupDao.update(backupId, backupVO); + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state. This is synchronized with the backup compression check. We get a new backup reference to make sure the compression has not + * changed the backup compression state. + * */ + protected boolean validateBackupStateForRemoval(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to acquire lock for backup [{}]. Cannot remove it.", backupId); + return false; + } + + if (!allowedBackupStatesToRemove.contains(backupVO.getStatus())) { + logger.error("Backup [{}] is not in a state allowed to be removed. Current state is [{}]; allowed states are [{}]", backupVO, backupVO.getStatus(), + allowedBackupStatesToRemove); + return false; + } + + if (Backup.CompressionStatus.Compressing.equals(backupVO.getCompressionStatus())) { + logger.error("Backup [{}] is being compressed, we cannot delete it. Please wait for the compress process to end and try again later.", backupVO.getUuid()); + return false; + } + + if (Backup.ValidationStatus.Validating.equals(backupVO.getValidationStatus())) { + logger.error("Backup [{}] is being validated, we cannot delete it. Please wait for the validation process to end and try again later.", backupVO.getUuid()); + return false; + } + return true; + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to start the compression. This is synchronized with the backup removal check. We get a new backup reference to make sure the + * delete process has not changed the backup state. + * */ + protected Pair validateBackupStateForStartCompressionAndUpdateCompressionStatus(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Will abort the start of the compression process. We might try again later.", backupId); + return new Pair<>(false, null); + } + + if (!allowedBackupStatesToCompress.contains(backupVO.getStatus())) { + logger.error("We can only compress backups that are on states [{}]. Current backup state is [{}].", allowedBackupStatesToCompress, backupVO.getStatus()); + return new Pair<>(false, null); + } + + logger.info("Compressing backup [{}].", backupVO.getUuid()); + backupVO.setCompressionStatus(Backup.CompressionStatus.Compressing); + backupDao.update(backupVO.getId(), backupVO); + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to finalize the compression. This is synchronized with the backup restore check. We get a new backup reference to make sure + * the restore process has not changed the backup state. + * */ + protected Pair validateBackupStateForFinalizeCompression(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Will abort the finalize compression process. We might try again later.", backupId); + return new Pair<>(false, null); + } + + List children = getBackupJoinChildren(backupVO); + if (Backup.Status.Restoring == backupVO.getStatus() || children.stream().anyMatch(backup -> backup.getStatus() == Backup.Status.Restoring)) { + logger.warn( + "Backup [{}] not in right state to finish compression. We can only finish compression process if backup is in [{}] state and no children are being " + "restored. Will try again later", + backupVO, Backup.Status.BackedUp); + return new Pair<>(false, null); + } + + if (Backup.Status.BackedUp == backupVO.getStatus()) { + logger.info("Backup [{}] is in the right state to finish compression. Will start the process.", backupVO.getUuid()); + backupVO.setCompressionStatus(Backup.CompressionStatus.FinalizingCompression); + backupDao.update(backupId, backupVO); + } else { + logger.warn("Backup [{}] is in [{}] state. Aborting compression and cleaning up compressed data. We can only finish compression process if backup is in [{}] " + + "state.", backupVO.getUuid(), backupVO.getStatus(), Backup.Status.BackedUp); + backupVO.setCompressionStatus(Backup.CompressionStatus.CompressionError); + backupDao.update(backupId, backupVO); + } + return new Pair<>(true, backupVO); + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + protected Pair validateBackupStateForRestoreBackupToVM(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback>) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to get lock on backup [{}]. Cannot create VM from this backup right now.", backupId); + return new Pair<>(false, null); + } + + if (Backup.Status.BackedUp == backupVO.getStatus() || Backup.Status.Restoring == backupVO.getStatus()) { + logger.debug("Backup [{}] is in the right state to create VM from it. Will start the process.", backupVO.getUuid()); + Backup.Status oldStatus = backupVO.getStatus(); + backupVO.setStatus(Backup.Status.Restoring); + backupDao.update(backupId, backupVO); + return new Pair<>(true, oldStatus); + } else { + logger.warn("Backup [{}] is in [{}] state. Aborting VM creation from backup. We can only create VM from backup if backup is in [{}] state.", + backupVO.getUuid(), backupVO.getStatus(), List.of(Backup.Status.BackedUp, Backup.Status.Restoring)); + return new Pair<>(false, null); + } + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + /** + * Validates that the backup is in a valid state to validate. This is synchronized with the backup removal check. We get a new backup reference to make sure the removal process + * has not changed the backup state. + * */ + protected boolean validateBackupStateForValidation(long backupId) { + return Transaction.execute(TransactionLegacy.CLOUD_DB, (TransactionCallback) result -> { + BackupVO backupVO = null; + try { + backupVO = lockBackup(backupId); + if (backupVO == null) { + logger.warn("Unable to acquire lock for backup [{}]. Cannot validate it. It might have been removed.", backupId); + return false; + } + + if (!allowedBackupStatesToValidate.contains(backupVO.getStatus())) { + logger.error("Backup [{}] is not in a state allowed to be validated. Current state is [{}]; allowed states are [{}]", backupVO, backupVO.getStatus(), + allowedBackupStatesToValidate); + return false; + } + return true; + } finally { + if (backupVO != null) { + releaseBackup(backupId); + } + } + }); + } + + protected void validateStorages(List volumeTOs, String vmUuid) { + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + StoragePoolVO storagePoolVO = storagePoolDao.findById(volumeObjectTO.getPoolId()); + if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { + logger.error("Only able to take backups of VMs with volumes in the following storage types [{}]. Throwing an exception.", supportedStoragePoolTypes); + throw new BackupProviderException(String.format("Unable to take backup of VM [%s], please check the logs.", vmUuid)); + } + } + } + + protected void validateNoVmSnapshots(VirtualMachine vm) { + List vmSnapshotVOs = vmSnapshotDao.findByVm(vm.getId()); + if (!vmSnapshotVOs.isEmpty()) { + throw new BackupProviderException(String.format("Restoring VM [%s] would remove the current VM snapshots it has. Please remove the VM snapshots [%s] before" + + " restoring the backup.", vm.getUuid(), vmSnapshotVOs.stream().map(VMSnapshotVO::getUuid).collect(Collectors.toList()))); + } + } + + protected BackupVO lockBackup(long backupId) { + return backupDao.acquireInLockTable(backupId, 300); + } + + protected void releaseBackup(long backupId) { + backupDao.releaseFromLockTable(backupId); + } + + protected void transitVmState(VirtualMachine vm, VirtualMachine.Event event, long hostId) { + try { + virtualMachineManager.stateTransitTo(vm, event, hostId); + } catch (NoTransitionException e) { + String msg = String.format("Failed to change VM [%s] state with event [%s].", vm.getUuid(), event.toString()); + logger.error(msg, e); + throw new CloudRuntimeException(msg, e); + } + } + + protected void transitVolumeState(Volume volume, Volume.Event event) { + try { + volumeApiService.stateTransitTo(volume, event); + } catch (NoTransitionException e) { + throw new CloudRuntimeException(e); + } + } +} diff --git a/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties new file mode 100644 index 000000000000..33c86662ed69 --- /dev/null +++ b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/module.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +name=kboss +parent=backup diff --git a/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml new file mode 100644 index 000000000000..da6600e6a7d2 --- /dev/null +++ b/plugins/backup/kboss/src/main/resources/META-INF/cloudstack/kboss/spring-backup-kboss-context.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java b/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java new file mode 100644 index 000000000000..d16e0bd948fc --- /dev/null +++ b/plugins/backup/kboss/src/test/java/org/apache/cloudstack/backup/KbossBackupProviderTest.java @@ -0,0 +1,3115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.backup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.apache.cloudstack.backup.dao.InternalBackupDataStoreDao; +import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; +import org.apache.cloudstack.backup.dao.InternalBackupServiceJobDao; +import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.secstorage.heuristics.HeuristicType; +import org.apache.cloudstack.storage.command.BackupDeleteAnswer; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper; +import org.apache.cloudstack.storage.volume.VolumeObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.manager.Commands; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.resource.ResourceState; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeApiServiceImpl; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.exception.BackupProviderException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.UserVmManager; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceDetailVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + +@RunWith(MockitoJUnitRunner.class) +public class KbossBackupProviderTest { + + @Mock + private VirtualMachine virtualMachineMock; + + @Mock + private BackupOfferingVO backupOfferingMock; + + @Mock + private VolumeDao volumeDaoMock; + + @Mock + private VolumeVO volumeVoMock; + + @Mock + private SnapshotDataStoreDao snapshotDataStoreDaoMock; + + @Mock + private SnapshotDataStoreVO snapshotDataStoreVoMock; + + @Mock + private VMSnapshotDao vmSnapshotDaoMock; + + @Mock + private VMSnapshotVO vmSnapshotVoMock; + + @Mock + private VMSnapshotDetailsDao vmSnapshotDetailsDaoMock; + + @Mock + private VMSnapshotDetailsVO vmSnapshotDetailsVoMock; + + @Mock + private InternalBackupJoinDao internalBackupJoinDaoMock; + + @Mock + private InternalBackupJoinVO internalBackupJoinVoMock; + + @Mock + private InternalBackupDataStoreDao internalBackupDataStoreDaoMock; + + @Mock + private InternalBackupDataStoreVO internalBackupDataStoreVoMock; + + @Mock + private InternalBackupStoragePoolDao internalBackupStoragePoolDaoMock; + + @Mock + private BackupVO backupVoMock; + + @Mock + private BackupDetailsDao backupDetailDaoMock; + + @Mock + private BackupDetailVO backupDetailVoMock; + + @Mock + private ConfigKey backupChainSize; + + @Mock + private DataStoreManager dataStoreManagerMock; + + @Mock + private DataStore dataStoreMock; + + @Mock + private HeuristicRuleHelper heuristicRuleHelperMock; + + @Mock + private VMSnapshotHelper vmSnapshotHelperMock; + + @Mock + private BackupDao backupDaoMock; + + @Mock + private VolumeObjectTO volumeObjectToMock; + + @Mock + private VirtualMachineManager virtualMachineManagerMock; + + @Mock + private HostDao hostDaoMock; + + @Mock + private HostVO hostVOMock; + + @Mock + private UserVmDao userVmDaoMock; + + @Mock + private VMInstanceDetailsDao vmInstanceDetailsDaoMock; + + @Mock + private VMInstanceDetailVO vmInstanceDetailVoMock; + + @Mock + private UserVmVO userVmVOMock; + + @Mock + private BackupOfferingDao backupOfferingDaoMock; + + @Mock + private AgentManager agentManagerMock; + + @Mock + private TakeKbossBackupAnswer takeKbossBackupAnswerMock; + + @Mock + private EndPointSelector endPointSelectorMock; + + @Mock + private EndPoint endPointMock; + + @Mock + private Backup.VolumeInfo backupVolumeInfoMock; + + @Mock + private VolumeInfo volumeInfoMock; + + @Mock + private VolumeApiService volumeApiServiceMock; + + @Mock + private VolumeDataFactory volumeDataFactoryMock; + + @Mock + private BackupOfferingDetailsDao backupOfferingDetailsDaoMock; + + @Mock + private BackupOfferingDetailsVO backupOfferingDetailsVoMock; + + @Mock + private Answer answerMock; + + @Mock + private InternalBackupServiceJobDao internalBackupServiceJobDaoMock; + + @Mock + private UserVmManager userVmManagerMock; + + @Mock + private HypervisorGuruManager hypervisorGuruManagerMock; + @Mock + private HypervisorGuru hypervisorGuruMock; + @Mock + private VirtualMachineTO virtualMachineToMock; + @Mock + private ImageStoreDao imageStoreDaoMock; + + @Mock + private VolumeObject volumeObjectMock; + + @Mock + private InternalBackupStoragePoolVO internalBackupStoragePoolVoMock; + + @Mock + private BackupDeltaTO backupDeltaToMock; + + @Mock + private DeltaMergeTreeTO deltaMergeTreeToMock; + + @Spy + @InjectMocks + private KbossBackupProvider kbossBackupProviderSpy; + + private long vmId = 319832; + private long volumeId = 41; + + Long backupId = 312L; + + @Before + public void setup() { + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(vmId).when(backupVoMock).getVmId(); + doReturn(vmId).when(internalBackupJoinVoMock).getVmId(); + doReturn(vmId).when(userVmVOMock).getId(); + doReturn(backupId).when(backupVoMock).getId(); + } + + + @Test + public void assignVMToBackupOfferingTestNotKvm() { + doReturn(Hypervisor.HypervisorType.Any).when(virtualMachineMock).getHypervisorType(); + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + @Test + public void assignVMToBackupOfferingTestKvmWithUnsupportedDiskOnlyVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn("Anything").when(vmSnapshotDetailsVoMock).getName(); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + @Test + public void assignVMToBackupOfferingTestKvmWithSupportedDiskOnlyVmSnapshotAndDiskAndMemoryVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT).when(vmSnapshotDetailsVoMock).getName(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertFalse(result); + } + + + @Test + public void assignVMToBackupOfferingTestKvmWithSupportedDiskOnlyVmSnapshotAndNoDiskAndMemoryVmSnapshot() { + doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + doReturn(List.of(vmSnapshotVoMock)).when(vmSnapshotDaoMock).findByVmAndByType(vmId, VMSnapshot.Type.Disk); + long vmSnapId = 921; + doReturn(vmSnapId).when(vmSnapshotVoMock).getId(); + doReturn(List.of(vmSnapshotDetailsVoMock)).when(vmSnapshotDetailsDaoMock).listDetails(vmSnapId); + doReturn(VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT).when(vmSnapshotDetailsVoMock).getName(); + + boolean result = kbossBackupProviderSpy.assignVMToBackupOffering(virtualMachineMock, backupOfferingMock); + assertTrue(result); + } + + @Test + public void removeVMFromBackupOfferingTestNoActiveChain() { + doReturn(VirtualMachine.State.Running).when(virtualMachineMock).getState(); + + boolean result = kbossBackupProviderSpy.removeVMFromBackupOffering(virtualMachineMock); + + verify(kbossBackupProviderSpy, Mockito.never()).mergeCurrentBackupDeltas(any()); + assertTrue(result); + } + + @Test + public void removeVMFromBackupOfferingTestWithActiveChain() { + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + + boolean result = kbossBackupProviderSpy.removeVMFromBackupOffering(virtualMachineMock); + + verify(kbossBackupProviderSpy, Mockito.times(1)).mergeCurrentBackupDeltas(any()); + assertTrue(result); + } + + @Test + public void removeVMFromBackupOfferingTestFailedToEndChain() { + doReturn(VirtualMachine.State.Stopped).when(userVmVOMock).getState(); + doReturn(false).when(kbossBackupProviderSpy).endBackupChain(any()); + doReturn(userVmVOMock).when(userVmDaoMock).findById(any()); + doNothing().when(vmInstanceDetailsDaoMock).addDetail(Mockito.anyLong(), any(), any(), Mockito.anyBoolean()); + + boolean result = kbossBackupProviderSpy.removeVMFromBackupOffering(userVmVOMock); + + verify(vmInstanceDetailsDaoMock, Mockito.times(1)).addDetail(Mockito.anyLong(), any(), any(), Mockito.anyBoolean()); + verify(userVmDaoMock, Mockito.times(1)).update(Mockito.anyLong(), any()); + assertFalse(result); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedEmptyList() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(new ArrayList<>()).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertTrue(result.isEmpty()); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorIsEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertTrue(result.isEmpty()); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestors() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), result); + } + + @Test + public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestorsNoEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock), result); + } + + @Test + public void getBackupJoinParentsTestNoRemovedAncestorMultipleAncestorsNoEndOfChain() { + Date date = DateUtil.now(); + doReturn(date).when(backupVoMock).getDate(); + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock1).getEndOfChain(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(false).when(internalBackupJoinVoMock2).getEndOfChain(); + doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(vmId, date, true, + false); + + List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, false); + + assertEquals(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock), result); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeLowerThanOneAndConfigIsZero() { + int chainSize = 0; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(-1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.never()).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeLowerThanOneAndConfigBiggerThanZero() { + int chainSize = 1; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(-1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeBiggerThanOne() { + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(2, 0, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.never()).persist(any()); + } + + @Test + public void setEndOfChainTrueIfRemainingChainSizeIsOneTestChainSizeIsOne() { + int chainSize = 2; + kbossBackupProviderSpy.setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(1, chainSize, 1, "uuid"); + + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsEmpty() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(), null); + + assertNull(result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), + any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsBiggerThanChainSize() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), null); + + assertEquals(internalBackupJoinVoMock1, result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsSmallerThanChainSize() { + int chainSize = 3; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO internalBackupJoinVoMock2 = Mockito.mock(InternalBackupJoinVO.class); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2), null); + + assertEquals(internalBackupJoinVoMock1, result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getParentAndSetEndOfChainTestBackupChainIsNotEmptyParentIsRemoved() { + int chainSize = 2; + doReturn(chainSize).when(kbossBackupProviderSpy).getChainSizeForBackup(any(), Mockito.anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + + InternalBackupJoinVO internalBackupJoinVoMock1 = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(internalBackupJoinVoMock1).getStatus(); + InternalBackupJoinVO result = kbossBackupProviderSpy.getParentAndSetEndOfChain(backupVoMock, List.of(internalBackupJoinVoMock1), null); + + assertNull(result); + verify(kbossBackupProviderSpy, Mockito.times(1)).setEndOfChainTrueIfRemainingChainSizeIsOneOrLess(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyLong(), any()); + } + + @Test + public void getImageStoreForBackupTestNoHeuristic() { + long zoneId = 2; + doReturn(null).when(heuristicRuleHelperMock).getImageStoreIfThereIsHeuristicRule(zoneId, HeuristicType.BACKUP, backupVoMock); + doReturn(dataStoreMock).when(dataStoreManagerMock).getImageStoreWithFreeCapacity(zoneId); + + DataStore result = kbossBackupProviderSpy.getImageStoreForBackup(zoneId, backupVoMock); + + assertEquals(dataStoreMock, result); + } + + @Test + public void getImageStoreForBackupTestWithHeuristic() { + long zoneId = 2; + doReturn(dataStoreMock).when(heuristicRuleHelperMock).getImageStoreIfThereIsHeuristicRule(zoneId, HeuristicType.BACKUP, backupVoMock); + + DataStore result = kbossBackupProviderSpy.getImageStoreForBackup(zoneId, backupVoMock); + + assertEquals(dataStoreMock, result); + verify(dataStoreManagerMock, Mockito.never()).getImageStoreWithFreeCapacity(Mockito.anyLong()); + } + + @Test (expected = CloudRuntimeException.class) + public void getImageStoreForBackupTestNoStorageFound() { + kbossBackupProviderSpy.getImageStoreForBackup(0L, backupVoMock); + } + + @Test + public void getSucceedingVmSnapshotListTestBackupIsNull() { + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(null); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestNoCurrentSnapshotVo() { + doReturn(null).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentCreatedBeforeBackup() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(before).when(vmSnapshotVoMock).getCreated(); + doReturn(now).when(internalBackupJoinVoMock).getDate(); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertTrue(result.isEmpty()); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasNoParent() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(1, result.size()); + assertEquals(vmSnapshotVoMock, result.get(0)); + } + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasParentsCreatedAfter() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime()-10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + long snapParentId = 909; + doReturn(snapParentId).when(vmSnapshotVoMock).getParent(); + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + doReturn(now).when(vmSnapshotVoMock1).getCreated(); + doReturn(vmSnapshotVoMock1).when(vmSnapshotDaoMock).findById(snapParentId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(List.of(vmSnapshotVoMock1, vmSnapshotVoMock), result); + } + + + @Test + public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasParentsCreatedBefore() { + doReturn(vmSnapshotVoMock).when(vmSnapshotDaoMock).findCurrentSnapshotByVmId(vmId); + Date before = DateUtil.now(); + before.setTime(before.getTime() - 10000); + Date now = DateUtil.now(); + doReturn(now).when(vmSnapshotVoMock).getCreated(); + doReturn(before).when(internalBackupJoinVoMock).getDate(); + long snapParentId = 909; + doReturn(snapParentId).when(vmSnapshotVoMock).getParent(); + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + Date evenBefore = new Date(before.getTime() - 10000); + doReturn(evenBefore).when(vmSnapshotVoMock1).getCreated(); + doReturn(vmSnapshotVoMock1).when(vmSnapshotDaoMock).findById(snapParentId); + + List result = kbossBackupProviderSpy.getSucceedingVmSnapshotList(internalBackupJoinVoMock); + + assertEquals(List.of(vmSnapshotVoMock), result); + } + + @Test + public void mapVolumesToVmSnapshotReferencesTestVmSnapshotVOListIsEmpty() { + kbossBackupProviderSpy.mapVolumesToVmSnapshotReferences(List.of(), List.of()); + + verify(vmSnapshotHelperMock, Mockito.never()).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(1); + } + + @Test + public void mapVolumesToVmSnapshotReferencesTestVmSnapshotVOListHasTwoElements() { + VMSnapshotVO vmSnapshotVoMock1 = Mockito.mock(VMSnapshotVO.class); + doReturn(1L).when(vmSnapshotVoMock).getId(); + doReturn(2L).when(vmSnapshotVoMock1).getId(); + doNothing().when(kbossBackupProviderSpy).mapVolumesToSnapshotReferences(Mockito.anyList(), Mockito.anyList(), anyMap()); + + kbossBackupProviderSpy.mapVolumesToVmSnapshotReferences(List.of(), List.of(vmSnapshotVoMock, vmSnapshotVoMock1)); + + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(1); + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(2); + verify(kbossBackupProviderSpy, times(1)).mapVolumesToSnapshotReferences(Mockito.anyList(), Mockito.anyList(), anyMap()); + } + + @Test + public void createDeltaReferencesTestFullBackupEndOfChain() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, + true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, List.of())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void createDeltaReferencesTestIsolatedBackup() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, + true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, List.of())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(0)).findAndSetParentBackupPath(any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(0)).findAndSetParentBackupPath(any(), any(), any()); + verify(internalBackupStoragePoolDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void createDeltaReferencesTestNotFullBackupEndOfChain() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + KbossTO kbossTO = new KbossTO(volumeObjectToMock, List.of()); + doReturn(null).when(kbossBackupProviderSpy).createDeltaMergeTreeForVolume(false, true, List.of(), null, kbossTO); + doNothing().when(kbossBackupProviderSpy).findAndSetParentBackupPath(List.of(), null, kbossTO); + + kbossBackupProviderSpy.createDeltaReferences(false, true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, kbossTO); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).findAndSetParentBackupPath(List.of(), null, kbossTO); + } + + @Test + public void createDeltaReferencesTestFullBackupNotEndOfChainDoesNotHaveVmSnapshotSucceedingLastBackup() { + doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); + + kbossBackupProviderSpy.createDeltaReferences(true, + false, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, List.of())); + + verify(internalBackupDataStoreDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void orchestrateTakeBackupTestHostIsDownReturnFalse() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Down); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertFalse(result.first()); + } + + @Test + public void orchestrateTakeBackupTestHostIsDisconnectedReturnFalse() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Disconnected); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertFalse(result.first()); + } + + @Test (expected = BackupProviderException.class) + public void orchestrateTakeBackupTestInitialValidationThrowException() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + Mockito.doThrow(new BackupProviderException("tst")).when(kbossBackupProviderSpy).validateStorages(any(), any()); + + kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertEquals(Backup.Status.Failed, backupVoMock.getStatus()); + Mockito.verify(backupDaoMock, Mockito.times(1)).update(Mockito.anyLong(), any()); + } + + @Test + public void orchestrateTakeBackupTestIsolatedBackupFailed() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, true); + assertFalse(result.first()); + assertNull(result.second()); + verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupFailure(any(), any(), Mockito.anyLong(), Mockito.anyBoolean(), any()); + } + + @Test + public void orchestrateTakeBackupTestIsolatedBackupSuccessWithCompression() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + doReturn(takeKbossBackupAnswerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(takeKbossBackupAnswerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean()); + doReturn(true).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).compressBackupAsync(internalBackupJoinVoMock, 0, 0); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, true); + assertTrue(result.first()); + assertEquals(backupId, result.second()); + verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean()); + verify(kbossBackupProviderSpy, Mockito.times(1)).compressBackupAsync(internalBackupJoinVoMock, 0, 0); + } + + @Test + public void orchestrateTakeBackupTestBackupSuccessWithValidation() { + Mockito.when(virtualMachineManagerMock.findById(Mockito.anyLong())).thenReturn(virtualMachineMock); + Mockito.when(vmSnapshotHelperMock.pickRunningHost(Mockito.anyLong())).thenReturn(1L); + Mockito.when(hostDaoMock.findById(Mockito.anyLong())).thenReturn(hostVOMock); + Mockito.when(hostVOMock.getStatus()).thenReturn(Status.Up); + Mockito.when(hostVOMock.getResourceState()).thenReturn(ResourceState.Enabled); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any()); + + VolumeObjectTO vol1 = Mockito.mock(VolumeObjectTO.class); + VolumeObjectTO vol2 = Mockito.mock(VolumeObjectTO.class); + doReturn(List.of(vol1, vol2)).when(vmSnapshotHelperMock).getVolumeTOList(Mockito.anyLong()); + + doNothing().when(kbossBackupProviderSpy).validateStorages(any(), any()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(any()); + InternalBackupJoinVO parentMock = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentMock).when(kbossBackupProviderSpy).getParentAndSetEndOfChain(any(), any(), any()); + doReturn(dataStoreMock).when(kbossBackupProviderSpy).getImageStoreForBackup(any(), any()); + doReturn(takeKbossBackupAnswerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(takeKbossBackupAnswerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean()); + doReturn(false).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).validateBackupAsyncIfHasOfferingSupport(any(), anyLong(), anyLong()); + + Pair result = kbossBackupProviderSpy.orchestrateTakeBackup(backupVoMock, false, false); + assertTrue(result.first()); + assertEquals(backupId, result.second()); + verify(internalBackupStoragePoolDaoMock).listByBackupId(0); + verify(internalBackupDataStoreDaoMock).listByBackupId(0); + verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).processBackupSuccess(anyBoolean(), any(), any(), any(), any(), any(), any(), any(), anyBoolean(), any(), + anyLong(), anyBoolean(), anyBoolean()); + verify(kbossBackupProviderSpy, Mockito.times(1)).validateBackupAsyncIfHasOfferingSupport(internalBackupJoinVoMock, 0, 0); + } + + @Test + public void orchestrateDeleteBackupTestBackupStateIsNotOk() { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteFailedBackup() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(true).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteBackupWithLiveChildren() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findByParentId(anyLong()); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupVoMock).setStatus(Backup.Status.Removed); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenFailedToMerge() throws OperationTimedoutException, AgentUnavailableException { + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(false).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + verify(kbossBackupProviderSpy).mergeCurrentBackupDeltas(any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentNoEndPoint() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(null).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(null).when(endPointSelectorMock).select((DataStore)null); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock).persist(any()); + verify(kbossBackupProviderSpy, never()).sendBackupCommands(anyLong(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentTimedoutException() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(null).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentFailedSetNotEmpty() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(new Pair<>(List.of(), parentVo)).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(false).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); + + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertFalse(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock, Mockito.times(2)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithParentFailedSetEmpty() throws OperationTimedoutException, AgentUnavailableException { + long parentBackupId = 12; + doReturn(parentBackupId).when(internalBackupJoinVoMock).getParentId(); + doReturn(virtualMachineMock).when(virtualMachineManagerMock).findById(vmId); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForRemoval(backupId); + doReturn(false).when(kbossBackupProviderSpy).deleteFailedBackup(backupVoMock); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentBackupId); + doReturn(Backup.Status.BackedUp).when(parentVo).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + doReturn(new Pair<>(List.of(), parentVo)).when(kbossBackupProviderSpy).getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(any(), any()); + doReturn(endPointMock).when(endPointSelectorMock).select((DataStore)null); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(true).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); + + + Boolean result = kbossBackupProviderSpy.orchestrateDeleteBackup(backupVoMock, false); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateVmState(any(), any(), any()); + verify(backupDetailDaoMock, Mockito.times(2)).persist(any()); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommands(anyLong(), any()); + } + + @Test + public void orchestrateRestoreVMFromBackupTestInvalidState() { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(false, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, false); + + assertFalse(result); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreVMFromBackupTestCurrentBackupNoHostToRestore() throws AgentUnavailableException { // fixxxxxxx + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + doThrow(AgentUnavailableException.class).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + + kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, false); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupTimeOut() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(currentBackupId).when(currentBackup).getId(); + doReturn(currentBackup).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupNullAnswers() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(currentBackupId).when(currentBackup).getId(); + doReturn(currentBackup).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + assertFalse(result); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupAnswerFalse() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, false); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(currentBackupId).when(currentBackup).getId(); + doReturn(currentBackup).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, false, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(new Answer[]{Mockito.mock(Answer.class)}).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(false).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, false, null, true); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + assertFalse(result); + } + + @Test + public void orchestrateRestoreVMFromBackupTestSameVmQuickRestoreCurrentBackupAnswerTrue() throws AgentUnavailableException, OperationTimedoutException { + doNothing().when(kbossBackupProviderSpy).validateNoVmSnapshots(virtualMachineMock); + doNothing().when(kbossBackupProviderSpy).validateQuickRestore(backupVoMock, true); + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + long currentBackupId = 39; + InternalBackupJoinVO currentBackup = Mockito.mock(InternalBackupJoinVO.class); + doReturn(currentBackupId).when(currentBackup).getId(); + doReturn(currentBackup).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(hostVOMock).when(kbossBackupProviderSpy).getHostToRestore(virtualMachineMock, true, null); + doNothing().when(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), anyBoolean()); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesThatAreNotPartOfTheBackup(any(), any()); + doReturn(List.of()).when(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); + doReturn(new Answer[]{Mockito.mock(Answer.class)}).when(kbossBackupProviderSpy).sendBackupCommands(anyLong(), any()); + doReturn(true).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(currentBackup); + doReturn(List.of()).when(kbossBackupProviderSpy).getVolumesToConsolidate(any(), any(), any(), anyLong(), anyBoolean()); + doReturn(true).when(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + + boolean result = kbossBackupProviderSpy.orchestrateRestoreVMFromBackup(backupVoMock, virtualMachineMock, true, null, true); + + verify(internalBackupStoragePoolDaoMock).listByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).createAndAttachVolumes(any(), any(), any(), any()); + verify(kbossBackupProviderSpy).populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(any(), any(), any(), any(), any()); + verify(kbossBackupProviderSpy).updateVolumePathsAndSizeIfNeeded(any(), any(), anyList(), anyList(), anyBoolean()); + verify(internalBackupStoragePoolDaoMock).expungeByBackupId(currentBackupId); + verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(currentBackup); + verify(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + assertTrue(result); + } + + @Test + public void orchestrateRestoreBackedUpVolumeTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, null, null, false); + + assertFalse(result.first()); + verify(kbossBackupProviderSpy, Mockito.never()).sendBackupCommand(anyLong(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void orchestrateRestoreBackedUpVolumeTestAnswerFalse() { + doReturn(new Pair<>(true, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + doReturn(volumeVoMock).when(volumeDaoMock).findByUuidIncludingRemoved(any()); + doReturn(hostVOMock).when(hostDaoMock).findByIp(any()); + doReturn(volumeInfoMock).when(kbossBackupProviderSpy).duplicateAndCreateVolume(virtualMachineMock, hostVOMock, backupVolumeInfoMock); + doReturn(volumeObjectToMock).when(volumeInfoMock).getTO(); + doReturn(new Pair(null, null)).when(kbossBackupProviderSpy).generateBackupAndVolumePairForSingleNewVolume(any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).getParentSecondaryStorageUrls(backupVoMock); + doReturn(false).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, backupVolumeInfoMock, null, false); + + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommand(anyLong(), any()); + } + + @Test + public void orchestrateRestoreBackedUpVolumeTestQuickRestoreAnswerTrue() { + doReturn(new Pair<>(true, null)).when(kbossBackupProviderSpy).validateCompressionStateForRestoreAndGetBackup(backupId); + doReturn(volumeVoMock).when(volumeDaoMock).findByUuidIncludingRemoved(any()); + doReturn(hostVOMock).when(hostDaoMock).findByIp(any()); + doReturn(volumeInfoMock).when(kbossBackupProviderSpy).duplicateAndCreateVolume(virtualMachineMock, hostVOMock, backupVolumeInfoMock); + doReturn(volumeObjectToMock).when(volumeInfoMock).getTO(); + doReturn(new Pair(null, null)).when(kbossBackupProviderSpy).generateBackupAndVolumePairForSingleNewVolume(any(), any(), any()); + doReturn(Set.of()).when(kbossBackupProviderSpy).getParentSecondaryStorageUrls(backupVoMock); + doReturn(true).when(kbossBackupProviderSpy).processRestoreAnswers(any(), any(), anyBoolean()); + doReturn(volumeVoMock).when(volumeInfoMock).getVolume(); + doReturn(volumeVoMock).when(volumeApiServiceMock).attachVolumeToVM(anyLong(), anyLong(), any(), anyBoolean(), anyBoolean()); + doReturn(true).when(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + + Pair result = kbossBackupProviderSpy.orchestrateRestoreBackedUpVolume(backupVoMock, virtualMachineMock, backupVolumeInfoMock, null, true); + + assertTrue(result.first()); + + verify(kbossBackupProviderSpy, Mockito.times(1)).sendBackupCommand(anyLong(), any()); + verify(volumeApiServiceMock).attachVolumeToVM(anyLong(), anyLong(), any(), anyBoolean(), anyBoolean()); + verify(kbossBackupProviderSpy).finalizeQuickRestore(any(), anyList(), anyLong()); + } + + @Test + public void startBackupCompressionTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertFalse(result); + } + + @Test + public void startBackupCompressionTestNullAnswer() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + long parentId = 1332; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of(internalBackupDataStoreVoMock)).when(internalBackupDataStoreDaoMock).listByBackupId(backupId); + InternalBackupDataStoreVO parentDelta = mock(InternalBackupDataStoreVO.class); + doReturn(List.of(parentDelta)).when(internalBackupDataStoreDaoMock).listByBackupId(parentId); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(anyLong(), any()); + doReturn("zstd").when(backupOfferingDetailsVoMock).getValue(); + doReturn(List.of(parentVo)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).getChainImageStoreUrls(any()); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertFalse(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void startBackupCompressionTestSuccess() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + long parentId = 1332; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + InternalBackupJoinVO parentVo = Mockito.mock(InternalBackupJoinVO.class); + doReturn(parentVo).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of(internalBackupDataStoreVoMock)).when(internalBackupDataStoreDaoMock).listByBackupId(backupId); + InternalBackupDataStoreVO parentDelta = mock(InternalBackupDataStoreVO.class); + doReturn(List.of(parentDelta)).when(internalBackupDataStoreDaoMock).listByBackupId(parentId); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(anyLong(), any()); + doReturn("zstd").when(backupOfferingDetailsVoMock).getValue(); + doReturn(List.of(parentVo)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).getChainImageStoreUrls(any()); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.startBackupCompression(backupId, 0); + + assertTrue(result); + verify(internalBackupServiceJobDaoMock).persist(any()); + } + + @Test + public void finalizeBackupCompressionTestInvalidState() { + doReturn(new Pair<>(false, null)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertFalse(result); + } + + @Test + public void finalizeBackupCompressionTestNullAnswer() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertFalse(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void finalizeBackupCompressionTestCleanup() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertTrue(result); + verify(backupVoMock, Mockito.never()).setCompressionStatus(Backup.CompressionStatus.Compressed); + verify(backupDaoMock, Mockito.never()).update(backupId, backupVoMock); + } + + @Test + public void finalizeBackupCompressionTestSuccess() { + doReturn(new Pair<>(true, backupVoMock)).when(kbossBackupProviderSpy).validateBackupStateForFinalizeCompression(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(hostVOMock).when(hostDaoMock).findById(0L); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("1").when(answerMock).getDetails(); + doNothing().when(kbossBackupProviderSpy).validateBackupAsyncIfHasOfferingSupport(any(), anyLong(), anyLong()); + + boolean result = kbossBackupProviderSpy.finalizeBackupCompression(backupId, 0); + + assertTrue(result); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.Compressed); + verify(backupDaoMock).update(backupId, backupVoMock); + } + + @Test + public void validateBackupTestInvalidState() { + doReturn(false).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertFalse(result); + } + + @Test + public void validateBackupTestValidateWithHash() { + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + doReturn(backupVoMock).when(backupDaoMock).findById(backupId); + doReturn(backupDetailVoMock).when(backupDetailDaoMock).findDetail(backupId, BackupDetailsDao.BACKUP_HASH); + doReturn(true).when(kbossBackupProviderSpy).validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateWithHash(backupId, backupVoMock, backupDetailVoMock); + } + + @Test + public void validateBackupTestValidateWithValidationVm() { + doReturn(true).when(kbossBackupProviderSpy).validateBackupStateForValidation(backupId); + doReturn(backupVoMock).when(backupDaoMock).findById(backupId); + doReturn(null).when(backupDetailDaoMock).findDetail(backupId, BackupDetailsDao.BACKUP_HASH); + doReturn(true).when(kbossBackupProviderSpy).validateWithValidationVm(backupId, 0, backupVoMock); + + boolean result = kbossBackupProviderSpy.validateBackup(backupId, 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).validateWithValidationVm(backupId, 0, backupVoMock); + } + + @Test + public void finishBackupChainTestInvalidState() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Migrating).when(userVmVOMock).getState(); + + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); + + assertFalse(result); + } + + @Test + public void finishBackupChainTestRunningVm() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(userVmVOMock); + + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).endBackupChain(userVmVOMock); + } + + @Test + public void finishBackupChainTestBackupError() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.BackupError).when(userVmVOMock).getState(); + doReturn(true).when(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); + + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); + } + + @Test + public void prepareVmForSnapshotRevertTestNoCurrentBackup() { + doReturn(null).when(internalBackupJoinDaoMock).findCurrent(vmId); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).getSucceedingVmSnapshot(any()); + } + + @Test + public void prepareVmForSnapshotRevertTestCurrentBackupBeforeVmSnapshot() { + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(Date.from(Instant.EPOCH)).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.now())).when(vmSnapshotVoMock).getCreated(); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).getSucceedingVmSnapshot(any()); + } + + @Test (expected = CloudRuntimeException.class) + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotTimeout() throws OperationTimedoutException, AgentUnavailableException { + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList()); + doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test (expected = CloudRuntimeException.class) + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotNullAnswer() throws OperationTimedoutException, AgentUnavailableException { + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy, never()).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test + public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotSuccess() throws OperationTimedoutException, AgentUnavailableException { + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); + doReturn(Date.from(Instant.now())).when(internalBackupJoinVoMock).getDate(); + doReturn(Date.from(Instant.EPOCH)).when(vmSnapshotVoMock).getCreated(); + doReturn(List.of()).when(vmSnapshotHelperMock).getVolumeTOList(vmId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList()); + doReturn(new Answer[]{}).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); + doNothing().when(kbossBackupProviderSpy).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); + + verify(kbossBackupProviderSpy).updateReferencesAfterPrepareForSnapshotRevert(any(), any(), any(), any()); + } + + @Test (expected = BackupException.class) + public void finalizeQuickRestoreTestStoppedVmStartException() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(VirtualMachine.State.Stopped).when(userVmVOMock).getState(); + doThrow(CloudRuntimeException.class).when(userVmManagerMock).startVirtualMachine(anyLong(), any(), any(), any(), anyBoolean()); + + kbossBackupProviderSpy.finalizeQuickRestore(virtualMachineMock, List.of(), 0); + } + + @Test + public void finalizeQuickRestoreTestStoppedVmStartSuccess() { + doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); + doReturn(true).when(kbossBackupProviderSpy).consolidateVolumes(any(), anyLong(), anyList()); + + boolean result = kbossBackupProviderSpy.finalizeQuickRestore(virtualMachineMock, List.of(), 0); + + assertTrue(result); + verify(kbossBackupProviderSpy).consolidateVolumes(any(), anyLong(), anyList()); + } + + @Test + public void validateWithHashTestNoHosts() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(null).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + } + + @Test + public void validateWithHashTestAnswerResultFalse() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(false).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + } + + @Test + public void validateWithHashTestDifferentHash() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("wrongHash").when(answerMock).getDetails(); + doReturn("correctHash").when(backupDetailVoMock).getValue(); + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(any(), any()); + verify(backupDaoMock, Mockito.never()).update(anyLong(), any()); + } + + @Test + public void validateWithHashTestSameHash() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupDeltaTOList(backupId); + doReturn(List.of(hostVOMock)).when(hostDaoMock).listAllHostsUpByZoneAndHypervisor(anyLong(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn("correctHash").when(answerMock).getDetails(); + doReturn("correctHash").when(backupDetailVoMock).getValue(); + + boolean result = kbossBackupProviderSpy.validateWithHash(backupId, backupVoMock, backupDetailVoMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy, never()).setBackupAsInvalidAndSendAlert(any(), any()); + verify(kbossBackupProviderSpy, never()).setBackupUnableToValidateAndSendAlert(any(), any()); + verify(backupDaoMock).update(anyLong(), any()); + } + + + @Test + public void validateWithValidationVmTestValidationVmIsNull() { + doReturn(null).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).cleanupValidation(anyBoolean(), any(), any(), any()); + } + + @Test + public void validateWithValidationVmTestPrepareForValidationFails() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(false).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).cleanupValidation(eq(false), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestValidateBackupFails() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(2L).when(userVmVOMock).getHostId(); + doReturn(Hypervisor.HypervisorType.KVM).when(userVmVOMock).getHypervisorType(); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(true).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + doReturn(hypervisorGuruMock).when(hypervisorGuruManagerMock).getGuru(any()); + doReturn(virtualMachineToMock).when(hypervisorGuruMock).implement(any()); + doReturn(false).when(kbossBackupProviderSpy).validateBackup(anyLong(), any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).endBackupChainIfConfigured(backupVoMock); + verify(kbossBackupProviderSpy).cleanupValidation(eq(true), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestSuccessfulValidation() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(2L).when(userVmVOMock).getHostId(); + doReturn(Hypervisor.HypervisorType.KVM).when(userVmVOMock).getHypervisorType(); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(2L).when(hostVOMock).getId(); + doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doNothing().when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(Set.of()).when(kbossBackupProviderSpy).generateBackupAndVolumePairsToRestore(any(), any(), any(), eq(false)); + doReturn(true).when(kbossBackupProviderSpy).prepareForValidation(anyLong(), any(), any(), any()); + doReturn(hypervisorGuruMock).when(hypervisorGuruManagerMock).getGuru(any()); + doReturn(virtualMachineToMock).when(hypervisorGuruMock).implement(any()); + doReturn(true).when(kbossBackupProviderSpy).validateBackup(anyLong(), any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).calculateAndSaveHash(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).calculateAndSaveHash(any(), eq(backupVoMock), anyLong()); + verify(kbossBackupProviderSpy).cleanupValidation(eq(true), eq(userVmVOMock), eq(backupVoMock), any()); + } + + @Test + public void validateWithValidationVmTestExceptionHandling() throws NoTransitionException { + doReturn(userVmVOMock).when(kbossBackupProviderSpy).allocateValidationVm(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(anyLong()); + doReturn(List.of()).when(volumeDaoMock).findByInstance(anyLong()); + doThrow(new RuntimeException("boom")).when(kbossBackupProviderSpy).createValidationVolumesOnPrimaryStorage(any(), any(), any(), any(), any()); + doNothing().when(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(any(), any()); + + boolean result = kbossBackupProviderSpy.validateWithValidationVm(backupId, 2L, backupVoMock); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupUnableToValidateAndSendAlert(eq(backupVoMock), contains("boom")); + verify(kbossBackupProviderSpy).cleanupValidation(eq(false), eq(userVmVOMock), eq(backupVoMock), any()); + } + + + @Test + public void setBackupAsIsolatedTestPersistIsolatedDetail() { + kbossBackupProviderSpy.setBackupAsIsolated(backupVoMock); + verify(backupDetailDaoMock, Mockito.times(1)).persist(any()); + } + + @Test + public void endBackupChainIfConfiguredTestFeatureDisabled() { + doReturn(false).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, never()).endBackupChain(any()); + } + + @Test + public void endBackupChainIfConfiguredTestNotCurrentAndNoCurrentChildren() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(false).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + InternalBackupJoinVO child = mock(InternalBackupJoinVO.class); + doReturn(false).when(child).getCurrent(); + doReturn(List.of(child)).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any(), any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, never()).endBackupChain(any()); + } + + @Test + public void endBackupChainIfConfiguredTestBackupIsCurrent() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(true).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + doReturn(userVmVOMock).when(userVmDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any()); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any(), any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, times(1)).endBackupChain(userVmVOMock); + } + + @Test + public void endBackupChainIfConfiguredTestLastChildIsCurrent() { + doReturn(true).when(kbossBackupProviderSpy).getValidationEndChainOnFail(backupVoMock); + doReturn(false).when(internalBackupJoinVoMock).getCurrent(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + InternalBackupJoinVO child = mock(InternalBackupJoinVO.class); + doReturn(true).when(child).getCurrent(); + doReturn(List.of(child)).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); + doReturn(userVmVOMock).when(userVmDaoMock).findByIdIncludingRemoved(anyLong()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any()); + doNothing().when(kbossBackupProviderSpy).validateVmState(any(), any(), any(), any()); + + kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); + + verify(kbossBackupProviderSpy, times(1)).endBackupChain(userVmVOMock); + } + + + @Test + public void normalizeBackupErrorAndFinishChainTestAnswerNull() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertFalse(result); + } + + @Test + public void normalizeBackupErrorAndFinishChainTestAnswerFailed() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(false).when(answerMock).getResult(); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertFalse(result); + } + + @Test + public void normalizeBackupErrorAndFinishChainTestSuccessCallsEndChain() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(false).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertTrue(result); + verify(kbossBackupProviderSpy).endBackupChain(userVmVOMock); + } + + + @Test + public void normalizeBackupErrorAndFinishChainTestChainAlreadyEnded() { + doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); + doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); + doReturn(mock(ImageStoreVO.class)).when(imageStoreDaoMock).findById(anyLong()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(anyLong()); + long parentId = 9382; + doReturn(parentId).when(internalBackupJoinVoMock).getParentId(); + doReturn(null).when(internalBackupJoinDaoMock).findById(parentId); + doReturn(List.of()).when(internalBackupDataStoreDaoMock).listByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(true).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any()); + InternalBackupJoinVO current = mock(InternalBackupJoinVO.class); + doReturn(current).when(internalBackupJoinDaoMock).findCurrent(anyLong()); + doNothing().when(internalBackupStoragePoolDaoMock).expungeByBackupId(anyLong()); + doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + + boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); + + assertTrue(result); + verify(internalBackupStoragePoolDaoMock).expungeByBackupId(anyLong()); + verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + } + + @Test + public void cleanupValidationTestStartedVmFalseAndValidationNotPreparedAndFailedToDestroyVolume() throws ResourceUnavailableException { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(52L).when(dataVolume).getId(); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(false, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(userVmManagerMock, Mockito.never()).stopVirtualMachine(anyLong(), anyBoolean()); + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(volumeApiServiceMock, Mockito.times(1)).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + verify(agentManagerMock, Mockito.never()).easySend(anyLong(), any()); + } + } + + @Test + public void cleanupValidationTestDestroyVmThrowsAndCleanupMailIsSent() throws ResourceUnavailableException { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doThrow(new RuntimeException("boom")).when(userVmManagerMock).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(kbossBackupProviderSpy, Mockito.times(1)).sendCleanupFailedEmail(eq(backupVoMock), contains("Got an unexpected exception while trying to destroy validation VM.")); + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + } + } + + @Test + public void cleanupValidationTestCleanupCommandFails() { + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doReturn(null).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(hostVOMock).when(hostDaoMock).findById(88L); + doNothing().when(kbossBackupProviderSpy).sendCleanupFailedEmail(any(), any()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(dataVolume)); + + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + verify(hostDaoMock, Mockito.times(1)).findById(88L); + verify(kbossBackupProviderSpy, times(1)).sendCleanupFailedEmail(any(), any()); + } + } + + @Test + public void cleanupValidationTestNoEmailSentAndAllStepsExecuted() throws ResourceUnavailableException { + VolumeVO rootVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.ROOT).when(rootVolume).getVolumeType(); + + VolumeVO dataVolume = Mockito.mock(VolumeVO.class); + doReturn(Volume.Type.DATADISK).when(dataVolume).getVolumeType(); + doReturn(52L).when(dataVolume).getId(); + + doReturn(77L).when(userVmVOMock).getId(); + doReturn(88L).when(userVmVOMock).getHostId(); + doReturn(Set.of("secondary-storage-1")).when(kbossBackupProviderSpy).getSecondaryStorageUrls(userVmVOMock); + doReturn(answerMock).when(agentManagerMock).easySend(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(volumeVoMock).when(volumeApiServiceMock).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + + try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + kbossBackupProviderSpy.cleanupValidation(true, userVmVOMock, backupVoMock, List.of(rootVolume, dataVolume)); + + verify(userVmManagerMock, Mockito.times(1)).stopVirtualMachine(77L, true); + verify(userVmManagerMock, Mockito.times(1)).destroyVm(any(DestroyVMCmd.class), Mockito.eq(false)); + verify(volumeApiServiceMock, Mockito.times(1)).destroyVolume(Mockito.eq(52L), any(), Mockito.eq(true), Mockito.eq(true), Mockito.isNull()); + verify(agentManagerMock, Mockito.times(1)).easySend(Mockito.eq(88L), any(CleanupKbossValidationCommand.class)); + verify(kbossBackupProviderSpy, Mockito.never()).sendCleanupFailedEmail(any(), any()); + verify(hostDaoMock, Mockito.never()).findById(anyLong()); + } + } + + @Test + public void getVolumesToConsolidateTestSameVmAsBackupFalse() { + VolumeObjectTO volumeObjectTO1 = Mockito.mock(VolumeObjectTO.class); + doReturn(11L).when(volumeObjectTO1).getVolumeId(); + VolumeInfo volumeInfo1 = Mockito.mock(VolumeInfo.class); + doReturn(volumeVoMock).when(volumeInfo1).getVolume(); + doReturn(volumeInfo1).when(volumeDataFactoryMock).getVolume(11L); + + VolumeObjectTO volumeObjectTO2 = Mockito.mock(VolumeObjectTO.class); + doReturn(22L).when(volumeObjectTO2).getVolumeId(); + VolumeInfo volumeInfo2 = Mockito.mock(VolumeInfo.class); + doReturn(Mockito.mock(VolumeVO.class)).when(volumeInfo2).getVolume(); + doReturn(volumeInfo2).when(volumeDataFactoryMock).getVolume(22L); + + doNothing().when(kbossBackupProviderSpy).transitVmState(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).transitVolumeState(any(), any()); + + List result = kbossBackupProviderSpy.getVolumesToConsolidate(virtualMachineMock, List.of(), List.of(volumeObjectTO1, volumeObjectTO2), 99L, false); + + assertEquals(List.of(volumeInfo1, volumeInfo2), result); + verify(kbossBackupProviderSpy, times(1)).transitVmState(virtualMachineMock, VirtualMachine.Event.RestoringSuccess, 99L); + verify(volumeDataFactoryMock, times(1)).getVolume(11L); + verify(volumeDataFactoryMock, times(1)).getVolume(22L); + verify(kbossBackupProviderSpy, times(2)).transitVolumeState(any(), eq(Volume.Event.RestoreSucceeded)); + } + + @Test + public void getVolumesToConsolidateTestSameVmAsBackupTrueFiltersBySecondaryDeltas() { + VolumeObjectTO volumeObjectTO1 = Mockito.mock(VolumeObjectTO.class); + doReturn(11L).when(volumeObjectTO1).getVolumeId(); + VolumeInfo volumeInfo1 = Mockito.mock(VolumeInfo.class); + doReturn(volumeVoMock).when(volumeInfo1).getVolume(); + doReturn(volumeInfo1).when(volumeDataFactoryMock).getVolume(11L); + + VolumeObjectTO volumeObjectTO2 = Mockito.mock(VolumeObjectTO.class); + doReturn(22L).when(volumeObjectTO2).getVolumeId(); + VolumeInfo volumeInfo2 = Mockito.mock(VolumeInfo.class); + doReturn(Mockito.mock(VolumeVO.class)).when(volumeInfo2).getVolume(); + doReturn(volumeInfo2).when(volumeDataFactoryMock).getVolume(22L); + + InternalBackupDataStoreVO delta = Mockito.mock(InternalBackupDataStoreVO.class); + doReturn(33L).when(delta).getVolumeId(); + + doNothing().when(kbossBackupProviderSpy).transitVmState(any(), any(), anyLong()); + doNothing().when(kbossBackupProviderSpy).transitVolumeState(any(), any()); + + List result = kbossBackupProviderSpy.getVolumesToConsolidate(virtualMachineMock, List.of(delta), List.of(volumeObjectTO1, volumeObjectTO2), 99L, true); + + assertEquals(List.of(), result); + verify(kbossBackupProviderSpy, times(1)).transitVmState(virtualMachineMock, VirtualMachine.Event.RestoringSuccess, 99L); + verify(volumeDataFactoryMock, times(1)).getVolume(11L); + verify(volumeDataFactoryMock, times(1)).getVolume(22L); + verify(kbossBackupProviderSpy, times(2)).transitVolumeState(any(), eq(Volume.Event.RestoreSucceeded)); + } + + @Test + public void checkErrorBackupTestNonErrorStatusDoesNothing() { + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock, never()).setStatus(Backup.Status.Failed); + } + + @Test + public void checkErrorBackupTestErrorStatusAndVmIsNullSetsBackupFailed() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, null); + + verify(backupVoMock).setStatus(Backup.Status.Failed); + } + + @Test + public void checkErrorBackupTestErrorStatusAndVmNotBackupErrorSetsBackupFailed() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + doReturn(VirtualMachine.State.Running).when(virtualMachineMock).getState(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock).setStatus(Backup.Status.Failed); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkErrorBackupTestErrorStatusAndVmInBackupErrorThrows() { + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + doReturn(VirtualMachine.State.BackupError).when(virtualMachineMock).getState(); + + kbossBackupProviderSpy.checkErrorBackup(backupVoMock, virtualMachineMock); + + verify(backupVoMock, never()).setStatus(Backup.Status.Failed); + } + + @Test + public void deleteFailedBackupTestFailedBackupIsExpungedAndCleanedUp() { + long backupId = 123L; + doReturn(backupId).when(backupVoMock).getId(); + doReturn(Backup.Status.Failed).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.deleteFailedBackup(backupVoMock); + + assertTrue(result); + verify(backupVoMock).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(internalBackupStoragePoolDaoMock).expungeByBackupId(backupId); + verify(internalBackupDataStoreDaoMock).expungeByBackupId(backupId); + verify(backupDetailDaoMock).removeDetails(backupId); + } + + @Test + public void deleteFailedBackupTestNonFailedBackupDoesNothing() { + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.deleteFailedBackup(backupVoMock); + + assertFalse(result); + verify(backupVoMock, never()).setStatus(Backup.Status.Expunged); + verify(backupDaoMock, never()).update(anyLong(), any()); + verify(internalBackupStoragePoolDaoMock, never()).expungeByBackupId(anyLong()); + verify(internalBackupDataStoreDaoMock, never()).expungeByBackupId(anyLong()); + verify(backupDetailDaoMock, never()).removeDetails(anyLong()); + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestNoDeltaDoesNothing() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(null).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); + + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(internalBackupStoragePoolDaoMock, times(1)).findOneByVolumeId(volumeId); + verify(internalBackupJoinDaoMock, never()).findById(anyLong()); + } + + @Test (expected = CloudRuntimeException.class) + public void mergeCurrentDeltaIntoVolumeTestNullAnswer() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); + doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(null).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any()); + doReturn(null).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(kbossBackupProviderSpy, never()).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); + } + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestNoSucceedingSnapshot() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); + doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(null).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doReturn(volumeVoMock).when(volumeDaoMock).findById(anyLong()); + doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); + doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); + doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); + verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); + } + } + + @Test + public void mergeCurrentDeltaIntoVolumeTestWithSucceedingSnapshotWithMoreDeltas() { + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); + doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(backupId); + doReturn(vmSnapshotVoMock).when(kbossBackupProviderSpy).getSucceedingVmSnapshot(internalBackupJoinVoMock); + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any()); + doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + doReturn(true).when(answerMock).getResult(); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); + doReturn(List.of(internalBackupStoragePoolVoMock)).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); + + try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { + when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); + + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + + verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); + verify(volumeDaoMock, never()).update(volumeId, volumeVoMock); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); + verify(kbossBackupProviderSpy, never()).setEndOfChainAndRemoveCurrentForBackup(any()); + } + } + + @Test + public void getHostToRestoreTestNonQuickRestoreUsesRunningHost() throws AgentUnavailableException { + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(77L).when(vmSnapshotHelperMock).pickRunningHost(vmId); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(77L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, false, null); + + assertEquals(hostVOMock, result); + verify(vmSnapshotHelperMock, times(1)).pickRunningHost(vmId); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(77L); + } + + @Test + public void getHostToRestoreTestQuickRestoreUsesProvidedHostId() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Enabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + + assertEquals(hostVOMock, result); + verify(vmSnapshotHelperMock, never()).pickRunningHost(anyLong()); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(55L); + } + + @Test + public void getHostToRestoreTestQuickRestoreUsesVmLastHostIdWhenHostIdIsNull() throws AgentUnavailableException { + doReturn(99L).when(virtualMachineMock).getLastHostId(); + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Enabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(99L); + + HostVO result = kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, null); + + assertEquals(hostVOMock, result); + verify(hostDaoMock, times(1)).findByIdIncludingRemoved(99L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithNoHostIdAndNoLastHostThrows() throws AgentUnavailableException { + doReturn(null).when(virtualMachineMock).getLastHostId(); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, null); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostDownThrows() throws AgentUnavailableException { + doReturn(Status.Down).when(hostVOMock).getStatus(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostInMaintenanceThrows() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(true).when(hostVOMock).isInMaintenanceStates(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test(expected = AgentUnavailableException.class) + public void getHostToRestoreTestQuickRestoreWithHostDisabledThrows() throws AgentUnavailableException { + doReturn(Status.Up).when(hostVOMock).getStatus(); + doReturn(false).when(hostVOMock).isInMaintenanceStates(); + doReturn(ResourceState.Disabled).when(hostVOMock).getResourceState(); + doReturn(hostVOMock).when(hostDaoMock).findByIdIncludingRemoved(55L); + + kbossBackupProviderSpy.getHostToRestore(virtualMachineMock, true, 55L); + } + + @Test + public void gatherSnapshotReferencesOfChildrenSnapshotTestVmSnapshotIsNull() { + List volumeObjectTOs = List.of(volumeObjectToMock); + + Map> result = kbossBackupProviderSpy.gatherSnapshotReferencesOfChildrenSnapshot(volumeObjectTOs, null); + + assertTrue(result.isEmpty()); + verify(vmSnapshotDaoMock, never()).listByParent(anyLong()); + verify(vmSnapshotHelperMock, never()).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(anyLong()); + verify(kbossBackupProviderSpy, never()).mapVolumesToSnapshotReferences(anyList(), anyList(), anyMap()); + } + + @Test + public void gatherSnapshotReferencesOfChildrenSnapshotTestChildrenListIsEmpty() { + doReturn(100L).when(vmSnapshotVoMock).getId(); + doReturn(List.of()).when(vmSnapshotDaoMock).listByParent(100L); + + List volumeObjectTOs = List.of(volumeObjectToMock); + + Map> result = kbossBackupProviderSpy.gatherSnapshotReferencesOfChildrenSnapshot(volumeObjectTOs, vmSnapshotVoMock); + + assertTrue(result.isEmpty()); + verify(vmSnapshotDaoMock, times(1)).listByParent(100L); + verify(vmSnapshotHelperMock, never()).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(anyLong()); + verify(kbossBackupProviderSpy, never()).mapVolumesToSnapshotReferences(anyList(), anyList(), anyMap()); + } + + @Test + public void gatherSnapshotReferencesOfChildrenSnapshotTestSingleChildWithSingleSnapshotReference() { + VMSnapshotVO childSnapshot = Mockito.mock(VMSnapshotVO.class); + SnapshotDataStoreVO snapshotDataStoreVO = Mockito.mock(SnapshotDataStoreVO.class); + + doReturn(100L).when(vmSnapshotVoMock).getId(); + doReturn(List.of(childSnapshot)).when(vmSnapshotDaoMock).listByParent(100L); + doReturn(200L).when(childSnapshot).getId(); + doReturn(List.of(snapshotDataStoreVO)).when(vmSnapshotHelperMock).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(200L); + doNothing().when(kbossBackupProviderSpy).mapVolumesToSnapshotReferences(anyList(), anyList(), anyMap()); + + List volumeObjectTOs = List.of(volumeObjectToMock); + + Map> result = + kbossBackupProviderSpy.gatherSnapshotReferencesOfChildrenSnapshot(volumeObjectTOs, vmSnapshotVoMock); + + assertTrue(result.isEmpty()); + verify(vmSnapshotDaoMock, times(1)).listByParent(100L); + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(200L); + verify(kbossBackupProviderSpy, times(1)).mapVolumesToSnapshotReferences(eq(volumeObjectTOs), anyList(), anyMap()); + } + + @Test + public void gatherSnapshotReferencesOfChildrenSnapshotTestMultipleChildrenAggregatesSnapshotReferences() { + VMSnapshotVO childSnapshot1 = Mockito.mock(VMSnapshotVO.class); + VMSnapshotVO childSnapshot2 = Mockito.mock(VMSnapshotVO.class); + SnapshotDataStoreVO snapshotDataStoreVO1 = Mockito.mock(SnapshotDataStoreVO.class); + SnapshotDataStoreVO snapshotDataStoreVO2 = Mockito.mock(SnapshotDataStoreVO.class); + + doReturn(100L).when(vmSnapshotVoMock).getId(); + doReturn(List.of(childSnapshot1, childSnapshot2)).when(vmSnapshotDaoMock).listByParent(100L); + doReturn(201L).when(childSnapshot1).getId(); + doReturn(202L).when(childSnapshot2).getId(); + doReturn(List.of(snapshotDataStoreVO1)).when(vmSnapshotHelperMock) + .getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(201L); + doReturn(List.of(snapshotDataStoreVO2)).when(vmSnapshotHelperMock) + .getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(202L); + doNothing().when(kbossBackupProviderSpy).mapVolumesToSnapshotReferences(anyList(), anyList(), anyMap()); + + List volumeObjectTOs = List.of(volumeObjectToMock); + + Map> result = + kbossBackupProviderSpy.gatherSnapshotReferencesOfChildrenSnapshot(volumeObjectTOs, vmSnapshotVoMock); + + assertTrue(result.isEmpty()); + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(201L); + verify(vmSnapshotHelperMock, times(1)).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(202L); + verify(kbossBackupProviderSpy, times(1)).mapVolumesToSnapshotReferences(eq(volumeObjectTOs), anyList(), anyMap()); + } + + @Test + public void createDeltaMergeTreeTestChildIsVolumeWithoutSucceedingSnapshot() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(true, true, internalBackupStoragePoolVoMock, + volumeObjectToMock, null); + + assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); + assertTrue(result.getGrandChildren().isEmpty()); + assertEquals(volumeObjectToMock, result.getChild()); + assertEquals("parent-path", result.getParent().getPath()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithoutSucceedingSnapshot() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, true, internalBackupStoragePoolVoMock, + volumeObjectToMock, null); + + assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); + assertEquals("parent-path", result.getParent().getPath()); + assertEquals("child-path", result.getChild().getPath()); + assertTrue(result.getGrandChildren().isEmpty()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotReferences() { + SnapshotDataStoreVO snapshotRefMock = Mockito.mock(SnapshotDataStoreVO.class); + + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("snapshot-grandchild").when(snapshotRefMock).getInstallPath(); + + doReturn(Map.of(volumeId, List.of(snapshotRefMock))).when(kbossBackupProviderSpy).gatherSnapshotReferencesOfChildrenSnapshot(List.of(volumeObjectToMock), vmSnapshotVoMock); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, true, internalBackupStoragePoolVoMock, + volumeObjectToMock, vmSnapshotVoMock); + + assertEquals("child-path", result.getChild().getPath()); + assertEquals(1, result.getGrandChildren().size()); + assertEquals("snapshot-grandchild", result.getGrandChildren().get(0).getPath()); + } + + @Test + public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotButNoReferencesRebasesToVolumePath() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn("/volume/path").when(volumeObjectToMock).getPath(); + + doReturn(Map.of()).when(kbossBackupProviderSpy) + .gatherSnapshotReferencesOfChildrenSnapshot(List.of(volumeObjectToMock), vmSnapshotVoMock); + + DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, false, internalBackupStoragePoolVoMock, + volumeObjectToMock, vmSnapshotVoMock); + + assertEquals(1, result.getGrandChildren().size()); + assertEquals("/volume/path", result.getGrandChildren().get(0).getPath()); + } + + @Test + public void generateBackupAndVolumePairsToRestoreTestSameVmAsBackupMatchesByVolumeId() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(10L).when(internalBackupDataStoreVoMock).getVolumeId(); + doReturn("delta-path").when(internalBackupDataStoreVoMock).getBackupPath(); + doReturn(10L).when(volumeObjectToMock).getVolumeId(); + + Set> result = kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), + List.of(volumeObjectToMock), internalBackupJoinVoMock, true); + + assertEquals(1, result.size()); + Pair pair = result.iterator().next(); + assertEquals(volumeObjectToMock, pair.second()); + assertEquals("delta-path", pair.first().getPath()); + } + + @Test + public void generateBackupAndVolumePairsToRestoreTestDifferentVmMatchesByDeviceId() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(5L).when(internalBackupDataStoreVoMock).getDeviceId(); + doReturn("delta-path").when(internalBackupDataStoreVoMock).getBackupPath(); + doReturn(5L).when(volumeObjectToMock).getDeviceId(); + + Set> result = kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), + List.of(volumeObjectToMock), internalBackupJoinVoMock, false); + + assertEquals(1, result.size()); + Pair pair = result.iterator().next(); + assertEquals(volumeObjectToMock, pair.second()); + assertEquals("delta-path", pair.first().getPath()); + } + + @Test(expected = CloudRuntimeException.class) + public void generateBackupAndVolumePairsToRestoreTestThrowsWhenNoMatchingVolumeExists() { + doReturn(77L).when(internalBackupJoinVoMock).getImageStoreId(); + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(77L, DataStoreRole.Image); + doReturn(10L).when(internalBackupDataStoreVoMock).getVolumeId(); + doReturn(123L).when(internalBackupDataStoreVoMock).getId(); + + kbossBackupProviderSpy.generateBackupAndVolumePairsToRestore(List.of(internalBackupDataStoreVoMock), List.of(volumeObjectToMock), internalBackupJoinVoMock, true); + } + + @Test + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestVolumeIsPartOfBackupAddsDeltaToRemoveAndUpdatesPath() { + doReturn(dataStoreMock).when(dataStoreManagerMock).getDataStore(11L, DataStoreRole.Primary); + doReturn(10L).when(internalBackupStoragePoolVoMock).getVolumeId(); + doReturn(11L).when(internalBackupStoragePoolVoMock).getStoragePoolId(); + doReturn("delta-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); + doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + doReturn(10L).when(volumeObjectToMock).getVolumeId(); + + Set deltasToRemove = new java.util.HashSet<>(); + + List result = kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), deltasToRemove, + List.of(volumeObjectToMock), List.of(), "vm-uuid"); + + assertTrue(result.isEmpty()); + assertEquals(1, deltasToRemove.size()); + verify(volumeObjectToMock, times(1)).setPath("parent-path"); + verify(dataStoreManagerMock, times(1)).getDataStore(11L, DataStoreRole.Primary); + } + + @Test + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestVolumeIsPartOfBackupCreatesMergeTree() { + doReturn(volumeId).when(internalBackupStoragePoolVoMock).getVolumeId(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + + Set deltasToRemove = new java.util.HashSet<>(); + + doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(true, false, internalBackupStoragePoolVoMock, volumeObjectToMock, null); + + List result = kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), deltasToRemove, + List.of(volumeObjectToMock), List.of(volumeObjectToMock), "vm-uuid"); + + assertEquals(List.of(deltaMergeTreeToMock), result); + assertTrue(deltasToRemove.isEmpty()); + verify(kbossBackupProviderSpy, times(1)).createDeltaMergeTree(true, false, internalBackupStoragePoolVoMock, volumeObjectToMock, null); + verify(dataStoreManagerMock, never()).getDataStore(anyLong(), eq(DataStoreRole.Primary)); + } + + @Test(expected = CloudRuntimeException.class) + public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestThrowsWhenNoMatchingVolumeExists() { + kbossBackupProviderSpy.populateDeltasToRemoveAndToMergeAndUpdateVolumePaths(List.of(internalBackupStoragePoolVoMock), Set.of(), List.of(), List.of(), "vm-uuid"); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestUpdatesPathAndSizeWhenVolumePathChanged() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("old-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("new-path").when(volumeObjectToMock).getPath(); + doReturn(150L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), false); + + verify(volumeVoMock).setPath("new-path"); + verify(volumeVoMock).setSize(150L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestUsesMergeTreeParentPathWhenVolumePathDidNotChange() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn(volumeId).when(volumeObjectToMock).getId(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn(150L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + doReturn(volumeObjectToMock).when(deltaMergeTreeToMock).getChild(); + doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + doReturn("parent-path").when(backupDeltaToMock).getPath(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(deltaMergeTreeToMock), + false); + + verify(volumeVoMock).setPath("parent-path"); + verify(volumeVoMock).setSize(150L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestLeavesSizeUntouchedWhenRestoreSizeMatchesCurrentSize() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn(5L).when(volumeVoMock).getDeviceId(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn(100L).when(backupVolumeInfoMock).getSize(); + doReturn(5L).when(backupVolumeInfoMock).getDeviceId(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), false); + + verify(volumeVoMock, never()).setSize(anyLong()); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + @Test + public void updateVolumePathsAndSizeIfNeededTestMatchesByUuidWhenSameVmAsBackup() { + doReturn(List.of(volumeVoMock)).when(volumeDaoMock).findByInstance(vmId); + doReturn(vmId).when(virtualMachineMock).getId(); + doReturn(volumeId).when(volumeVoMock).getId(); + doReturn("same-path").when(volumeVoMock).getPath(); + doReturn("vm-uuid").when(volumeVoMock).getUuid(); + doReturn(100L).when(volumeVoMock).getSize(); + doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); + doReturn("same-path").when(volumeObjectToMock).getPath(); + doReturn("vm-uuid").when(backupVolumeInfoMock).getUuid(); + doReturn(120L).when(backupVolumeInfoMock).getSize(); + + kbossBackupProviderSpy.updateVolumePathsAndSizeIfNeeded(virtualMachineMock, List.of(volumeObjectToMock), List.of(backupVolumeInfoMock), List.of(), true); + + verify(volumeVoMock).setSize(120L); + verify(volumeDaoMock).update(volumeId, volumeVoMock); + } + + + @Test + public void processRemoveBackupFailuresTestNoFailuresReturnsTrueAndRemovesNothing() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + Answer[] deleteAnswers = new Answer[] {answerMock}; + doReturn(true).when(answerMock).getResult(); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, deleteAnswers, removedBackupIds, internalBackupJoinVoMock); + + assertTrue(result); + assertEquals(List.of(backupId, 200L), removedBackupIds); + verify(backupDaoMock, never()).findByIdIncludingRemoved(anyLong()); + verify(backupDaoMock, never()).update(anyLong(), any()); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnCurrentBackupNotForcedSetsError() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + BackupDeleteAnswer failedCurrentBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedCurrentBackupAnswer).getResult(); + doReturn(backupId).when(failedCurrentBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedCurrentBackupAnswer).getDetails(); + + doReturn(backupId).when(backupVoMock).getId(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); + + assertFalse(result); + assertEquals(List.of(200L), removedBackupIds); + verify(backupVoMock).setStatus(Backup.Status.Error); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(backupDaoMock, never()).findByIdIncludingRemoved(200L); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnCurrentBackupForcedSetBackupAsExpunged() { + BackupDeleteAnswer failedCurrentBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedCurrentBackupAnswer).getResult(); + doReturn(backupId).when(failedCurrentBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedCurrentBackupAnswer).getDetails(); + doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(true, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); + + assertFalse(result); + assertEquals(List.of(200L), removedBackupIds); + verify(backupVoMock).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(backupDaoMock, never()).findByIdIncludingRemoved(200L); + } + + @Test + public void processRemoveBackupFailuresTestFailureOnOtherBackupMarksItExpunged() { + doReturn(backupId).when(internalBackupJoinVoMock).getId(); + + BackupDeleteAnswer failedOtherBackupAnswer = Mockito.mock(BackupDeleteAnswer.class); + doReturn(false).when(failedOtherBackupAnswer).getResult(); + doReturn(200L).when(failedOtherBackupAnswer).getBackupId(); + doReturn("delete failed").when(failedOtherBackupAnswer).getDetails(); + + BackupVO failedBackup = Mockito.mock(BackupVO.class); + doReturn(failedBackup).when(backupDaoMock).findByIdIncludingRemoved(200L); + + List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); + + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedOtherBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); + + assertFalse(result); + assertEquals(List.of(backupId), removedBackupIds); + verify(failedBackup).setStatus(Backup.Status.Expunged); + verify(backupDaoMock).update(200L, failedBackup); + verify(backupDaoMock, never()).findByIdIncludingRemoved(backupId); + } + + @Test + public void processValidationAnswerTestNullAnswer() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(null, backupVoMock, userVmVOMock, hostVOMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("Null answer from host")); + } + + @Test + public void processValidationAnswerTestFalseAnswer() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + doReturn(false).when(answerMock).getResult(); + doReturn("fail-reason").when(answerMock).getDetails(); + boolean result = kbossBackupProviderSpy.processValidationAnswer(answerMock, backupVoMock, userVmVOMock, hostVOMock, null); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("fail-reason")); + } + + @Test + public void processValidationAnswerTestBootNotValidated() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + } + + @Test + public void processValidationAnswerTestBootNotValidatedScriptResultFalse() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + doReturn("false").when(answer).getScriptResult(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The script did not output the expected output.")); + } + + @Test + public void processValidationAnswerTestBootNotValidatedScriptResultFalseScreenshotPathNull() { + doNothing().when(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), any()); + + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(false).when(answer).isBootValidated(); + doReturn("false").when(answer).getScriptResult(); + doReturn(null).when(answer).getScreenshotPath(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + doReturn(true).when(cmd).isTakeScreenshot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertFalse(result); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The VM did not boot within the expected time")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("The script did not output the expected output.")); + verify(kbossBackupProviderSpy).setBackupAsInvalidAndSendAlert(eq(backupVoMock), contains("We were unable to take a screenshot of the VM.")); + } + + @Test + public void processValidationAnswerTestBootValidatedScriptResultTrueScreenshotPathNotNull() { + ValidateKbossVmAnswer answer = Mockito.mock(ValidateKbossVmAnswer.class); + doReturn(true).when(answer).getResult(); + doReturn(true).when(answer).isBootValidated(); + doReturn(null).when(answer).getScriptResult(); + doReturn("snap-path").when(answer).getScreenshotPath(); + + ValidateKbossVmCommand cmd = Mockito.mock(ValidateKbossVmCommand.class); + doReturn(true).when(cmd).isWaitForBoot(); + doReturn(true).when(cmd).isExecuteScript(); + doReturn(true).when(cmd).isTakeScreenshot(); + + boolean result = kbossBackupProviderSpy.processValidationAnswer(answer, backupVoMock, userVmVOMock, hostVOMock, cmd); + + assertTrue(result); + verify(kbossBackupProviderSpy, never()).setBackupAsInvalidAndSendAlert(any(), any()); + verify(backupDetailDaoMock).addDetail(eq(backupId), eq(BackupDetailsDao.SCREENSHOT_PATH), eq("snap-path"), eq(false)); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestNoParents() { + doReturn(List.of()).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, new Commands(Command.OnError.Stop)); + + assertEquals(List.of(), result.first()); + assertNull(result.second()); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestAliveParents() { + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, new Commands(Command.OnError.Stop)); + + assertEquals(List.of(), result.first()); + assertEquals(internalBackupJoinVoMock, result.second()); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestDeadParents() { + InternalBackupJoinVO deadParent = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(deadParent).getStatus(); + doReturn(backupId).when(deadParent).getId(); + + doReturn(List.of(deadParent)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + Commands commands = new Commands(Command.OnError.Stop); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, commands); + + assertEquals(List.of(deadParent), result.first()); + assertNull(result.second()); + verify(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(backupId, commands); + } + + @Test + public void getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommandsTestDeadAndAliveParents() { + InternalBackupJoinVO deadParent = Mockito.mock(InternalBackupJoinVO.class); + doReturn(Backup.Status.Removed).when(deadParent).getStatus(); + doReturn(backupId).when(deadParent).getId(); + + doReturn(List.of(deadParent, internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinParents(backupVoMock, true); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + doReturn(null).when(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(anyLong(), any()); + Commands commands = new Commands(Command.OnError.Stop); + + Pair, InternalBackupJoinVO> result = + kbossBackupProviderSpy.getParentsToBeExpungedWithBackupAndAddThemToListOfDeleteCommands(backupVoMock, commands); + + assertEquals(List.of(deadParent), result.first()); + assertEquals(internalBackupJoinVoMock, result.second()); + verify(kbossBackupProviderSpy).addBackupDeltasToDeleteCommand(backupId, commands); + } + + @Test + public void configureValidationStepsTestScreenshot() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("screenshot").when(backupOfferingDetailsVoMock).getValue(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertTrue(cmd.isTakeScreenshot()); + assertFalse(cmd.isWaitForBoot()); + assertFalse(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestWaitForBoot() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("wait_for_boot").when(backupOfferingDetailsVoMock).getValue(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertFalse(cmd.isTakeScreenshot()); + assertTrue(cmd.isWaitForBoot()); + assertFalse(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestExecuteCommand() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("execute_command").when(backupOfferingDetailsVoMock).getValue(); + doReturn(vmInstanceDetailVoMock).when(vmInstanceDetailsDaoMock).findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + doReturn(vmId).when(backupVoMock).getVmId(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertFalse(cmd.isTakeScreenshot()); + assertFalse(cmd.isWaitForBoot()); + assertTrue(cmd.isExecuteScript()); + } + + @Test + public void configureValidationStepsTestAllSteps() { + long backupOfferingId = 32L; + + doReturn(backupOfferingId).when(backupVoMock).getBackupOfferingId(); + doReturn(backupOfferingId).when(backupOfferingMock).getId(); + doReturn(backupOfferingMock).when(backupOfferingDaoMock).findByIdIncludingRemoved(backupOfferingId); + doReturn(backupOfferingDetailsVoMock).when(backupOfferingDetailsDaoMock).findDetail(backupOfferingId, ApiConstants.VALIDATION_STEPS); + doReturn("screenshot,wait_for_boot,execute_command").when(backupOfferingDetailsVoMock).getValue(); + doReturn(vmInstanceDetailVoMock).when(vmInstanceDetailsDaoMock).findDetail(vmId, VmDetailConstants.VALIDATION_COMMAND); + doReturn(vmId).when(backupVoMock).getVmId(); + + ValidateKbossVmCommand cmd = new ValidateKbossVmCommand(null, null); + + kbossBackupProviderSpy.configureValidationSteps(cmd, backupVoMock); + + assertTrue(cmd.isTakeScreenshot()); + assertTrue(cmd.isWaitForBoot()); + assertTrue(cmd.isExecuteScript()); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertFalse(result.first()); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestFinalizingCompression() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.CompressionStatus.FinalizingCompression).when(backupVoMock).getCompressionStatus(); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertFalse(result.first()); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateCompressionStateForRestoreAndGetBackupTestValidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + + Pair result = kbossBackupProviderSpy.validateCompressionStateForRestoreAndGetBackup(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).getCompressionStatus(); + verify(backupVoMock).setStatus(Backup.Status.Restoring); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock, Mockito.atLeast(1)).getStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestCompressing() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressing).when(backupVoMock).getCompressionStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestValidating() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + doReturn(Backup.ValidationStatus.Validating).when(backupVoMock).getValidationStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertFalse(result); + verify(backupVoMock).getValidationStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRemovalTestValidStates() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(Backup.CompressionStatus.Compressed).when(backupVoMock).getCompressionStatus(); + doReturn(Backup.ValidationStatus.UnableToValidate).when(backupVoMock).getValidationStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForRemoval(backupId); + + assertTrue(result); + verify(backupVoMock).getStatus(); + verify(backupVoMock).getValidationStatus(); + verify(backupVoMock).getCompressionStatus(); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertFalse(result.first()); + verify(backupVoMock, Mockito.atLeastOnce()).getStatus(); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForStartCompressionAndUpdateCompressionStatusTestValidStates() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForStartCompressionAndUpdateCompressionStatus(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).getStatus(); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.Compressing); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRestoringBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRestoringChildBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinChildren(backupVoMock); + doReturn(Backup.Status.Restoring).when(internalBackupJoinVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestAllBackedUp() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + doReturn(List.of(internalBackupJoinVoMock)).when(kbossBackupProviderSpy).getBackupJoinChildren(backupVoMock); + doReturn(Backup.Status.BackedUp).when(internalBackupJoinVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.FinalizingCompression); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForFinalizeCompressionTestRemovedBackup() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForFinalizeCompression(backupId); + + assertTrue(result.first()); + assertEquals(backupVoMock, result.second()); + verify(backupVoMock).setCompressionStatus(Backup.CompressionStatus.CompressionError); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestBackedUp() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertTrue(result.first()); + assertEquals(Backup.Status.BackedUp, result.second()); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestRestoring() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Restoring).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertTrue(result.first()); + assertEquals(Backup.Status.Restoring, result.second()); + verify(backupDaoMock).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForRestoreBackupToVMTestError() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Error).when(backupVoMock).getStatus(); + + Pair result = kbossBackupProviderSpy.validateBackupStateForRestoreBackupToVM(backupId); + + assertFalse(result.first()); + verify(backupDaoMock, never()).update(backupId, backupVoMock); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + + @Test + public void validateBackupStateForValidationTestUnableToLock() { + doReturn(null).when(kbossBackupProviderSpy).lockBackup(backupId); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy, never()).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForValidationTestInvalidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.Removed).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertFalse(result); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + + @Test + public void validateBackupStateForValidationTestValidState() { + doReturn(backupVoMock).when(kbossBackupProviderSpy).lockBackup(backupId); + doReturn(Backup.Status.BackedUp).when(backupVoMock).getStatus(); + + boolean result = kbossBackupProviderSpy.validateBackupStateForValidation(backupId); + + assertTrue(result); + verify(kbossBackupProviderSpy).releaseBackup(backupId); + } + +} diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java new file mode 100644 index 000000000000..511f0ccb7114 --- /dev/null +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupChainKeys.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.backup; + +/** + * Keys used by the NAS backup provider when storing incremental-chain metadata + * in the existing {@code backup_details} key/value table. Stored here (not on + * the {@code backups} table) so other providers do not need a schema change to + * support their own incremental implementations. + */ +public final class NASBackupChainKeys { + + /** UUID of the parent backup (full or previous incremental). Empty for full backups. */ + public static final String PARENT_BACKUP_ID = "nas.parent_backup_id"; + + /** QEMU dirty-bitmap name created by this backup, used as the {@code } reference for the next one. */ + public static final String BITMAP_NAME = "nas.bitmap_name"; + + /** Identifier shared by every backup in the same chain (the full anchors a chain; its incrementals inherit the id). */ + public static final String CHAIN_ID = "nas.chain_id"; + + /** Position within the chain: 0 for the full, 1 for the first incremental, and so on. */ + public static final String CHAIN_POSITION = "nas.chain_position"; + + /** + * In-memory chain-mode sentinels used by {@code ChainDecision.mode}. The persisted + * full-vs-incremental backup type lives on the {@code backup.type} column (set in + * {@code takeBackup}) — single source of truth. Not duplicated into backup_details. + */ + public static final String TYPE_FULL = "full"; + public static final String TYPE_INCREMENTAL = "incremental"; + // Feature disabled: behave exactly like the pre-incremental full-only backup — no QEMU + // bitmap/checkpoint is created and no chain metadata is persisted. Matches nasbackup.sh's + // "legacy-full" mode token (which sets make_checkpoint=0). + public static final String TYPE_LEGACY_FULL = "legacy-full"; + + /** + * VM-scoped detail (stored in {@code vm_instance_details}) holding the QEMU dirty-bitmap + * name that currently exists on the running VM and is therefore the only valid parent + * for the next incremental backup. Written by {@link #BITMAP_NAME} on each successful + * backup; cleared on restore (the restored disk image has no bitmap, so the next backup + * must be a fresh full). When the VM has no detail, {@code decideChain} forces full. + */ + public static final String VM_ACTIVE_CHECKPOINT_ID = "nas.active_checkpoint_id"; + + private NASBackupChainKeys() { + } +} diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index a350d80d27fc..8e6d33c4e668 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -19,6 +19,7 @@ import com.cloud.agent.AgentManager; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.configuration.Resource; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -37,17 +38,21 @@ import com.cloud.storage.dao.VolumeDao; import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.GlobalLock; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.user.ResourceLimitService; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; - import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; @@ -70,7 +75,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -79,6 +83,8 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Configurable { private static final Logger LOG = LogManager.getLogger(NASBackupProvider.class); + private static final int CHAIN_LOCK_TIMEOUT_SECONDS = 300; + ConfigKey NASBackupRestoreMountTimeout = new ConfigKey<>("Advanced", Integer.class, "nas.backup.restore.mount.timeout", "30", @@ -86,6 +92,29 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co true, BackupFrameworkEnabled.key()); + ConfigKey NASBackupFullEvery = new ConfigKey<>("Advanced", Integer.class, + "nas.backup.full.every", + "10", + "Take a full NAS backup every Nth backup; remaining backups in between are incremental. " + + "Counts backups, not days, so it works for hourly, daily, and ad-hoc schedules. " + + "Set to 1 to disable incrementals (every backup is full).", + true, + ConfigKey.Scope.Zone, + BackupFrameworkEnabled.key()); + + ConfigKey NASBackupIncrementalEnabled = new ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", + "false", + "Master switch for NAS incremental backups. Defaults to false so existing zones keep the " + + "legacy full-only behavior on upgrade; opt in per-zone when ready to use chains. " + + "When false, every NAS backup is taken as a full regardless of nas.backup.full.every. " + + "Toggling this is safe at any time: switching off forces the next backup to be a fresh " + + "full anchor (existing chains stay restorable), switching back on resumes incrementals " + + "on the next full + incremental cycle.", + true, + ConfigKey.Scope.Zone, + BackupFrameworkEnabled.key()); + @Inject private BackupDao backupDao; @@ -107,6 +136,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private VMInstanceDao vmInstanceDao; + @Inject + private VMInstanceDetailsDao vmInstanceDetailsDao; + @Inject private PrimaryDataStoreDao primaryDataStoreDao; @@ -116,6 +148,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private AgentManager agentManager; + @Inject + private ResourceLimitService resourceLimitMgr; + @Inject private VMSnapshotDao vmSnapshotDao; @@ -131,6 +166,9 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private DiskOfferingDao diskOfferingDao; + @Inject + private BackupDetailsDao backupDetailsDao; + private Long getClusterIdFromRootVolume(VirtualMachine vm) { VolumeVO rootVolume = volumeDao.getInstanceRootVolume(vm.getId()); StoragePoolVO rootDiskPool = primaryDataStoreDao.findById(rootVolume.getPoolId()); @@ -169,6 +207,330 @@ protected Host getVMHypervisorHost(VirtualMachine vm) { return resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, vm.getDataCenterId()); } + /** + * Returned by {@link #decideChain(VirtualMachine)} to describe the next backup's place in + * the chain: full vs incremental, the bitmap name to create, and (for incrementals) the + * parent bitmap and parent file path. + */ + static final class ChainDecision { + final String mode; // "full" or "incremental" + final String bitmapNew; + final String bitmapParent; // null for full + // Per-volume parent backup file paths, one per current VM volume in deviceId order. + // null/empty for full. Each volume needs its own parent file because backup files + // are named after each volume's own UUID (root..qcow2 / datadisk..qcow2). + final List parentPaths; + final String chainId; // chain identifier this backup belongs to + final int chainPosition; // 0 for full, N for the Nth incremental in the chain + + private ChainDecision(String mode, String bitmapNew, String bitmapParent, List parentPaths, + String chainId, int chainPosition) { + this.mode = mode; + this.bitmapNew = bitmapNew; + this.bitmapParent = bitmapParent; + this.parentPaths = parentPaths; + this.chainId = chainId; + this.chainPosition = chainPosition; + } + + static ChainDecision fullStart(String bitmapName) { + return new ChainDecision(NASBackupChainKeys.TYPE_FULL, bitmapName, null, null, + UUID.randomUUID().toString(), 0); + } + + /** + * Decision used when the incremental feature is disabled: a plain full backup that + * creates no bitmap and carries no chain identity, so nothing chain/checkpoint-related + * is sent to the agent or persisted. Keeps the feature-off path byte-for-byte legacy. + */ + static ChainDecision legacyFull() { + return new ChainDecision(NASBackupChainKeys.TYPE_LEGACY_FULL, null, null, null, null, 0); + } + + static ChainDecision incremental(String bitmapNew, String bitmapParent, List parentPaths, + String chainId, int chainPosition) { + return new ChainDecision(NASBackupChainKeys.TYPE_INCREMENTAL, bitmapNew, bitmapParent, + parentPaths, chainId, chainPosition); + } + + boolean isIncremental() { + return NASBackupChainKeys.TYPE_INCREMENTAL.equals(mode); + } + + boolean isLegacyFull() { + return NASBackupChainKeys.TYPE_LEGACY_FULL.equals(mode); + } + } + + /** + * Decides whether the next backup for {@code vm} should be a fresh full or an incremental + * appended to the existing chain. Stopped VMs are always full (libvirt {@code backup-begin} + * requires a running QEMU process). The {@code nas.backup.full.every} ConfigKey controls + * how many backups (full + incrementals) form one chain before a new full is forced. + * + *

    The decision is anchored on the VM's {@code nas.active_checkpoint_id} detail, which + * records the bitmap that currently exists on the running QEMU. After a restore that + * detail is cleared, so the next backup is automatically full — even though there may be + * a more recent "last backup taken" row in the database. The decision deliberately avoids + * relying on "last backup taken", because that row is misleading after a restore.

    + */ + protected ChainDecision decideChain(VirtualMachine vm) { + // Master switch — when the operator disables incrementals at the zone level the backup + // behaves exactly like the pre-incremental full-only path: no bitmap is generated and no + // chain/checkpoint metadata is created, sent to the agent, or persisted (legacy-full). + Boolean incrementalEnabled = NASBackupIncrementalEnabled.valueIn(vm.getDataCenterId()); + if (incrementalEnabled == null || !incrementalEnabled) { + return ChainDecision.legacyFull(); + } + + // Incremental backups rely on QEMU dirty bitmaps / libvirt checkpoints, which only exist + // on file-based qcow2 storage. Storage such as Ceph-RBD and Linstor cannot carry per-disk + // checkpoints, so a VM with any volume on such a pool must stay on the full-only (legacy) + // path — otherwise an incremental attempt would fail or regress those storages. + if (!allVolumesOnCheckpointCapableStorage(vm)) { + return ChainDecision.legacyFull(); + } + + final String newBitmap = "backup-" + System.currentTimeMillis() / 1000L; + + // Stopped VMs cannot do incrementals — script will also fall back, but we make the + // decision here so we register the right type up-front. + if (VirtualMachine.State.Stopped.equals(vm.getState())) { + return ChainDecision.fullStart(newBitmap); + } + + Integer fullEvery = NASBackupFullEvery.valueIn(vm.getDataCenterId()); + if (fullEvery == null || fullEvery <= 1) { + // Disabled or every-backup-is-full mode. + return ChainDecision.fullStart(newBitmap); + } + + // 1. If the VM has no active_checkpoint_id, there is no bitmap on the host to use as + // a parent. This is the case after restore (we clear it), after VM was just assigned + // to the offering, or on the very first backup. + String activeCheckpoint = readVmActiveCheckpoint(vm.getId()); + if (activeCheckpoint == null) { + return ChainDecision.fullStart(newBitmap); + } + + // 2. The most-recent BackedUp backup is the only safe parent — after restore the + // next backup is always a fresh full, so anything older has a rotated-out bitmap. + // If the latest backup's bitmap doesn't match the VM's active_checkpoint_id, the + // chain is broken: force a full. + Backup parent = findLatestBackedUpBackup(vm.getId()); + if (parent == null || !activeCheckpoint.equals(readDetail(parent, NASBackupChainKeys.BITMAP_NAME))) { + LOG.debug("VM {} latest backup does not match active_checkpoint_id={} — forcing full", + vm.getInstanceName(), activeCheckpoint); + return ChainDecision.fullStart(newBitmap); + } + + String parentChainId = readDetail(parent, NASBackupChainKeys.CHAIN_ID); + int parentChainPosition = chainPosition(parent); + if (parentChainId == null || parentChainPosition == Integer.MAX_VALUE) { + return ChainDecision.fullStart(newBitmap); + } + + // Force a fresh full when the chain has reached the configured length. + if (parentChainPosition + 1 >= fullEvery) { + return ChainDecision.fullStart(newBitmap); + } + + // The script needs the parent backup's on-NAS file path PER VOLUME so it can rebase + // each new qcow2 onto the matching parent. The paths are stored relative to the NAS + // mount root — the script resolves them inside its mount session. When alignment + // fails (volume count changed, etc.) compose returns null and we fall back to full + // so we don't risk corrupting the chain. + List parentPaths = composeParentBackupPaths(parent, vm.getId()); + if (parentPaths == null) { + LOG.debug("VM {} parent backup {} volume layout no longer matches current VM — forcing full", + vm.getInstanceName(), parent.getUuid()); + return ChainDecision.fullStart(newBitmap); + } + return ChainDecision.incremental(newBitmap, activeCheckpoint, parentPaths, + parentChainId, parentChainPosition + 1); + } + + /** + * Incremental backups require QEMU dirty bitmaps / libvirt checkpoints, which are only + * possible on file-based qcow2 storage. Returns {@code true} only when EVERY volume of the + * VM sits on HOST-scope local, {@code SharedMountPoint}, or {@code NetworkFilesystem} (NFS) + * storage. Ceph-RBD, Linstor, and any other pool that cannot carry a per-disk checkpoint + * make this return {@code false} so the caller falls back to the legacy full-only path. A + * volume whose pool can no longer be resolved is treated as incapable (safe default). + */ + protected boolean allVolumesOnCheckpointCapableStorage(VirtualMachine vm) { + List volumes = volumeDao.findByInstance(vm.getId()); + if (volumes == null) { + return true; + } + for (VolumeVO volume : volumes) { + StoragePoolVO pool = primaryDataStoreDao.findById(volume.getPoolId()); + if (pool == null) { + LOG.debug("VM {} volume {} has no resolvable storage pool — forcing legacy full", + vm.getInstanceName(), volume.getUuid()); + return false; + } + boolean checkpointCapable = ScopeType.HOST.equals(pool.getScope()) + || Storage.StoragePoolType.SharedMountPoint.equals(pool.getPoolType()) + || Storage.StoragePoolType.NetworkFilesystem.equals(pool.getPoolType()); + if (!checkpointCapable) { + LOG.debug("VM {} volume {} is on {} (scope {}) which cannot carry checkpoints — forcing legacy full", + vm.getInstanceName(), volume.getUuid(), pool.getPoolType(), pool.getScope()); + return false; + } + } + return true; + } + + /** + * Read the {@code nas.active_checkpoint_id} VM detail. Returns {@code null} when no detail + * exists (post-restore, first backup, or after explicit reset). + */ + private String readVmActiveCheckpoint(long vmId) { + VMInstanceDetailVO d = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (d == null) { + return null; + } + String v = d.getValue(); + return (v == null || v.isEmpty()) ? null : v; + } + + /** + * Locate the most-recent {@code BackedUp} backup for {@code vmId}. The chain invariant + * guarantees the latest backup is the only valid incremental parent — after restore the + * next backup is always a fresh full, and {@link #decideChain} checks the bitmap matches. + */ + private Backup findLatestBackedUpBackup(long vmId) { + List history = backupDao.listByVmId(null, vmId); + if (history == null || history.isEmpty()) { + return null; + } + return history.stream() + .filter(b -> Backup.Status.BackedUp.equals(b.getStatus())) + .max(Comparator.comparing(Backup::getDate)) + .orElse(null); + } + + private String readDetail(Backup backup, String key) { + BackupDetailVO d = backupDetailsDao.findDetail(backup.getId(), key); + return d == null ? null : d.getValue(); + } + + /** + * Compose the on-NAS path of EVERY parent backup file (one per VM volume) in the same + * order the script will iterate the current VM's disks (deviceId asc). Relative to the + * NAS mount, matches the layout written by {@code nasbackup.sh}: + * first disk -> {@code /root..qcow2} + * others -> {@code /datadisk..qcow2} + * + * Returns {@code null} if the parent's stored volume count doesn't match the current VM's + * volume count. Volume attach/detach is blocked while a VM is assigned to a backup offering; + * if the offering was removed and re-assigned the active checkpoint is cleared in + * {@link #removeVMFromBackupOffering}, so this method doesn't need to revalidate volume + * identities — a count mismatch is the only way to reach this branch with a non-null + * active_checkpoint_id. + */ + private List composeParentBackupPaths(Backup parent, long vmId) { + // backupPath is stored as externalId by createBackupObject — e.g. + // "i-2-1234-VM/2026.04.27.13.45.00". + String dir = parent.getExternalId(); + if (dir == null || dir.isEmpty()) { + return null; + } + + List parentVols = parent.getBackedUpVolumes(); + if (parentVols == null || parentVols.isEmpty()) { + return null; + } + + List currentVols = volumeDao.findByInstance(vmId); + if (currentVols == null || currentVols.size() != parentVols.size()) { + return null; + } + + // parentVols is in deviceId order at the time the parent was taken. The script names the + // per-disk files from the volume PATH basename (root..qcow2 / datadisk..qcow2, + // see nasbackup.sh: volUuid="${fullpath##*/}"). Use getPath(), NOT getUuid(): after a + // volume migration the uuid and the on-disk path diverge, and the backup file is named by + // path — a uuid-based parent path then fails to resolve for the incremental (test 13). + List paths = new ArrayList<>(parentVols.size()); + for (int i = 0; i < parentVols.size(); i++) { + String volPath = parentVols.get(i).getPath(); + String prefix = (i == 0) ? "root" : "datadisk"; + paths.add(dir + "/" + prefix + "." + volPath + ".qcow2"); + } + return paths; + } + + /** + * Persist chain metadata under backup_details. Stored here (not on the backups table) so + * other providers can implement their own chain semantics without schema changes. + */ + private void persistChainMetadata(Backup backup, ChainDecision decision, String bitmapFromAgent) { + // Only persist nas.bitmap_name when the agent confirmed the bitmap exists on the host. + // The agent wrapper sets bitmapFromAgent=null when nasbackup.sh exits + // EXIT_BITMAP_NOT_SEEDED (=22) — currently only the stopped-VM path where qemu-img + // bitmap --add failed on every source disk. Anchoring the next incremental on a + // bitmap that doesn't exist would force a non-recoverable failure, so we leave the + // detail empty and let the next backup start a fresh full chain. + if (bitmapFromAgent != null && !bitmapFromAgent.isEmpty()) { + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.BITMAP_NAME, bitmapFromAgent, true)); + } + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.CHAIN_ID, decision.chainId, true)); + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.CHAIN_POSITION, + String.valueOf(decision.chainPosition), true)); + // Backup full-vs-incremental type lives on backup.type (set by takeBackup) — single + // source of truth. Not duplicated into backup_details. + if (decision.isIncremental()) { + // Resolve the parent backup's UUID so restore can walk the chain by id, not by path. + String parentUuid = lookupParentBackupUuid(backup.getVmId(), decision.bitmapParent); + if (parentUuid != null) { + backupDetailsDao.persist(new BackupDetailVO(backup.getId(), NASBackupChainKeys.PARENT_BACKUP_ID, parentUuid, true)); + } + } + } + + /** + * Upsert the VM's {@code nas.active_checkpoint_id} detail to {@code bitmapName}. Called + * after every successful backup so the next backup's parent-bitmap decision is anchored + * on what actually exists on QEMU, not on "last backup taken". + */ + private void upsertVmActiveCheckpoint(long vmId, String bitmapName) { + VMInstanceDetailVO existing = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (existing == null) { + vmInstanceDetailsDao.addDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID, bitmapName, false); + return; + } + existing.setValue(bitmapName); + vmInstanceDetailsDao.update(existing.getId(), existing); + } + + /** + * Remove the VM's {@code nas.active_checkpoint_id} detail. Called from the restore paths: + * after restore the disk image has no QEMU bitmap attached, so any future incremental + * would be based on stale state. Clearing forces the next backup to be a fresh full. + */ + private void clearVmActiveCheckpoint(long vmId) { + VMInstanceDetailVO existing = vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + if (existing != null) { + vmInstanceDetailsDao.removeDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + LOG.debug("Cleared nas.active_checkpoint_id for VM id={} (was {})", vmId, existing.getValue()); + } + } + + private String lookupParentBackupUuid(long vmId, String parentBitmap) { + if (parentBitmap == null) { + return null; + } + for (Backup b : backupDao.listByVmId(null, vmId)) { + String bm = readDetail(b, NASBackupChainKeys.BITMAP_NAME); + if (parentBitmap.equals(bm)) { + return b.getUuid(); + } + } + return null; + } + protected Host getVMHypervisorHostForBackup(VirtualMachine vm) { Long hostId = vm.getHostId(); if (hostId == null && VirtualMachine.State.Running.equals(vm.getState())) { @@ -188,7 +550,7 @@ protected Host getVMHypervisorHostForBackup(VirtualMachine vm) { } @Override - public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated) { final Host host = getVMHypervisorHostForBackup(vm); final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId()); @@ -206,12 +568,20 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce final String backupPath = String.format("%s/%s", vm.getInstanceName(), new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(creationDate)); - BackupVO backupVO = createBackupObject(vm, backupPath); + // Decide full vs incremental for this backup. Stopped VMs are always full + // (libvirt backup-begin requires a running QEMU process). + ChainDecision decision = decideChain(vm); + + BackupVO backupVO = createBackupObject(vm, backupPath, decision.isIncremental() ? "INCREMENTAL" : "FULL"); TakeBackupCommand command = new TakeBackupCommand(vm.getInstanceName(), backupPath); command.setBackupRepoType(backupRepository.getType()); command.setBackupRepoAddress(backupRepository.getAddress()); command.setMountOptions(backupRepository.getMountOptions()); command.setQuiesce(quiesceVM); + command.setMode(decision.mode); + command.setBitmapNew(decision.bitmapNew); + command.setBitmapParent(decision.bitmapParent); + command.setParentPaths(decision.parentPaths); if (VirtualMachine.State.Stopped.equals(vm.getState())) { List vmVolumes = volumeDao.findByInstance(vm.getId()); @@ -227,12 +597,12 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce } catch (AgentUnavailableException e) { logger.error("Unable to contact backend control plane to initiate backup for VM {}", vm.getInstanceName()); backupVO.setStatus(Backup.Status.Failed); - backupDao.remove(backupVO.getId()); + backupDao.update(backupVO.getId(), backupVO); throw new CloudRuntimeException("Unable to contact backend control plane to initiate backup"); } catch (OperationTimedoutException e) { logger.error("Operation to initiate backup timed out for VM {}", vm.getInstanceName()); backupVO.setStatus(Backup.Status.Failed); - backupDao.remove(backupVO.getId()); + backupDao.update(backupVO.getId(), backupVO); throw new CloudRuntimeException("Operation to initiate backup timed out, please try again"); } @@ -240,32 +610,54 @@ public Pair takeBackup(final VirtualMachine vm, Boolean quiesce backupVO.setDate(new Date()); backupVO.setSize(answer.getSize()); backupVO.setStatus(Backup.Status.BackedUp); + // If the agent fell back to full (stopped VM mid-incremental cycle), record this + // backup as a full and start a new chain. + ChainDecision effective = decision; + if (answer.getIncrementalFallback()) { + effective = ChainDecision.fullStart(decision.bitmapNew); + backupVO.setType("FULL"); + } List volumes = new ArrayList<>(volumeDao.findByInstance(vm.getId())); backupVO.setBackedUpVolumes(backupManager.createVolumeInfoFromVolumes(volumes)); if (backupDao.update(backupVO.getId(), backupVO)) { + // Legacy-full (incremental feature disabled): persist no chain/checkpoint metadata + // and do not touch the VM's active_checkpoint_id — keep the feature-off path legacy. + if (!decision.isLegacyFull()) { + persistChainMetadata(backupVO, effective, answer.getBitmapCreated()); + // Pin the VM's active_checkpoint_id to whichever bitmap the agent actually + // created — the only valid parent for the next incremental (see decideChain). + // If the agent reports no bitmap (bitmapCreated=null), clear any stale detail + // so the next backup starts a fresh full. + String confirmedBitmap = answer.getBitmapCreated(); + if (confirmedBitmap != null) { + upsertVmActiveCheckpoint(vm.getId(), confirmedBitmap); + } else { + clearVmActiveCheckpoint(vm.getId()); + } + } return new Pair<>(true, backupVO); } else { throw new CloudRuntimeException("Failed to update backup"); } } else { logger.error("Failed to take backup for VM {}: {}", vm.getInstanceName(), answer != null ? answer.getDetails() : "No answer received"); - if (answer.getNeedsCleanup()) { - logger.error("Backup cleanup failed for VM {}. Leaving the backup in Error state.", vm.getInstanceName()); + if (answer != null && answer.getNeedsCleanup()) { + logger.error("Backup cleanup failed for VM {}. Leaving the backup in Error state. Backup should be manually deleted to free up the space", vm.getInstanceName()); backupVO.setStatus(Backup.Status.Error); backupDao.update(backupVO.getId(), backupVO); } else { backupVO.setStatus(Backup.Status.Failed); - backupDao.remove(backupVO.getId()); + backupDao.update(backupVO.getId(), backupVO); } return new Pair<>(false, null); } } - private BackupVO createBackupObject(VirtualMachine vm, String backupPath) { + private BackupVO createBackupObject(VirtualMachine vm, String backupPath, String type) { BackupVO backup = new BackupVO(); backup.setVmId(vm.getId()); backup.setExternalId(backupPath); - backup.setType("FULL"); + backup.setType(type); backup.setDate(new Date()); long virtualSize = 0L; for (final Volume volume: volumeDao.findByInstance(vm.getId())) { @@ -287,17 +679,18 @@ private BackupVO createBackupObject(VirtualMachine vm, String backupPath) { } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return restoreVMBackup(vm, backup); } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { return restoreVMBackup(vm, backup).first(); } private Pair restoreVMBackup(VirtualMachine vm, Backup backup) { - List backedVolumesUUIDs = backup.getBackedUpVolumes().stream() + List backedVolumes = backup.getBackedUpVolumes(); + List backedVolumesUUIDs = backedVolumes.stream() .sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId)) .map(Backup.VolumeInfo::getUuid) .collect(Collectors.toList()); @@ -320,6 +713,7 @@ private Pair restoreVMBackup(VirtualMachine vm, Backup backup) Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(restoreVolumes); restoreCommand.setRestoreVolumePools(volumePoolsAndPaths.first()); restoreCommand.setRestoreVolumePaths(volumePoolsAndPaths.second()); + restoreCommand.setBackupFiles(getBackupFiles(backedVolumes)); restoreCommand.setVmExists(vm.getRemoved() == null); restoreCommand.setVmState(vm.getState()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); @@ -332,9 +726,22 @@ private Pair restoreVMBackup(VirtualMachine vm, Backup backup) } catch (OperationTimedoutException e) { throw new CloudRuntimeException("Operation to restore backup timed out, please try again"); } + // After a restore the QEMU dirty-bitmap chain is gone — clear active_checkpoint_id so + // the next backup is taken as a fresh full and starts a new chain. See decideChain. + if (answer != null && answer.getResult()) { + clearVmActiveCheckpoint(vm.getId()); + } return new Pair<>(answer.getResult(), answer.getDetails()); } + private List getBackupFiles(List backedVolumes) { + List backupFiles = new ArrayList<>(); + for (Backup.VolumeInfo backedVolume : backedVolumes) { + backupFiles.add(backedVolume.getPath()); + } + return backupFiles; + } + private Pair, List> getVolumePoolsAndPaths(List volumes) { List volumePools = new ArrayList<>(); List volumePaths = new ArrayList<>(); @@ -348,7 +755,8 @@ private Pair, List> getVolumePoolsAndPaths(List volumePools.add(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null); String volumePathPrefix = getVolumePathPrefix(storagePool); - volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath())); + String volumePathSuffix = getVolumePathSuffix(storagePool); + volumePaths.add(String.format("%s%s%s", volumePathPrefix, volume.getPath(), volumePathSuffix)); } return new Pair<>(volumePools, volumePaths); } @@ -358,22 +766,39 @@ private String getVolumePathPrefix(StoragePoolVO storagePool) { if (ScopeType.HOST.equals(storagePool.getScope()) || Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType()) || Storage.StoragePoolType.RBD.equals(storagePool.getPoolType())) { - volumePathPrefix = storagePool.getPath(); + volumePathPrefix = storagePool.getPath() + "/"; + } else if (Storage.StoragePoolType.Linstor.equals(storagePool.getPoolType())) { + volumePathPrefix = "/dev/drbd/by-res/cs-"; } else { // Should be Storage.StoragePoolType.NetworkFilesystem - volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); + volumePathPrefix = String.format("/mnt/%s/", storagePool.getUuid()); } return volumePathPrefix; } + private String getVolumePathSuffix(StoragePoolVO storagePool) { + if (Storage.StoragePoolType.Linstor.equals(storagePool.getPoolType())) { + return "/0"; + } else { + return ""; + } + } + @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); final HostVO hostVO = hostDao.findByIp(hostIp); - LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", backupVolumeInfo, backup); + Backup.VolumeInfo matchingVolume = getBackedUpVolumeInfo(backup.getBackedUpVolumes(), volume.getUuid()); + if (matchingVolume == null) { + throw new CloudRuntimeException(String.format("Unable to find volume %s in the list of backed up volumes for backup %s, cannot proceed with restore", volume.getUuid(), backup)); + } + Long backedUpVolumeSize = matchingVolume.getSize(); + + LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", volume, backup); BackupRepository backupRepository = getBackupRepository(backup); VolumeVO restoredVolume = new VolumeVO(Volume.Type.DATADISK, null, backup.getZoneId(), @@ -391,7 +816,7 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoredVolume.setPoolType(pool.getPoolType()); restoredVolume.setPath(restoredVolume.getUuid()); restoredVolume.setState(Volume.State.Copying); - restoredVolume.setSize(backupVolumeInfo.getSize()); + restoredVolume.setSize(backedUpVolumeSize); restoredVolume.setDiskOfferingId(diskOffering.getId()); if (pool.getPoolType() != Storage.StoragePoolType.RBD) { restoredVolume.setFormat(Storage.ImageFormat.QCOW2); @@ -404,15 +829,17 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoAddress(backupRepository.getAddress()); restoreCommand.setVmName(vmNameAndState.first()); - restoreCommand.setRestoreVolumePaths(Collections.singletonList(String.format("%s/%s", getVolumePathPrefix(pool), volumeUUID))); + String restoreVolumePath = String.format("%s%s%s", getVolumePathPrefix(pool), volumeUUID, getVolumePathSuffix(pool)); + restoreCommand.setRestoreVolumePaths(Collections.singletonList(restoreVolumePath)); + restoreCommand.setRestoreVolumeSizes(Collections.singletonList(backedUpVolumeSize)); DataStore dataStore = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); restoreCommand.setRestoreVolumePools(Collections.singletonList(dataStore != null ? (PrimaryDataStoreTO)dataStore.getTO() : null)); restoreCommand.setDiskType(backupVolumeInfo.getType().name().toLowerCase(Locale.ROOT)); restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmExists(null); restoreCommand.setVmState(vmNameAndState.second()); - restoreCommand.setRestoreVolumeUUID(backupVolumeInfo.getUuid()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); + restoreCommand.setBackupFiles(Collections.singletonList(matchingVolume.getPath())); BackupAnswer answer; try { @@ -429,6 +856,13 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI } catch (Exception e) { throw new CloudRuntimeException("Unable to create restored volume due to: " + e); } + // The restored volume is attached to this VM with no QEMU bitmap on its image, so the + // VM's tracked checkpoint is now stale; clear it to force the next backup to be a full + // (mirrors the full restore paths restoreVMFromBackup/restoreBackupToVM). + VirtualMachine restoreTargetVm = vmInstanceDao.findVMByInstanceName(vmNameAndState.first()); + if (restoreTargetVm != null) { + clearVmActiveCheckpoint(restoreTargetVm.getId()); + } } return new Pair<>(answer.getResult(), answer.getDetails()); @@ -442,10 +876,19 @@ private BackupRepository getBackupRepository(Backup backup) { return backupRepository; } - private Optional getBackedUpVolumeInfo(List backedUpVolumes, String volumeUuid) { + private Backup.VolumeInfo getBackedUpVolumeInfo(List backedUpVolumes, String volumeUuid) { return backedUpVolumes.stream() .filter(v -> v.getUuid().equals(volumeUuid)) - .findFirst(); + .findFirst() + .orElse(null); + } + + @Override + public boolean handlesChainDeleteResourceAccounting() { + // This provider deletes whole incremental chains (leaf + swept delete-pending ancestors) + // and decrements resource count/usage once per physically-removed backup itself, so the + // manager must not also decrement or remove the DB row. + return true; } @Override @@ -462,10 +905,78 @@ public boolean deleteBackup(Backup backup, boolean forced) { } else { host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, backup.getZoneId()); } + if (host == null) { + throw new CloudRuntimeException(String.format("Unable to find a running KVM host in zone %d to delete backup %s", backup.getZoneId(), backup.getUuid())); + } + + // Backups outside any tracked chain (legacy or non-incremental providers) are + // deleted straight away — no children semantics apply. + if (readDetail(backup, NASBackupChainKeys.CHAIN_ID) == null) { + return deleteBackupFileAndRow(backup, backupRepository, host); + } + + // Concurrent deletes can mutate the same chain concurrently resulting in inconsistent states. + // take a per-VM lock before modifying the chain. + final GlobalLock chainLock = acquireChainDeleteLock(backup.getVmId()); + try { + Backup current = backupDao.findById(backup.getId()); + if (current == null) { + LOG.debug("Backup {} was already removed by a concurrent chain delete", backup.getUuid()); + return true; + } + backup = current; + + // Snapshot-style cascade: defer the on-NAS rm + DB row while there are live children, + // mark this backup as delete-pending, and let the leaf's deletion sweep it up later. + // See DefaultSnapshotStrategy#deleteSnapshotChain for the same pattern on incremental + // snapshots. forced=true means the caller wants the entire subtree gone right now. + if (forced) { + return cascadeDeleteSubtree(backup, backupRepository, host); + } + + if (hasLiveChildren(backup)) { + markDeletePending(backup); + LOG.debug("Backup {} has live descendants in its chain; setting status as Hidden. " + + "The on-NAS file and DB row will be removed once the last descendant is gone, " + + "or pass forced=true.", + backup.getUuid()); + return true; + } + + // No live children — physically delete this backup, then walk up the chain and + // collect any ancestors that were left in Hidden state. + return deleteLeafBackupAndSweepPendingAncestors(backup, backupRepository, host); + } finally { + releaseChainDeleteLock(chainLock); + } + } + + /** + * Take the per-VM lock that serializes chain-mutating backup deletes. + */ + protected GlobalLock acquireChainDeleteLock(long vmId) { + GlobalLock lock = GlobalLock.getInternLock("nas.backup.chain.vm." + vmId); + if (!lock.lock(CHAIN_LOCK_TIMEOUT_SECONDS)) { + lock.releaseRef(); + throw new CloudRuntimeException(String.format( + "Timed out waiting for concurrent backup chain operations on VM %d to finish, please try again", vmId)); + } + return lock; + } - DeleteBackupCommand command = new DeleteBackupCommand(backup.getExternalId(), backupRepository.getType(), - backupRepository.getAddress(), backupRepository.getMountOptions()); + protected void releaseChainDeleteLock(GlobalLock lock) { + lock.unlock(); + lock.releaseRef(); + } + /** + * The single physical-delete step: rm the on-NAS directory, then remove the DB row. + * Returns {@code false} (and leaves both intact) if the agent reports failure, so the + * caller's recursion stops cleanly. + */ + private boolean deleteBackupFileAndRow(Backup backup, BackupRepository repo, Host host) { + DeleteBackupCommand command = new DeleteBackupCommand(backup.getExternalId(), repo.getType(), + repo.getAddress(), repo.getMountOptions()); BackupAnswer answer; try { answer = (BackupAnswer) agentManager.send(host.getId(), command); @@ -474,15 +985,157 @@ public boolean deleteBackup(Backup backup, boolean forced) { } catch (OperationTimedoutException e) { throw new CloudRuntimeException("Operation to delete backup timed out, please try again"); } + if (answer == null || !answer.getResult()) { + logger.error("Failed to delete backup file for {} ({}); leaving DB row intact", + backup.getUuid(), backup.getExternalId()); + return false; + } + // Capture the deleted backup's bitmap before the row (and its backup_details) are removed. + String deletedBitmap = readDetail(backup, NASBackupChainKeys.BITMAP_NAME); + backupDao.remove(backup.getId()); + // If this backup owned the bitmap the VM's active_checkpoint_id points to, that on-host QEMU + // dirty-bitmap is gone with it — clear active_checkpoint_id so the next backup starts a fresh + // full chain instead of anchoring an incremental on a deleted checkpoint (test: delete last backup). + if (deletedBitmap != null && deletedBitmap.equals(readVmActiveCheckpoint(backup.getVmId()))) { + clearVmActiveCheckpoint(backup.getVmId()); + } + // Exactly-once resource accounting: decrement at the single point a backup row + file are + // physically removed. This runs for the leaf and for every swept delete-pending ancestor, + // so a chain delete decrements once per actually-removed backup. The manager skips its own + // accounting for this provider (see handlesChainDeleteResourceAccounting()). + long size = backup.getSize() != null ? backup.getSize() : 0L; + resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup); + resourceLimitMgr.decrementResourceCount(backup.getAccountId(), Resource.ResourceType.backup_storage, size); + return true; + } - if (answer != null && answer.getResult()) { - return backupDao.remove(backup.getId()); + /** + * Tombstone {@code backup} by moving it to {@link Backup.Status#Hidden}. Idempotent. + * The row stays in the DB so the chain GC can sweep it once its last descendant is deleted + * ({@code listByVmId} is status-agnostic), but it disappears from the user-facing list + * ({@link BackupManagerImpl#listBackups} filters Hidden) and all backup operations refuse it + * (they require {@code BackedUp}). Replaces the previous {@code nas.delete_pending} detail. + */ + private void markDeletePending(Backup backup) { + if (Backup.Status.Hidden.equals(backup.getStatus())) { + return; + } + BackupVO vo = backupDao.findById(backup.getId()); + if (vo != null) { + vo.setStatus(Backup.Status.Hidden); + backupDao.update(vo.getId(), vo); } + } + + /** + * @return true if this backup is a tombstone (Hidden) awaiting chain sweep. + */ + private boolean isDeletePending(Backup backup) { + return Backup.Status.Hidden.equals(backup.getStatus()); + } - logger.debug("There was an error removing the backup with id {}", backup.getId()); + /** + * Whether there are any live (not tombstoned/Hidden, not Removed) descendants of {@code backup} within the + * same chain. This uses {@code CHAIN_POSITION} to detect dependents deeper in the chain. + */ + private boolean hasLiveChildren(Backup backup) { + String chainId = readDetail(backup, NASBackupChainKeys.CHAIN_ID); + if (chainId == null) { + return false; + } + int position = chainPosition(backup); + for (Backup b : backupDao.listByVmId(null, backup.getVmId())) { + if (b.getId() == backup.getId() || !chainId.equals(readDetail(b, NASBackupChainKeys.CHAIN_ID))) { + continue; + } + if (!isDeletePending(b) && chainPosition(b) > position) { + return true; + } + } return false; } + /** + * Return the chain containing {@code member}, ordered leaf-first + * (highest {@code CHAIN_POSITION} → root). + * + *

    Materialises the chain via a single {@link BackupDao#listByVmId} call. Callers that + * previously walked the chain by repeatedly calling {@link #findChainParent} were doing + * O(N) {@code listByVmId} calls (one per ancestor); this collapses that to one. + * + *

    If {@code member} has no {@code CHAIN_ID} metadata it is returned as a one-element + * list (it is its own degenerate chain). + */ + private List getChainOrderedLeafToRoot(Backup member) { + String chainId = readDetail(member, NASBackupChainKeys.CHAIN_ID); + if (chainId == null) { + return Collections.singletonList(member); + } + List chain = new ArrayList<>(); + for (Backup b : backupDao.listByVmId(null, member.getVmId())) { + if (chainId.equals(readDetail(b, NASBackupChainKeys.CHAIN_ID))) { + chain.add(b); + } + } + // Descending CHAIN_POSITION = leaf-first (highest position = furthest from root). + chain.sort((a, b) -> Integer.compare(chainPosition(b), chainPosition(a))); + return chain; + } + + /** + * Physically delete the leaf {@code backup}, then walk up the chain while each ancestor + * is in Hidden state. Mirrors the snapshot subsystem pattern: once a leaf is + * gone, garbage-collect any tombstoned parents. + * + *

    Caller must guarantee {@code backup} is a leaf (no live children). Each tombstoned + * ancestor is by definition childless once its sole child is deleted here, so no extra + * live-children check is needed inside the loop. + */ + private boolean deleteLeafBackupAndSweepPendingAncestors(Backup backup, BackupRepository repo, Host host) { + // Snapshot the chain BEFORE the leaf delete — deleteBackupFileAndRow removes the row, + // after which the in-memory list still resolves but the DB no longer would. + List chain = getChainOrderedLeafToRoot(backup); + if (!deleteBackupFileAndRow(backup, repo, host)) { + return false; + } + for (Backup member : chain) { + if (member.getId() == backup.getId()) { + continue; + } + if (!isDeletePending(member)) { + break; + } + deleteBackupFileAndRow(member, repo, host); + } + return true; + } + + /** + * Forced delete of {@code root}'s entire chain, leaf-first. NAS backups form a linear + * chain (full → inc → inc → …), not a tree, so we just walk the ordered chain and + * delete each member without re-querying parents. + */ + private boolean cascadeDeleteSubtree(Backup root, BackupRepository repo, Host host) { + for (Backup b : getChainOrderedLeafToRoot(root)) { + if (!deleteBackupFileAndRow(b, repo, host)) { + return false; + } + } + return true; + } + + private int chainPosition(Backup b) { + String s = readDetail(b, NASBackupChainKeys.CHAIN_POSITION); + if (s == null) { + return Integer.MAX_VALUE; // no metadata => sort to end + } + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return Integer.MAX_VALUE; + } + } + public void syncBackupMetrics(Long zoneId) { } @@ -511,6 +1164,11 @@ public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backup @Override public boolean removeVMFromBackupOffering(VirtualMachine vm) { + // Clear the VM's active checkpoint so any future re-assignment to a backup offering + // starts a fresh chain. Without this, a detach-volume + attach-different-volume cycle + // while the offering is unassigned would lead to the next backup trying to rebase + // onto a stale parent (different volume identity, same VM id). + clearVmActiveCheckpoint(vm.getId()); return true; } @@ -548,7 +1206,14 @@ public Pair getBackupStorageStats(Long zoneId) { @Override public void syncBackupStorageStats(Long zoneId) { final List repositories = backupRepositoryDao.listByZoneAndProvider(zoneId, getName()); + if (CollectionUtils.isEmpty(repositories)) { + return; + } final Host host = resourceManager.findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); + if (host == null) { + logger.warn("Unable to find a running KVM host in zone {} to sync backup storage stats", zoneId); + return; + } for (final BackupRepository repository : repositories) { GetBackupStorageStatsCommand command = new GetBackupStorageStatsCommand(repository.getType(), repository.getAddress(), repository.getMountOptions()); BackupStorageStatsAnswer answer; @@ -590,7 +1255,9 @@ public Boolean crossZoneInstanceCreationEnabled(BackupOffering backupOffering) { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ - NASBackupRestoreMountTimeout + NASBackupRestoreMountTimeout, + NASBackupFullEvery, + NASBackupIncrementalEnabled }; } diff --git a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java index a512292cd28f..f1d5613ab7fc 100644 --- a/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java +++ b/plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java @@ -28,6 +28,7 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -38,22 +39,36 @@ import com.cloud.agent.AgentManager; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; +import com.cloud.configuration.Resource; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; import com.cloud.resource.ResourceManager; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.ResourceLimitService; import com.cloud.utils.Pair; +import com.cloud.utils.db.GlobalLock; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; + +import com.google.gson.Gson; import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; @@ -96,6 +111,21 @@ public class NASBackupProviderTest { @Mock private VMSnapshotDao vmSnapshotDaoMock; + @Mock + private BackupDetailsDao backupDetailsDao; + + @Mock + private VMInstanceDetailsDao vmInstanceDetailsDao; + + @Mock + private DiskOfferingDao diskOfferingDao; + + @Mock + private DataStoreManager dataStoreMgr; + + @Mock + private ResourceLimitService resourceLimitMgr; + @Test public void testDeleteBackup() throws OperationTimedoutException, AgentUnavailableException { Long hostId = 1L; @@ -221,7 +251,7 @@ public void takeBackupSuccessfully() throws AgentUnavailableException, Operation Mockito.when(backupDao.persist(Mockito.any(BackupVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); Mockito.when(backupDao.update(Mockito.anyLong(), Mockito.any(BackupVO.class))).thenReturn(true); - Pair result = nasBackupProvider.takeBackup(vm, false); + Pair result = nasBackupProvider.takeBackup(vm, false, false); Assert.assertTrue(result.first()); Assert.assertNotNull(result.second()); @@ -353,4 +383,655 @@ public void testGetVMHypervisorHostFallbackToZoneWideKVMHost() { Mockito.verify(hostDao).findHypervisorHostInCluster(clusterId); Mockito.verify(resourceManager).findOneRandomRunningHostByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); } + + // -- nas.backup.incremental.enabled master switch ------------------------------------ + + /** + * When the operator sets nas.backup.incremental.enabled=false at the zone level, every + * backup must be a fresh full anchor, regardless of VM state or nas.backup.full.every. + * This is a single toggle the + * operator can flip without having to count remaining backups in a chain. + */ + @Test + public void decideChainReturnsLegacyFullWhenIncrementalDisabled() { + Long zoneId = 1L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.lenient().when(vm.getDataCenterId()).thenReturn(zoneId); + + // Stub the master switch to false. ConfigKey.valueIn delegates to the framework's + // ConfigDepot at runtime; for the unit test we override the in-memory value via the + // ConfigKey's local override (set by ReflectionTestUtils on the spy provider). + ReflectionTestUtils.setField(nasBackupProvider, "NASBackupIncrementalEnabled", + new org.apache.cloudstack.framework.config.ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", "false", + "test override — disabled", true, + org.apache.cloudstack.framework.config.ConfigKey.Scope.Zone)); + + NASBackupProvider.ChainDecision decision = nasBackupProvider.decideChain(vm); + Assert.assertNotNull(decision); + Assert.assertEquals(NASBackupChainKeys.TYPE_LEGACY_FULL, decision.mode); + Assert.assertNull("legacy-full must not carry a bitmap", decision.bitmapNew); + Assert.assertNull(decision.bitmapParent); + Assert.assertNull("legacy-full must not start a chain", decision.chainId); + Assert.assertEquals(0, decision.chainPosition); + } + + // -- decideChain anchored on VM's active_checkpoint_id ------------------------------- + + /** + * No active_checkpoint_id on the VM (post-restore, first-ever backup, or detail purged) => + * decideChain must return a fresh full. Relying on the last backup taken as the parent + * breaks after a restore, so the decision is anchored on the active checkpoint instead. + */ + @Test + public void decideChainReturnsFullWhenVmHasNoActiveCheckpoint() { + Long zoneId = 1L; + Long vmId = 42L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getDataCenterId()).thenReturn(zoneId); + Mockito.when(vm.getState()).thenReturn(VMInstanceVO.State.Running); + + // Master switch defaults to false (opt-in by zone) — explicitly enable it for this + // test so we exercise the "no active_checkpoint_id" branch rather than short-circuit + // at the master-switch gate. + ReflectionTestUtils.setField(nasBackupProvider, "NASBackupIncrementalEnabled", + new org.apache.cloudstack.framework.config.ConfigKey<>("Advanced", Boolean.class, + "nas.backup.incremental.enabled", "true", + "test override — enabled", true, + org.apache.cloudstack.framework.config.ConfigKey.Scope.Zone)); + + Mockito.when(vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(null); + + NASBackupProvider.ChainDecision decision = nasBackupProvider.decideChain(vm); + Assert.assertNotNull(decision); + Assert.assertEquals(NASBackupChainKeys.TYPE_FULL, decision.mode); + Assert.assertNull(decision.bitmapParent); + Assert.assertEquals(0, decision.chainPosition); + } + + // -- incremental storage-capability guard (Ceph-RBD / Linstor stay on legacy full) ---- + + /** + * Incremental checkpoints are only possible on file-based qcow2 storage. A VM whose every + * volume sits on NFS / HOST-scope local / SharedMountPoint is checkpoint-capable. + */ + @Test + public void allVolumesOnCheckpointCapableStorageTrueForNfsHostAndSharedMount() { + Long vmId = 55L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + + VolumeVO nfsVol = mock(VolumeVO.class); + Mockito.when(nfsVol.getPoolId()).thenReturn(1L); + VolumeVO hostVol = mock(VolumeVO.class); + Mockito.when(hostVol.getPoolId()).thenReturn(2L); + VolumeVO smpVol = mock(VolumeVO.class); + Mockito.when(smpVol.getPoolId()).thenReturn(3L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(nfsVol, hostVol, smpVol)); + + StoragePoolVO nfs = mock(StoragePoolVO.class); + Mockito.when(nfs.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + StoragePoolVO host = mock(StoragePoolVO.class); + Mockito.when(host.getScope()).thenReturn(ScopeType.HOST); + StoragePoolVO smp = mock(StoragePoolVO.class); + Mockito.when(smp.getPoolType()).thenReturn(Storage.StoragePoolType.SharedMountPoint); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(nfs); + Mockito.when(storagePoolDao.findById(2L)).thenReturn(host); + Mockito.when(storagePoolDao.findById(3L)).thenReturn(smp); + + Assert.assertTrue(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + /** + * A single volume on Ceph-RBD (or any pool that cannot carry a per-disk checkpoint) forces + * the whole VM onto the legacy full-only path — avoids regressing RBD/Linstor storages. + */ + @Test + public void allVolumesOnCheckpointCapableStorageFalseWhenAnyVolumeOnRbd() { + Long vmId = 56L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + + VolumeVO nfsVol = mock(VolumeVO.class); + Mockito.when(nfsVol.getPoolId()).thenReturn(1L); + VolumeVO rbdVol = mock(VolumeVO.class); + Mockito.when(rbdVol.getPoolId()).thenReturn(9L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(nfsVol, rbdVol)); + + StoragePoolVO nfs = mock(StoragePoolVO.class); + Mockito.when(nfs.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + StoragePoolVO rbd = mock(StoragePoolVO.class); + Mockito.when(rbd.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(nfs); + Mockito.when(storagePoolDao.findById(9L)).thenReturn(rbd); + + Assert.assertFalse(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + /** A volume whose storage pool can no longer be resolved is treated as incapable (safe). */ + @Test + public void allVolumesOnCheckpointCapableStorageFalseWhenPoolUnresolvable() { + Long vmId = 57L; + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + VolumeVO vol = mock(VolumeVO.class); + Mockito.when(vol.getPoolId()).thenReturn(1L); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(List.of(vol)); + Mockito.when(storagePoolDao.findById(1L)).thenReturn(null); + + Assert.assertFalse(nasBackupProvider.allVolumesOnCheckpointCapableStorage(vm)); + } + + // -- restore clears active_checkpoint_id --------------------------------------------- + + /** + * After a successful restoreVMFromBackup, decideChain on the next backup must produce + * a full. We verify this end-to-end by checking that vmInstanceDetailsDao.removeDetail + * is called with the active_checkpoint_id key. + */ + @Test + public void restoreClearsActiveCheckpointDetail() throws AgentUnavailableException, OperationTimedoutException { + Long vmId = 7L; + Long hostId = 8L; + Long backupOfferingId = 9L; + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + Mockito.when(vm.getRemoved()).thenReturn(null); + Mockito.when(vm.getName()).thenReturn("vm7"); + + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupVO backup = new BackupVO(); + backup.setVmId(vmId); + backup.setBackupOfferingId(backupOfferingId); + backup.setExternalId("i-2-7-VM/2026.05.16.10.00.00"); + ReflectionTestUtils.setField(backup, "id", 100L); + // backedUpVolumes defaults to null => BackupVO.getBackedUpVolumes returns emptyList(). + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(backupOfferingId)).thenReturn(repo); + + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(Collections.emptyList()); + + BackupAnswer answer = mock(BackupAnswer.class); + Mockito.when(answer.getResult()).thenReturn(true); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(RestoreBackupCommand.class))).thenReturn(answer); + + // Pre-existing checkpoint detail so removeDetail has something to "clear". + VMInstanceDetailVO existing = mock(VMInstanceDetailVO.class); + Mockito.when(existing.getValue()).thenReturn("backup-1715000000"); + Mockito.when(vmInstanceDetailsDao.findDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); + + boolean ok = nasBackupProvider.restoreVMFromBackup(vm, backup, false, null); + Assert.assertTrue(ok); + Mockito.verify(vmInstanceDetailsDao).removeDetail(vmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + } + + /** + * Single-volume restore (restoreBackedUpVolume) must also clear the target VM's + * active_checkpoint_id, so the next backup of that VM is a fresh full — the restored + * volume's image carries no QEMU bitmap. + */ + @Test + public void restoreBackedUpVolumeClearsTargetVmActiveCheckpoint() + throws AgentUnavailableException, OperationTimedoutException { + Long targetVmId = 42L; + Long backupOfferingId = 9L; + String targetVmName = "i-2-42-VM"; + String volUuid = "vol-uuid-1"; + String hostIp = "10.0.0.5"; + String dsUuid = "ds-uuid-1"; + + VolumeVO srcVolume = mock(VolumeVO.class); + Mockito.when(srcVolume.getUuid()).thenReturn(volUuid); + Mockito.when(srcVolume.getName()).thenReturn("data1"); + Mockito.when(volumeDao.findByUuid(volUuid)).thenReturn(srcVolume); + + DiskOfferingVO diskOffering = mock(DiskOfferingVO.class); + Mockito.when(diskOffering.getId()).thenReturn(5L); + Mockito.when(diskOffering.getProvisioningType()).thenReturn(Storage.ProvisioningType.THIN); + Mockito.when(diskOfferingDao.findByUuid(Mockito.anyString())).thenReturn(diskOffering); + + StoragePoolVO pool = mock(StoragePoolVO.class); + Mockito.when(pool.getId()).thenReturn(11L); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(storagePoolDao.findByUuid(dsUuid)).thenReturn(pool); + + HostVO host = mock(HostVO.class); + Mockito.when(host.getId()).thenReturn(8L); + Mockito.when(hostDao.findByIp(hostIp)).thenReturn(host); + + Backup.VolumeInfo backedUp = new Backup.VolumeInfo(volUuid, "i-2-99-VM/2026/data1.qcow2", + Volume.Type.DATADISK, 1024L, 1L, "disk-offering-uuid", null, null); + + BackupVO backup = new BackupVO(); + backup.setVmId(99L); + backup.setBackupOfferingId(backupOfferingId); + backup.setExternalId("i-2-99-VM/2026.06.22.10.00.00"); + backup.setSize(1024L); + backup.setBackedUpVolumes(new Gson().toJson(Collections.singletonList(backedUp))); + ReflectionTestUtils.setField(backup, "id", 200L); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(backupOfferingId)).thenReturn(repo); + + BackupAnswer answer = mock(BackupAnswer.class); + Mockito.when(answer.getResult()).thenReturn(true); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(RestoreBackupCommand.class))).thenReturn(answer); + + VMInstanceVO targetVm = mock(VMInstanceVO.class); + Mockito.when(targetVm.getId()).thenReturn(targetVmId); + Mockito.when(vmInstanceDao.findVMByInstanceName(targetVmName)).thenReturn(targetVm); + + VMInstanceDetailVO existing = mock(VMInstanceDetailVO.class); + Mockito.when(existing.getValue()).thenReturn("backup-1718000000"); + Mockito.when(vmInstanceDetailsDao.findDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID)).thenReturn(existing); + + Pair result = nasBackupProvider.restoreBackedUpVolume( + backup, backedUp, hostIp, dsUuid, new Pair<>(targetVmName, VirtualMachine.State.Stopped), null, false); + + Assert.assertTrue(result.first()); + Mockito.verify(vmInstanceDetailsDao).removeDetail(targetVmId, NASBackupChainKeys.VM_ACTIVE_CHECKPOINT_ID); + } + + // -- delete-pending cascade ---------------------------------------------------------- + + /** + * Deleting an incremental that has a live child must mark the incremental as + * delete-pending in backup_details and NOT touch the on-NAS file or the backups row. + * A parent with live children is soft-deleted (delete-pending) rather than removed. + */ + @Test + public void deleteWithLiveChildMarksDeletePendingAndPreservesFile() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO parent = new BackupVO(); + parent.setVmId(vmId); + parent.setBackupOfferingId(offeringId); + parent.setExternalId("i-2-2-VM/2026.05.10.10.00.00"); + parent.setZoneId(zoneId); + ReflectionTestUtils.setField(parent, "id", 50L); + ReflectionTestUtils.setField(parent, "uuid", "parent-uuid"); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + // Note: host.getId() is intentionally not stubbed — the live-child path never + // contacts the agent (verified below), so the stub would be unnecessary. + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + // CHAIN_ID on the parent => not the no-chain fast path. + BackupDetailVO chainIdDetail = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO chainPosDetail = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)).thenReturn(chainIdDetail); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(chainPosDetail); + + // A live child sits deeper in the chain (higher CHAIN_POSITION). + BackupVO child = new BackupVO(); + child.setVmId(vmId); + child.setBackupOfferingId(offeringId); + child.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + child.setZoneId(zoneId); + child.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(child, "id", 51L); + ReflectionTestUtils.setField(child, "uuid", "child-uuid"); + + BackupDetailVO childChainId = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO childChainPos = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_POSITION, "1", true); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)).thenReturn(childChainId); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(childChainPos); + + Mockito.when(backupDao.listByVmId(null, vmId)).thenReturn(List.of(parent, child)); + // Re-read under the chain lock + markDeletePending both load the row by id. + Mockito.when(backupDao.findById(50L)).thenReturn(parent); + Mockito.doReturn(mock(GlobalLock.class)).when(nasBackupProvider).acquireChainDeleteLock(vmId); + + boolean result = nasBackupProvider.deleteBackup(parent, false); + Assert.assertTrue(result); + + // No agent traffic — the on-NAS file must be preserved while children are alive. + Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + // No DB row removal — the row is the tombstone marker. + Mockito.verify(backupDao, Mockito.never()).remove(50L); + // A tombstoned backup is NOT decremented — its space is still occupied until swept. + Mockito.verify(resourceLimitMgr, Mockito.never()).decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup)); + // The tombstoned backup is moved to Status.Hidden (replaces the old DELETE_PENDING detail). + ArgumentCaptor captor = ArgumentCaptor.forClass(BackupVO.class); + Mockito.verify(backupDao).update(Mockito.eq(50L), captor.capture()); + Assert.assertEquals(Backup.Status.Hidden, captor.getValue().getStatus()); + Mockito.verify(backupDetailsDao, Mockito.never()).persist(Mockito.any(BackupDetailVO.class)); + } + + /** + * Deleting a leaf incremental whose parent is delete-pending must (a) delete the leaf and + * then (b) sweep up the tombstoned parent. Mirrors DefaultSnapshotStrategy's + * "delete leaf, then walk up while parent is destroying-and-childless". + */ + @Test + public void deletingLeafSweepsUpDeletePendingParent() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO leaf = new BackupVO(); + leaf.setVmId(vmId); + leaf.setBackupOfferingId(offeringId); + leaf.setExternalId("i-2-2-VM/2026.05.10.11.00.00"); + leaf.setZoneId(zoneId); + ReflectionTestUtils.setField(leaf, "id", 51L); + ReflectionTestUtils.setField(leaf, "uuid", "leaf-uuid"); + + BackupVO parent = new BackupVO(); + parent.setVmId(vmId); + parent.setBackupOfferingId(offeringId); + parent.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + parent.setZoneId(zoneId); + ReflectionTestUtils.setField(parent, "id", 50L); + ReflectionTestUtils.setField(parent, "uuid", "parent-uuid"); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + // Leaf details. CHAIN_POSITION=1 puts the leaf after the full anchor in the + // ordered chain — getChainOrderedLeafToRoot sorts by CHAIN_POSITION descending. + BackupDetailVO leafChainId = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO leafChainPos = new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_POSITION, "1", true); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)).thenReturn(leafChainId); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(leafChainPos); + + // Parent is the tombstoned full anchor (CHAIN_POSITION=0). + BackupDetailVO parentChainId = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true); + BackupDetailVO parentChainPos = new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true); + // The parent is the tombstone — now represented by Status.Hidden (was the DELETE_PENDING detail). + parent.setStatus(Backup.Status.Hidden); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)).thenReturn(parentChainId); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)).thenReturn(parentChainPos); + + // We still use a mutable list + remove() answer so the DAO contract is realistic. + java.util.List liveBackups = new java.util.ArrayList<>(List.of(parent, leaf)); + Mockito.when(backupDao.listByVmId(null, vmId)).thenAnswer(inv -> new java.util.ArrayList<>(liveBackups)); + // The target row is re-read under the chain lock before any chain decision. + Mockito.when(backupDao.findById(51L)).thenReturn(leaf); + Mockito.doReturn(mock(GlobalLock.class)).when(nasBackupProvider).acquireChainDeleteLock(vmId); + + // Agent acknowledges every delete. + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class))) + .thenReturn(new BackupAnswer(new DeleteBackupCommand(null, null, null, null), true, "ok")); + // backupDao.remove(id) drops the corresponding row from the live list so the next + // listByVmId call reflects post-delete state — mirrors the real DAO contract. + Mockito.when(backupDao.remove(Mockito.anyLong())).thenAnswer(inv -> { + Long id = inv.getArgument(0); + liveBackups.removeIf(b -> b.getId() == id); + return true; + }); + + boolean result = nasBackupProvider.deleteBackup(leaf, false); + Assert.assertTrue(result); + + // Both backups must be physically deleted (leaf first, then tombstoned parent). + Mockito.verify(agentManager, Mockito.times(2)) + .send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + Mockito.verify(backupDao).remove(51L); + Mockito.verify(backupDao).remove(50L); + // Exactly-once resource accounting: decremented for BOTH physically-removed backups + // (leaf + swept ancestor), not just one. + Mockito.verify(resourceLimitMgr, Mockito.times(2)) + .decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup)); + Mockito.verify(resourceLimitMgr, Mockito.times(2)) + .decrementResourceCount(Mockito.anyLong(), Mockito.eq(Resource.ResourceType.backup_storage), Mockito.any()); + } + + @Test + public void deletingLastLiveMemberCollectsDeeperOrphanTombstones() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO full = new BackupVO(); + full.setVmId(vmId); + full.setBackupOfferingId(offeringId); + full.setExternalId("i-2-2-VM/2026.05.10.10.00.00"); + full.setZoneId(zoneId); + full.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(full, "id", 50L); + + // The last live incremental — the one being deleted. + BackupVO inc1 = new BackupVO(); + inc1.setVmId(vmId); + inc1.setBackupOfferingId(offeringId); + inc1.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + inc1.setZoneId(zoneId); + inc1.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(inc1, "id", 51L); + + // A stranded tombstone deeper in the chain: its own descendants are long gone, so + // only a sweep triggered by an ancestor's deletion can ever collect it. + BackupVO orphan = new BackupVO(); + orphan.setVmId(vmId); + orphan.setBackupOfferingId(offeringId); + orphan.setExternalId("i-2-2-VM/2026.05.10.11.00.00"); + orphan.setZoneId(zoneId); + orphan.setStatus(Backup.Status.Hidden); + ReflectionTestUtils.setField(orphan, "id", 52L); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true)); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_POSITION, "1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_POSITION, "2", true)); + + Mockito.when(backupDao.listByVmId(null, vmId)).thenReturn(List.of(full, inc1, orphan)); + Mockito.when(backupDao.findById(51L)).thenReturn(inc1); + Mockito.doReturn(mock(GlobalLock.class)).when(nasBackupProvider).acquireChainDeleteLock(vmId); + + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class))) + .thenReturn(new BackupAnswer(new DeleteBackupCommand(null, null, null, null), true, "ok")); + + Assert.assertTrue(nasBackupProvider.deleteBackup(inc1, false)); + + // inc1 and the deeper orphan tombstone are physically removed; the live full survives. + Mockito.verify(agentManager, Mockito.times(2)) + .send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + Mockito.verify(backupDao).remove(51L); + Mockito.verify(backupDao).remove(52L); + Mockito.verify(backupDao, Mockito.never()).remove(50L); + } + + @Test + public void deletingAncestorOfTombstoneWithLiveDescendantTombstonesIt() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO full = new BackupVO(); + full.setVmId(vmId); + full.setBackupOfferingId(offeringId); + full.setExternalId("i-2-2-VM/2026.05.10.10.00.00"); + full.setZoneId(zoneId); + full.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(full, "id", 50L); + + BackupVO inc1 = new BackupVO(); + inc1.setVmId(vmId); + inc1.setBackupOfferingId(offeringId); + inc1.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + inc1.setZoneId(zoneId); + inc1.setStatus(Backup.Status.Hidden); + ReflectionTestUtils.setField(inc1, "id", 51L); + + BackupVO inc2 = new BackupVO(); + inc2.setVmId(vmId); + inc2.setBackupOfferingId(offeringId); + inc2.setExternalId("i-2-2-VM/2026.05.10.11.00.00"); + inc2.setZoneId(zoneId); + inc2.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(inc2, "id", 52L); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true)); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_POSITION, "2", true)); + + Mockito.when(backupDao.listByVmId(null, vmId)).thenReturn(List.of(full, inc1, inc2)); + Mockito.when(backupDao.findById(50L)).thenReturn(full); + Mockito.doReturn(mock(GlobalLock.class)).when(nasBackupProvider).acquireChainDeleteLock(vmId); + + Assert.assertTrue(nasBackupProvider.deleteBackup(full, false)); + + // The full anchor must NOT be physically deleted — inc2 (live) still restores + // through inc1's and full's files. It becomes a tombstone instead. + Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + Mockito.verify(backupDao, Mockito.never()).remove(Mockito.anyLong()); + ArgumentCaptor captor = ArgumentCaptor.forClass(BackupVO.class); + Mockito.verify(backupDao).update(Mockito.eq(50L), captor.capture()); + Assert.assertEquals(Backup.Status.Hidden, captor.getValue().getStatus()); + } + + @Test + public void sweepContinuesPastFailedTombstoneDelete() + throws AgentUnavailableException, OperationTimedoutException { + Long zoneId = 1L; + Long vmId = 2L; + Long hostId = 3L; + Long offeringId = 4L; + + BackupVO full = new BackupVO(); + full.setVmId(vmId); + full.setBackupOfferingId(offeringId); + full.setExternalId("i-2-2-VM/2026.05.10.10.00.00"); + full.setZoneId(zoneId); + full.setStatus(Backup.Status.Hidden); + ReflectionTestUtils.setField(full, "id", 50L); + + BackupVO inc1 = new BackupVO(); + inc1.setVmId(vmId); + inc1.setBackupOfferingId(offeringId); + inc1.setExternalId("i-2-2-VM/2026.05.10.10.30.00"); + inc1.setZoneId(zoneId); + inc1.setStatus(Backup.Status.Hidden); + ReflectionTestUtils.setField(inc1, "id", 51L); + + // The last live member — deleting it triggers the sweep of both tombstones. + BackupVO inc2 = new BackupVO(); + inc2.setVmId(vmId); + inc2.setBackupOfferingId(offeringId); + inc2.setExternalId("i-2-2-VM/2026.05.10.11.00.00"); + inc2.setZoneId(zoneId); + inc2.setStatus(Backup.Status.BackedUp); + ReflectionTestUtils.setField(inc2, "id", 52L); + + VMInstanceVO vm = mock(VMInstanceVO.class); + Mockito.when(vm.getLastHostId()).thenReturn(hostId); + HostVO host = mock(HostVO.class); + Mockito.when(host.getStatus()).thenReturn(Status.Up); + Mockito.when(host.getId()).thenReturn(hostId); + Mockito.when(hostDao.findById(hostId)).thenReturn(host); + + BackupRepositoryVO repo = new BackupRepositoryVO(1L, "nas", "test-repo", + "nfs", "address", "sync", 1024L, null); + Mockito.when(backupRepositoryDao.findByBackupOfferingId(offeringId)).thenReturn(repo); + Mockito.when(vmInstanceDao.findByIdIncludingRemoved(vmId)).thenReturn(vm); + + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(50L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(50L, NASBackupChainKeys.CHAIN_POSITION, "0", true)); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(51L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(51L, NASBackupChainKeys.CHAIN_POSITION, "1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_ID)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_ID, "chain-1", true)); + Mockito.when(backupDetailsDao.findDetail(52L, NASBackupChainKeys.CHAIN_POSITION)) + .thenReturn(new BackupDetailVO(52L, NASBackupChainKeys.CHAIN_POSITION, "2", true)); + + Mockito.when(backupDao.listByVmId(null, vmId)).thenReturn(List.of(full, inc1, inc2)); + Mockito.when(backupDao.findById(52L)).thenReturn(inc2); + Mockito.doReturn(mock(GlobalLock.class)).when(nasBackupProvider).acquireChainDeleteLock(vmId); + + // Deletes run leaf-to-root: inc2 succeeds, inc1's rm FAILS, full succeeds. + DeleteBackupCommand dummy = new DeleteBackupCommand(null, null, null, null); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class))) + .thenReturn(new BackupAnswer(dummy, true, "ok"), + new BackupAnswer(dummy, false, "rm failed"), + new BackupAnswer(dummy, true, "ok")); + + Assert.assertTrue(nasBackupProvider.deleteBackup(inc2, false)); + + // All three members were attempted; the failed inc1 keeps its row, full is collected. + Mockito.verify(agentManager, Mockito.times(3)) + .send(Mockito.anyLong(), Mockito.any(DeleteBackupCommand.class)); + Mockito.verify(backupDao).remove(52L); + Mockito.verify(backupDao, Mockito.never()).remove(51L); + Mockito.verify(backupDao).remove(50L); + } } diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index 66b633e11a94..1cf962edae51 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.backup; +import com.cloud.agent.AgentManager; import com.cloud.dc.dao.ClusterDao; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -134,6 +135,9 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid @Inject private DiskOfferingDao diskOfferingDao; + @Inject + private AgentManager agentMgr; + private static String getUrlDomain(String url) throws URISyntaxException { URI uri; try { @@ -251,8 +255,13 @@ private String executeBackupCommand(HostVO host, String username, String passwor String nstRegex = "\\bcompleted savetime=([0-9]{10})"; Pattern saveTimePattern = Pattern.compile(nstRegex); + if (host == null) { + LOG.warn("Unable to take backup, host is null"); + return null; + } + try { - Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22, + Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host), username, null, password, command, 120000, 120000, 3600000); if (!response.first()) { LOG.error("Backup Script failed on HYPERVISOR {} due to: {}", host, response.second()); @@ -271,9 +280,13 @@ private String executeBackupCommand(HostVO host, String username, String passwor return null; } private boolean executeRestoreCommand(HostVO host, String username, String password, String command) { + if (host == null) { + LOG.warn("Unable to restore backup, host is null"); + return false; + } try { - Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22, + Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host), username, null, password, command, 120000, 120000, 3600000); if (!response.first()) { @@ -344,7 +357,7 @@ public boolean removeVMFromBackupOffering(VirtualMachine vm) { } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { String networkerServer; HostVO hostVO; @@ -394,7 +407,8 @@ public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { String networkerServer; VolumeVO volume = volumeDao.findByUuid(backupVolumeInfo.getUuid()); final DiskOffering diskOffering = diskOfferingDao.findByUuid(backupVolumeInfo.getDiskOfferingId()); @@ -478,7 +492,7 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated) { String networkerServer; String clusterName; @@ -635,7 +649,7 @@ public void syncBackupStorageStats(Long zoneId) { public boolean willDeleteBackupsOnOfferingRemoval() { return false; } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { return new Pair<>(true, null); } } diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java index 39970dab3427..9b34af2d6f49 100644 --- a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -219,7 +219,7 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM) { + public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated) { final VeeamClient client = getClient(vm.getDataCenterId()); Boolean result = client.startBackupJob(vm.getBackupExternalId()); return new Pair<>(result, null); @@ -256,7 +256,7 @@ public boolean deleteBackup(Backup backup, boolean forced) { } @Override - public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup, boolean quickRestore, Long hostId) { final String restorePointId = backup.getExternalId(); try { return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId); @@ -291,7 +291,8 @@ private void prepareForBackupRestoration(VirtualMachine vm) { } @Override - public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, Pair vmNameAndState) { + public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeInfo backupVolumeInfo, String hostIp, String dataStoreUuid, + Pair vmNameAndState, VirtualMachine vm, boolean quickRestore) { final Long zoneId = backup.getZoneId(); final String restorePointId = backup.getExternalId(); return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, null, hostIp, dataStoreUuid); @@ -337,7 +338,7 @@ public List listRestorePoints(VirtualMachine vm) { } @Override - public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid) { + public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, String hostIp, String dataStoreUuid, boolean quickrestore) { final Long zoneId = backup.getZoneId(); final String restorePointId = backup.getExternalId(); final String restoreLocation = vm.getInstanceName(); diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java index 5ff036fef12f..d018d488c64a 100644 --- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java +++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java @@ -40,17 +40,17 @@ public final class RootCACustomTrustManager implements X509TrustManager { private boolean authStrictness = true; private boolean allowExpiredCertificate = true; private CrlDao crlDao; - private X509Certificate caCertificate; + private List caCertificates; private Map activeCertMap; - public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final X509Certificate caCertificate, final CrlDao crlDao) { + public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final List caCertificates, final CrlDao crlDao) { if (StringUtils.isNotEmpty(clientAddress)) { this.clientAddress = clientAddress.replace("/", "").split(":")[0]; } this.authStrictness = authStrictness; this.allowExpiredCertificate = allowExpiredCertificate; this.activeCertMap = activeCertMap; - this.caCertificate = caCertificate; + this.caCertificates = caCertificates; this.crlDao = crlDao; } @@ -151,6 +151,6 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) thr @Override public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{caCertificate}; + return caCertificates.toArray(new X509Certificate[0]); } } diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java index 25c45ed2a102..afb4f561160e 100644 --- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java +++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -40,7 +40,6 @@ import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; @@ -60,6 +59,7 @@ import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.ValidatedConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.utils.security.CertUtils; import org.apache.cloudstack.utils.security.KeyStoreUtils; @@ -92,6 +92,7 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con private static KeyPair caKeyPair = null; private static X509Certificate caCertificate = null; + private static List caCertificates = null; private static KeyStore managementKeyStore = null; @Inject @@ -103,20 +104,25 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con /////////////// Root CA Settings /////////////////// //////////////////////////////////////////////////// - private static ConfigKey rootCAPrivateKey = new ConfigKey<>("Hidden", String.class, - "ca.plugin.root.private.key", - null, - "The ROOT CA private key.", true); - - private static ConfigKey rootCAPublicKey = new ConfigKey<>("Hidden", String.class, - "ca.plugin.root.public.key", - null, - "The ROOT CA public key.", true); - - private static ConfigKey rootCACertificate = new ConfigKey<>("Hidden", String.class, - "ca.plugin.root.ca.certificate", - null, - "The ROOT CA certificate.", true); + private static ConfigKey rootCAPrivateKey = new ValidatedConfigKey<>("Hidden", String.class, + "ca.plugin.root.private.key", null, + "The ROOT CA private key in PEM format. " + + "When set along with the public key and certificate, CloudStack uses this custom CA instead of auto-generating one. " + + "All three ca.plugin.root.* keys must be set together. Restart management server(s) when changed.", + false, ConfigKey.Scope.Global, null, RootCAProvider::validatePrivateKeyPem); + + private static ConfigKey rootCAPublicKey = new ValidatedConfigKey<>("Hidden", String.class, + "ca.plugin.root.public.key", null, + "The ROOT CA public key in PEM format (X.509/SPKI: must start with '-----BEGIN PUBLIC KEY-----'). " + + "Required when providing a custom CA. Restart management server(s) when changed.", + false, ConfigKey.Scope.Global, null, RootCAProvider::validatePublicKeyPem); + + private static ConfigKey rootCACertificate = new ValidatedConfigKey<>("Hidden", String.class, + "ca.plugin.root.ca.certificate", null, + "The CA certificate(s) in PEM format (must start with '-----BEGIN CERTIFICATE-----'). " + + "For intermediate CAs, concatenate the signing cert first, followed by intermediate(s) and root. " + + "Required when providing a custom CA. Restart management server(s) when changed.", + false, ConfigKey.Scope.Global, null, RootCAProvider::validateCACertificatePem); private static ConfigKey rootCAIssuerDN = new ConfigKey<>("Advanced", String.class, "ca.plugin.root.issuer.dn", @@ -151,7 +157,7 @@ private Certificate generateCertificate(final List domainNames, final Li caCertificate, caKeyPair, keyPair.getPublic(), subject, CAManager.CertSignatureAlgorithm.value(), validityDays, domainNames, ipAddresses); - return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate)); + return new Certificate(clientCertificate, keyPair.getPrivate(), caCertificates); } private Certificate generateCertificateUsingCsr(final String csr, final List names, final List ips, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException, OperatorCreationException { @@ -205,7 +211,7 @@ private Certificate generateCertificateUsingCsr(final String csr, final List getCaCertificate() { - return Collections.singletonList(caCertificate); + return caCertificates; } @Override @@ -254,8 +260,8 @@ public boolean revokeCertificate(final BigInteger certSerial, final String certC private KeyStore getCaKeyStore() throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException { final KeyStore ks = KeyStore.getInstance("JKS"); ks.load(null, null); - if (caKeyPair != null && caCertificate != null) { - ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(), new X509Certificate[]{caCertificate}); + if (caKeyPair != null && CollectionUtils.isNotEmpty(caCertificates)) { + ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), getKeyStorePassphrase(), caCertificates.toArray(new X509Certificate[0])); } else { return null; } @@ -274,7 +280,7 @@ public SSLEngine createSSLEngine(final SSLContext sslContext, final String remot final boolean authStrictness = rootCAAuthStrictness.value(); final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value(); - TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificate, crlDao)}; + TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificates, crlDao)}; sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); final SSLEngine sslEngine = sslContext.createSSLEngine(); @@ -316,33 +322,39 @@ private boolean saveNewRootCAKeypair() { if (!configDao.update(rootCAPrivateKey.key(), rootCAPrivateKey.category(), CertUtils.privateKeyToPem(keyPair.getPrivate()))) { logger.error("Failed to save RootCA private key"); } + caKeyPair = keyPair; } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) { logger.error("Failed to generate/save RootCA private/public keys due to exception:", e); } - return loadRootCAKeyPair(); + return caKeyPair != null && caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null; } - private boolean saveNewRootCACertificate() { + boolean saveNewRootCACertificate() { if (caKeyPair == null) { throw new CloudRuntimeException("Cannot issue self-signed root CA certificate as CA keypair is not initialized"); } try { logger.debug("Generating root CA certificate"); - final X509Certificate rootCaCertificate = CertUtils.generateV3Certificate( + final X509Certificate generatedCACert = CertUtils.generateV3Certificate( null, caKeyPair, caKeyPair.getPublic(), rootCAIssuerDN.value(), CAManager.CertSignatureAlgorithm.value(), getCaValidityDays(), null, null); - if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(rootCaCertificate))) { + if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(generatedCACert))) { logger.error("Failed to update RootCA public/x509 certificate"); } + caCertificates = new ArrayList<>(java.util.Collections.singletonList(generatedCACert)); + caCertificate = generatedCACert; } catch (final CertificateException | NoSuchAlgorithmException | NoSuchProviderException | SignatureException | InvalidKeyException | OperatorCreationException | IOException e) { logger.error("Failed to generate RootCA certificate from private/public keys due to exception:", e); return false; } - return loadRootCACertificate(); + return caCertificate != null; } private boolean loadRootCAKeyPair() { + if (caKeyPair != null) { + return true; + } if (StringUtils.isAnyEmpty(rootCAPublicKey.value(), rootCAPrivateKey.value())) { return false; } @@ -355,14 +367,35 @@ private boolean loadRootCAKeyPair() { return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null; } - private boolean loadRootCACertificate() { + boolean loadRootCACertificate() { + if (caCertificate != null && CollectionUtils.isNotEmpty(caCertificates)) { + return true; + } + caCertificate = null; + caCertificates = null; if (StringUtils.isEmpty(rootCACertificate.value())) { return false; } try { - caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value()); - caCertificate.verify(caKeyPair.getPublic()); - } catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) { + final List loadedCerts = CertUtils.pemToX509Certificates(rootCACertificate.value()); + if (CollectionUtils.isEmpty(loadedCerts)) { + logger.error("No certificates found in ca.plugin.root.ca.certificate"); + return false; + } + final X509Certificate loadedCACert = loadedCerts.get(0); + + // Verify key ownership without enforcing self-signature + if (!loadedCACert.getPublicKey().equals(caKeyPair.getPublic())) { + logger.error("The public key in the CA certificate does not match the configured CA public key"); + return false; + } + + if (loadedCerts.size() > 1) { + logger.info("Loaded CA certificate chain with {} certificate(s)", loadedCerts.size()); + } + caCertificates = loadedCerts; + caCertificate = loadedCACert; + } catch (final IOException | CertificateException e) { logger.error("Failed to load saved RootCA certificate due to exception:", e); return false; } @@ -389,9 +422,15 @@ private boolean loadManagementKeyStore() { try { managementKeyStore = KeyStore.getInstance("JKS"); managementKeyStore.load(null, null); - managementKeyStore.setCertificateEntry(caAlias, caCertificate); + int caIndex = 0; + for (final X509Certificate cert : caCertificates) { + managementKeyStore.setCertificateEntry(caAlias + "-" + caIndex++, cert); + } + final List fullChain = new ArrayList<>(); + fullChain.add(serverCertificate.getClientCertificate()); + fullChain.addAll(caCertificates); managementKeyStore.setKeyEntry(managementAlias, serverCertificate.getPrivateKey(), getKeyStorePassphrase(), - new X509Certificate[]{serverCertificate.getClientCertificate(), caCertificate}); + fullChain.toArray(new X509Certificate[0])); } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) { logger.error("Failed to load root CA management-server keystore due to exception: ", e); return false; @@ -421,14 +460,63 @@ protected void addConfiguredManagementIp(List ipList) { } + private static void validatePrivateKeyPem(String value) { + if (StringUtils.isEmpty(value)) return; + try { + CertUtils.pemToPrivateKey(value); + } catch (InvalidKeySpecException | IOException e) { + throw new IllegalArgumentException( + "ca.plugin.root.private.key is not a valid PEM private key: " + e.getMessage()); + } + } + + private static void validatePublicKeyPem(String value) { + if (StringUtils.isEmpty(value)) return; + try { + CertUtils.pemToPublicKey(value); + } catch (InvalidKeySpecException | IOException e) { + throw new IllegalArgumentException( + "ca.plugin.root.public.key is not a valid PEM public key: " + e.getMessage()); + } + } + + static void validateCACertificatePem(String value) { + if (StringUtils.isEmpty(value)) return; + try { + final List certs = CertUtils.pemToX509Certificates(value); + if (CollectionUtils.isEmpty(certs)) { + throw new IllegalArgumentException( + "ca.plugin.root.ca.certificate contains no certificates"); + } + } catch (IOException | CertificateException e) { + throw new IllegalArgumentException( + "ca.plugin.root.ca.certificate is not a valid PEM certificate: " + e.getMessage()); + } + } + private boolean setupCA() { - if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) { - logger.error("Failed to save and load root CA keypair"); - return false; + if (!loadRootCAKeyPair()) { + if (hasUserProvidedCAKeys()) { + logger.error("Failed to load user-provided CA keys from configuration. " + + "Check that ca.plugin.root.private.key, ca.plugin.root.public.key, and " + + "ca.plugin.root.ca.certificate are all set and in the correct PEM format. " + + "Overwriting with auto-generated keys."); + } + if (!saveNewRootCAKeypair()) { + logger.error("Failed to save and load root CA keypair"); + return false; + } } - if (!loadRootCACertificate() && !saveNewRootCACertificate()) { - logger.error("Failed to save and load root CA certificate"); - return false; + if (!loadRootCACertificate()) { + if (hasUserProvidedCAKeys()) { + logger.error("Failed to load user-provided CA certificate. " + + "Check that ca.plugin.root.ca.certificate is set and in PEM format. " + + "Overwriting with auto-generated certificate."); + } + if (!saveNewRootCACertificate()) { + logger.error("Failed to save and load root CA certificate"); + return false; + } } if (!loadManagementKeyStore()) { logger.error("Failed to check and configure management server keystore"); @@ -437,10 +525,16 @@ private boolean setupCA() { return true; } + private boolean hasUserProvidedCAKeys() { + return StringUtils.isNotEmpty(rootCAPublicKey.value()) + || StringUtils.isNotEmpty(rootCAPrivateKey.value()) + || StringUtils.isNotEmpty(rootCACertificate.value()); + } + @Override public boolean start() { managementCertificateCustomSAN = CAManager.CertManagementCustomSubjectAlternativeName.value(); - return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore(); + return loadRootCAKeyPair() && loadRootCACertificate() && loadManagementKeyStore(); } @Override diff --git a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java index d4ded3023321..714e18c3449f 100644 --- a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java +++ b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java @@ -23,9 +23,11 @@ import java.security.KeyPair; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.List; import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Assert; @@ -63,14 +65,14 @@ public void setUp() throws Exception { @Test public void testAuthNotStrictWithInvalidCert() throws Exception { - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(null, null); } @Test public void testAuthNotStrictWithRevokedCert() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO()); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), caCertificate); @@ -79,7 +81,7 @@ public void testAuthNotStrictWithRevokedCert() throws Exception { @Test public void testAuthNotStrictWithInvalidCertOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), caCertificate); @@ -88,14 +90,14 @@ public void testAuthNotStrictWithInvalidCertOwnership() throws Exception { @Test(expected = CertificateException.class) public void testAuthNotStrictWithDenyExpiredCertAndOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); } @Test public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); Assert.assertTrue(certMap.containsKey(clientIp)); Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); @@ -103,35 +105,50 @@ public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws Exception @Test(expected = CertificateException.class) public void testAuthStrictWithInvalidCert() throws Exception { - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(null, null); } @Test(expected = CertificateException.class) public void testAuthStrictWithRevokedCert() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new CrlVO()); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); } @Test(expected = CertificateException.class) public void testAuthStrictWithInvalidCertOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, "RSA"); } @Test(expected = CertificateException.class) public void testAuthStrictWithDenyExpiredCertAndOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, false, certMap, Collections.singletonList(caCertificate), crlDao); trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); } + @Test + public void testGetAcceptedIssuersWithChain() throws Exception { + final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(), + "CN=root", "SHA256withRSA", 365, null, null); + final List chain = Arrays.asList(caCertificate, rootCert); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager( + clientIp, false, true, certMap, chain, crlDao); + + final X509Certificate[] issuers = trustManager.getAcceptedIssuers(); + Assert.assertEquals(2, issuers.length); + Assert.assertEquals(caCertificate, issuers[0]); + Assert.assertEquals(rootCert, issuers[1]); + } + @Test public void testAuthStrictWithAllowExpiredCertAndOwnership() throws Exception { Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null); - final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao); + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, Collections.singletonList(caCertificate), crlDao); Assert.assertTrue(trustManager.getAcceptedIssuers() != null); Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1); Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate); diff --git a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java index 8311f4d45abc..21f00c66a1df 100644 --- a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java +++ b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -38,6 +39,7 @@ import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.utils.security.CertUtils; import org.apache.cloudstack.utils.security.SSLUtils; import org.bouncycastle.asn1.x509.GeneralName; @@ -49,7 +51,6 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) @@ -75,7 +76,7 @@ public void setUp() throws Exception { addField(provider, "caKeyPair", caKeyPair); addField(provider, "caCertificate", caCertificate); - addField(provider, "caKeyPair", caKeyPair); + addField(provider, "caCertificates", Collections.singletonList(caCertificate)); } @After @@ -129,6 +130,46 @@ public void testIssueCertificateWithCsr() throws NoSuchProviderException, Certif certificate.getClientCertificate().verify(caCertificate.getPublicKey()); } + @Test + public void testGetCaCertificateWithChain() throws Exception { + final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(), + "CN=root", "SHA256withRSA", 365, null, null); + final KeyPair intermediateKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate intermediateCert = CertUtils.generateV3Certificate(rootCert, rootKeyPair, + intermediateKeyPair.getPublic(), "CN=intermediate", "SHA256withRSA", 365, null, null); + + final List chain = Arrays.asList(intermediateCert, rootCert); + addField(provider, "caKeyPair", intermediateKeyPair); + addField(provider, "caCertificate", intermediateCert); + addField(provider, "caCertificates", chain); + + Assert.assertEquals(2, provider.getCaCertificate().size()); + Assert.assertEquals(intermediateCert, provider.getCaCertificate().get(0)); + Assert.assertEquals(rootCert, provider.getCaCertificate().get(1)); + } + + @Test + public void testIssueCertificateWithoutCsrAndChain() throws Exception { + final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate rootCert = CertUtils.generateV3Certificate(null, rootKeyPair, rootKeyPair.getPublic(), + "CN=root", "SHA256withRSA", 365, null, null); + final KeyPair intermediateKeyPair = CertUtils.generateRandomKeyPair(1024); + final X509Certificate intermediateCert = CertUtils.generateV3Certificate(rootCert, rootKeyPair, + intermediateKeyPair.getPublic(), "CN=intermediate", "SHA256withRSA", 365, null, null); + + addField(provider, "caKeyPair", intermediateKeyPair); + addField(provider, "caCertificate", intermediateCert); + addField(provider, "caCertificates", Arrays.asList(intermediateCert, rootCert)); + + final Certificate certificate = provider.issueCertificate(Arrays.asList("domain1.com"), null, 1); + Assert.assertNotNull(certificate); + Assert.assertEquals(2, certificate.getCaCertificates().size()); + Assert.assertEquals(intermediateCert, certificate.getCaCertificates().get(0)); + Assert.assertEquals(rootCert, certificate.getCaCertificates().get(1)); + certificate.getClientCertificate().verify(intermediateKeyPair.getPublic()); + } + @Test public void testRevokeCertificate() throws Exception { Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), "anyString")); @@ -177,8 +218,8 @@ public void testIsManagementCertificateNoAltNames() { } @Test - public void testIsManagementCertificateNoMatch() { - ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", "cloudstack"); + public void testIsManagementCertificateNoMatch() throws Exception { + addField(provider, "managementCertificateCustomSAN", "cloudstack"); try { X509Certificate certificate = Mockito.mock(X509Certificate.class); List> altNames = new ArrayList<>(); @@ -193,9 +234,9 @@ public void testIsManagementCertificateNoMatch() { } @Test - public void testIsManagementCertificateMatch() { + public void testIsManagementCertificateMatch() throws Exception { String customSAN = "cloudstack"; - ReflectionTestUtils.setField(provider, "managementCertificateCustomSAN", customSAN); + addField(provider, "managementCertificateCustomSAN", customSAN); try { X509Certificate certificate = Mockito.mock(X509Certificate.class); List> altNames = new ArrayList<>(); @@ -208,4 +249,58 @@ public void testIsManagementCertificateMatch() { Assert.fail(String.format("Exception occurred: %s", e.getMessage())); } } + + @Test + public void testLoadRootCACertificateWithMismatchedCert() throws Exception { + KeyPair otherKeyPair = CertUtils.generateRandomKeyPair(1024); + X509Certificate mismatchedCert = CertUtils.generateV3Certificate(null, otherKeyPair, otherKeyPair.getPublic(), "CN=other", "SHA256withRSA", 365, null, null); + String mismatchedPem = CertUtils.x509CertificateToPem(mismatchedCert); + + ConfigKey mockCertKey = Mockito.mock(ConfigKey.class); + Mockito.when(mockCertKey.value()).thenReturn(mismatchedPem); + addField(provider, "rootCACertificate", mockCertKey); + + addField(provider, "caCertificate", null); + addField(provider, "caCertificates", null); + + Boolean result = provider.loadRootCACertificate(); + Assert.assertFalse(result); + Assert.assertNull(provider.getCaCertificate()); + } + + @Test + public void testSaveNewRootCACertificateWithStaleCache() throws Exception { + ConfigurationDao configDao = Mockito.mock(ConfigurationDao.class); + addField(provider, "configDao", configDao); + + ConfigKey mockCertKey = Mockito.mock(ConfigKey.class); + Mockito.when(mockCertKey.key()).thenReturn("ca.plugin.root.ca.certificate"); + Mockito.when(mockCertKey.category()).thenReturn("Hidden"); + addField(provider, "rootCACertificate", mockCertKey); + + ConfigKey mockIssuerKey = Mockito.mock(ConfigKey.class); + Mockito.when(mockIssuerKey.value()).thenReturn("CN=ca.cloudstack.apache.org"); + addField(provider, "rootCAIssuerDN", mockIssuerKey); + + addField(provider, "caCertificate", null); + addField(provider, "caCertificates", null); + + Mockito.when(configDao.update(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())).thenReturn(true); + + Boolean result = provider.saveNewRootCACertificate(); + Assert.assertTrue(result); + Assert.assertNotNull(provider.getCaCertificate()); + Assert.assertEquals(1, provider.getCaCertificate().size()); + } + + @Test + public void testValidateCACertificatePem() throws Exception { + String truncatedPem = "-----BEGIN CERTIFICATE-----\nMIICxTCCAa0CAQAw\n"; + try { + RootCAProvider.validateCACertificatePem(truncatedPem); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains("is not a valid PEM certificate")); + } + } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java index 0cec0df66182..e5c89c5de1c5 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java @@ -17,12 +17,9 @@ package org.apache.cloudstack.api.command; import java.util.Date; -import java.util.List; import javax.inject.Inject; -import com.cloud.user.Account; - import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -30,36 +27,41 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaBalanceResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; -import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.api.response.QuotaStatementItemResponse; +import org.apache.commons.lang3.ObjectUtils; -@APICommand(name = "quotaBalance", responseObject = QuotaStatementItemResponse.class, description = "Create a quota balance statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, +@APICommand(name = "quotaBalance", responseObject = QuotaBalanceResponse.class, description = "Create Quota balance statements for the provided Accounts or Projects.", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaBalanceCmd extends BaseCmd { - - - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which statement needs to be generated") + @ACL + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which balance statements will be generated. " + + "Deprecated, please use 'accountid' instead.") private String accountName; @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "If domain Id is given and the caller is domain admin then the statement is generated for domain.") + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain which the Account identified by 'account' belongs to." + + "Deprecated, please use 'accountid' instead.") private Long domainId; - @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End of the period of the Quota balance." + - ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Date of the last Quota balance to be returned. Must be informed together with the " + + "startdate parameter, and can not be before startdate. " + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) private Date endDate; - @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start of the period of the Quota balance. " + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Date of the first Quota balance to be returned. Must be before today. " + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) private Date startDate; @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified Account") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which balance statements will be generated.") private Long accountId; + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which balance statements will be generated. Can not be specified with accountid.") + private Long projectId; + @Inject QuotaResponseBuilder _responseBuilder; @@ -88,48 +90,32 @@ public void setDomainId(Long domainId) { } public Date getEndDate() { - if (endDate == null){ - return null; - } - else{ - return _responseBuilder.startOfNextDay(new Date(endDate.getTime())); - } + return endDate; } public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + this.endDate = endDate; } public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + return startDate; } public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + this.startDate = startDate; } @Override public long getEntityOwnerId() { - if (accountId != null) { - return accountId; - } - Account account = _accountService.getActiveAccountByName(accountName, domainId); - if (account != null) { - return account.getAccountId(); + if (ObjectUtils.allNull(accountId, accountName, domainId, projectId)) { + return -1; } - return Account.ACCOUNT_ID_SYSTEM; + return _accountService.finalizeAccountId(accountId, accountName, domainId, projectId); } @Override public void execute() { - List quotaUsage = _responseBuilder.getQuotaBalance(this); - - QuotaBalanceResponse response; - if (endDate == null) { - response = _responseBuilder.createQuotaLastBalanceResponse(quotaUsage, getStartDate()); - } else { - response = _responseBuilder.createQuotaBalanceResponse(quotaUsage, getStartDate(), new Date(endDate.getTime())); - } + QuotaBalanceResponse response = _responseBuilder.createQuotaBalanceResponse(this); response.setResponseName(getCommandName()); setResponseObject(response); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java index a6d1db41ddd2..0c99efd9a2d2 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsCmd.java @@ -21,14 +21,13 @@ import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; -import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.quota.QuotaService; import javax.inject.Inject; @@ -42,22 +41,35 @@ public class QuotaCreditsCmd extends BaseCmd { @Inject QuotaService _quotaService; - - - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which quota credits need to be added") + @Deprecated + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which Quota credits will be added. Deprecated, please use '" + + ApiConstants.ACCOUNT_ID + "' instead.") private String accountName; @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Domain for which quota credits need to be added") + @Deprecated + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "Domain of the Account specified by '" + ApiConstants.ACCOUNT + "' for which Quota credits will be added. " + + "Deprecated, please use '" + ApiConstants.ACCOUNT_ID + "' instead.") private Long domainId; - @Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Value of the credits to be added+, subtracted-") + @ACL + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which Quota credits will be added. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.") + private Long accountId; + + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which Quota credits will be added. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.") + private Long projectId; + + @Parameter(name = ApiConstants.VALUE, type = CommandType.DOUBLE, required = true, description = "Amount of credits to be added (in case of a positive value) or subtracted (in case of a negative value).") private Double value; - @Parameter(name = "min_balance", type = CommandType.DOUBLE, required = false, description = "Minimum balance threshold of the Account") + @Parameter(name = "min_balance", type = CommandType.DOUBLE, description = "An email will be sent to the Account when the Quota credits get below this threshold.") private Double minBalance; - @Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, required = false, description = "Account for which quota enforce is set to false will not be locked when there is no credit balance") + @Parameter(name = "quota_enforce", type = CommandType.BOOLEAN, description = "Whether to lock the Account when Quota credits are below zero.") private Boolean quotaEnforce; public Double getMinBalance() { @@ -100,31 +112,21 @@ public void setValue(Double value) { this.value = value; } + public Long getAccountId() { + return accountId; + } + + public Long getProjectId() { + return projectId; + } + public QuotaCreditsCmd() { super(); } @Override public void execute() { - Long accountId = null; - Account account = _accountService.getActiveAccountByName(accountName, domainId); - if (account != null) { - accountId = account.getAccountId(); - } - if (accountId == null) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "The Account does not exists or has been removed/disabled"); - } - if (getValue() == null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please send a valid non-empty quota value"); - } - if (getQuotaEnforce() != null) { - _quotaService.setLockAccount(accountId, getQuotaEnforce()); - } - if (getMinBalance() != null) { - _quotaService.setMinBalance(accountId, getMinBalance()); - } - - final QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(accountId, getDomainId(), getValue(), CallContext.current().getCallingUserId(), getQuotaEnforce()); + QuotaCreditsResponse response = _responseBuilder.addQuotaCredits(this); response.setResponseName(getCommandName()); response.setObjectName("quotacredits"); setResponseObject(response); @@ -132,10 +134,6 @@ public void execute() { @Override public long getEntityOwnerId() { - Account account = _accountService.getActiveAccountByName(accountName, domainId); - if (account != null) { - return account.getAccountId(); - } return Account.ACCOUNT_ID_SYSTEM; } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java index 48bb7ef79e70..7555923600d9 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java @@ -18,7 +18,7 @@ import com.cloud.utils.Pair; -import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; @@ -26,8 +26,10 @@ import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; @@ -44,13 +46,16 @@ public class QuotaCreditsListCmd extends BaseCmd { @Inject QuotaResponseBuilder quotaResponseBuilder; - @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which the credit statement will be generated. Cannot be specified with '" + ApiConstants.PROJECT_ID + "'.") private Long accountId; - @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " + - "Available only for administrators.") + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which the credit statement will be generated. Cannot be specified with '" + ApiConstants.ACCOUNT_ID + "'.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which credit statement will be generated. " + + "Available only for administrators.", authorized = {RoleType.Admin, RoleType.DomainAdmin}) private Long domainId; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End date of the credit statement. If not provided, the current date will be " + @@ -97,14 +102,18 @@ public void setStartDate(Date startDate) { this.startDate = startDate; } - public Boolean getRecursive() { - return recursive; + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); } public void setRecursive(Boolean recursive) { this.recursive = recursive; } + public Long getProjectId() { + return projectId; + } + @Override public void execute() { Pair, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this); @@ -116,7 +125,10 @@ public void execute() { @Override public long getEntityOwnerId() { - return -1; + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java new file mode 100644 index 000000000000..00dcb8fa5752 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java @@ -0,0 +1,118 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.api.command; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.cloudstack.api.response.QuotaResourceStatementResponse; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; + +import javax.inject.Inject; + +import java.util.Date; + +@APICommand(name = "quotaResourceStatement", responseObject = QuotaResourceStatementResponse.class, since = "4.23.0.0", + description = "Generates a detailed Quota statement for a specific resource.", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class QuotaResourceStatementCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "ID of the resource.") + private String id; + + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "Generate the statement for this Account. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Account.") + private Long accountId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "Generate the statement for this Project. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Project.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated.") + private Long domainId; + + @Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "Usage type of the Quota usage.") + private Integer usageType; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = true, description = "Start date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + + @Inject + QuotaResponseBuilder responseBuilder; + + @Override + public void execute() { + QuotaResourceStatementResponse response = responseBuilder.createQuotaResourceStatement(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); + } + + public String getId() { + return id; + } + + public Integer getUsageType() { + return usageType; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Long getAccountId() { + return accountId; + } + + public Long getDomainId() { + return domainId; + } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java index d3bd3868ed16..f3a27ea58718 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java @@ -17,35 +17,33 @@ package org.apache.cloudstack.api.command; import java.util.Date; -import java.util.List; import javax.inject.Inject; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaStatementItemResponse; import org.apache.cloudstack.api.response.QuotaStatementResponse; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; -import com.cloud.user.Account; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; -@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a quota statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - httpMethod = "GET") +@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a Quota statement for the provided Account, Project, or Domain.", + since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaStatementCmd extends BaseCmd { - - - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Optional, Account Id for which statement needs to be generated") + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, + description = "Name of the Account for which the Quota statement will be generated. Deprecated, please use accountid instead.") private String accountName; - @ACL - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.") + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated. May be used individually or with account.") private Long domainId; @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End of the period of the Quota statement. " + @@ -56,15 +54,27 @@ public class QuotaStatementCmd extends BaseCmd { ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) private Date startDate; - @Parameter(name = ApiConstants.TYPE, type = CommandType.INTEGER, description = "List quota usage records for the specified usage type") + @Parameter(name = ApiConstants.TYPE, type = CommandType.INTEGER, + description = "Consider only Quota usage records for the specified usage type in the statement.") private Integer usageType; - @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified Account") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "ID of the Account for which the Quota statement will be generated. Can not be specified with projectid.") private Long accountId; + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "ID of the Project for which the Quota statement will be generated. Can not be specified with accountid.", since = "4.23.0") + private Long projectId; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + + @Parameter(name = ApiConstants.SHOW_RESOURCES, type = CommandType.BOOLEAN, description = "List the resources of each Quota type in the period.", since = "4.23.0") + private boolean showResources; + @Inject - private QuotaResponseBuilder _responseBuilder; + QuotaResponseBuilder responseBuilder; public Long getAccountId() { return accountId; @@ -99,43 +109,52 @@ public void setDomainId(Long domainId) { } public Date getEndDate() { - return _responseBuilder.startOfNextDay(endDate == null ? new Date() : new Date(endDate.getTime())); + return endDate; } public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + this.endDate = endDate; } public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + return startDate; } public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + this.startDate = startDate; + } + + public boolean isShowResources() { + return showResources; + } + + public void setShowResources(boolean showResources) { + this.showResources = showResources; + } + + public Long getProjectId() { + return projectId; } @Override public long getEntityOwnerId() { - if (accountId != null) { - return accountId; - } - Account activeAccountByName = _accountService.getActiveAccountByName(accountName, domainId); - if (activeAccountByName != null) { - return activeAccountByName.getAccountId(); + if (ObjectUtils.allNull(accountId, accountName, projectId)) { + return -1; } - return Account.ACCOUNT_ID_SYSTEM; + return _accountService.finalizeAccountId(accountId, accountName, domainId, projectId); } @Override public void execute() { - List quotaUsage = _responseBuilder.getQuotaUsage(this); - - QuotaStatementResponse response = _responseBuilder.createQuotaStatementResponse(quotaUsage); - response.setStartDate(startDate == null ? null : new Date(startDate.getTime())); - response.setEndDate(endDate == null ? null : new Date(endDate.getTime())); - + QuotaStatementResponse response = responseBuilder.createQuotaStatementResponse(this); + response.setStartDate(startDate); + response.setEndDate(endDate); response.setResponseName(getCommandName()); setResponseObject(response); } + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 87322b01f4d4..89ab96ebb9f8 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -16,61 +16,80 @@ //under the License. package org.apache.cloudstack.api.command; -import com.cloud.user.Account; import com.cloud.utils.Pair; + +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaSummaryResponse; -import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.quota.QuotaAccountStateFilter; +import org.apache.cloudstack.quota.QuotaService; +import org.apache.commons.lang3.ObjectUtils; import java.util.List; import javax.inject.Inject; -@APICommand(name = "quotaSummary", responseObject = QuotaSummaryResponse.class, description = "Lists balance and quota usage for all Accounts", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - httpMethod = "GET") +@APICommand(name = "quotaSummary", responseObject = QuotaSummaryResponse.class, description = "Lists Quota balance summary of Accounts and Projects.", since = "4.7.0", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaSummaryCmd extends BaseListCmd { - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Optional, Account Id for which statement needs to be generated") + @Inject + QuotaResponseBuilder quotaResponseBuilder; + + @Inject + QuotaService quotaService; + + @ACL + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which balance will be listed. Can not be specified with projectid.", since = "4.23.0") + private Long accountId; + + @ACL + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Name of the Account for which balance will be listed.") private String accountName; - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.") + @ACL + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "ID of the Domain for which balance will be listed. May be used individually or with accountname.") private Long domainId; - @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, required = false, description = "Optional, to list all Accounts irrespective of the quota activity") + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "False (default) lists the Quota balance summary for calling Account. True lists balance summary for " + + "Accounts which the caller has access. If domain ID is informed, this parameter is considered as true.") private Boolean listAll; - @Inject - QuotaResponseBuilder _responseBuilder; + @Parameter(name = ApiConstants.ACCOUNT_STATE_TO_SHOW, type = CommandType.STRING, description = "Possible values are [ALL, ACTIVE, REMOVED]. ALL will list summaries for " + + "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.", + since = "4.23.0") + private String accountStateToShow; - public QuotaSummaryCmd() { - super(); - } + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which balance will be listed. Can not be specified with accountId.", since = "4.23.0") + private Long projectId; @Override public void execute() { - Account caller = CallContext.current().getCallingAccount(); - Pair, Integer> responses; - if (caller.getType() == Account.Type.ADMIN) { - if (getAccountName() != null && getDomainId() != null) - responses = _responseBuilder.createQuotaSummaryResponse(getAccountName(), getDomainId()); - else - responses = _responseBuilder.createQuotaSummaryResponse(isListAll(), getKeyword(), getStartIndex(), getPageSizeVal()); - } else { - responses = _responseBuilder.createQuotaSummaryResponse(caller.getAccountName(), caller.getDomainId()); - } - final ListResponse response = new ListResponse(); + Pair, Integer> responses = quotaResponseBuilder.createQuotaSummaryResponse(this); + ListResponse response = new ListResponse<>(); response.setResponses(responses.first(), responses.second()); response.setResponseName(getCommandName()); setResponseObject(response); } + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + public String getAccountName() { return accountName; } @@ -88,16 +107,35 @@ public void setDomainId(Long domainId) { } public Boolean isListAll() { - return listAll == null ? false: listAll; + // If a domain ID was specified, then allow listing all summaries of domain + return ObjectUtils.defaultIfNull(listAll, Boolean.FALSE) || domainId != null; } public void setListAll(Boolean listAll) { this.listAll = listAll; } + public Long getProjectId() { + if (projectId == null || projectId == -1L) { + return null; + } + return projectId; + } + + public QuotaAccountStateFilter getAccountStateToShow() { + QuotaAccountStateFilter state = QuotaAccountStateFilter.getValue(accountStateToShow); + if (state != null) { + return state; + } + return QuotaAccountStateFilter.ACTIVE; + } + @Override public long getEntityOwnerId() { - return Account.ACCOUNT_ID_SYSTEM; + Long convertedProjectId = getProjectId(); + if (ObjectUtils.allNull(accountId, accountName, convertedProjectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, accountName, domainId, convertedProjectId); } - } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffDeleteCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffDeleteCmd.java index 7810760c56e7..a5d588c20c32 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffDeleteCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaTariffDeleteCmd.java @@ -49,7 +49,7 @@ public String getId() { @Override public void execute() { - CallContext.current().setEventDetails(String.format("Tariff id: %s", getId())); + CallContext.current().setEventDetails(String.format("Tariff ID: %s", getResourceUuid(ApiConstants.ID))); boolean result = responseBuilder.deleteQuotaTariff(getId()); SuccessResponse response = new SuccessResponse(getCommandName()); response.setSuccess(result); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java index 625f4829c673..714066b048ad 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java @@ -17,137 +17,65 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; import java.util.Date; import java.util.List; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; -import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import com.cloud.serializer.Param; public class QuotaBalanceResponse extends BaseResponse { - @SerializedName("accountid") - @Param(description = "Account ID") - private Long accountId; - - @SerializedName("account") - @Param(description = "Account name") - private String accountName; - - @SerializedName("domain") - @Param(description = "Domain ID") - private Long domainId; - - @SerializedName("startquota") - @Param(description = "Quota started with") - private BigDecimal startQuota; - - @SerializedName("endquota") - @Param(description = "Quota by end of this period") - private BigDecimal endQuota; - - @SerializedName("credits") - @Param(description = "List of credits made during this period") - private List credits = null; - - @SerializedName("startdate") - @Param(description = "Start date") - private Date startDate = null; - - @SerializedName("enddate") - @Param(description = "End date") - private Date endDate = null; - - @SerializedName("currency") - @Param(description = "Currency") + @SerializedName(ApiConstants.CURRENCY) + @Param(description = "Balance's currency.") private String currency; - public QuotaBalanceResponse() { - super(); - credits = new ArrayList(); - } - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public String getAccountName() { - return accountName; - } + @SerializedName(ApiConstants.DATE) + @Param(description = "Balance's date.") + private Date date; - public void setAccountName(String accountName) { - this.accountName = accountName; - } + @SerializedName(ApiConstants.BALANCE) + @Param(description = "Balance's value.") + private BigDecimal balance; - public Long getDomainId() { - return domainId; - } - - public void setDomainId(Long domainId) { - this.domainId = domainId; - } + @SerializedName(ApiConstants.BALANCES) + @Param(description = "List of balances in the period.") + private List balances; - public BigDecimal getStartQuota() { - return startQuota; - } - - public void setStartQuota(BigDecimal startQuota) { - this.startQuota = startQuota.setScale(2, RoundingMode.HALF_EVEN); - } - - public BigDecimal getEndQuota() { - return endQuota; - } - - public void setEndQuota(BigDecimal endQuota) { - this.endQuota = endQuota.setScale(2, RoundingMode.HALF_EVEN); - } - - public List getCredits() { - return credits; - } - - public void setCredits(List credits) { - this.credits = credits; + public QuotaBalanceResponse() { + super("balance"); } - public void addCredits(QuotaBalanceVO credit) { - QuotaCreditsResponse cr = new QuotaCreditsResponse(); - cr.setCredit(credit.getCreditBalance()); - cr.setCreditedOn(credit.getUpdatedOn()); - credits.add(0, cr); + public QuotaBalanceResponse(Date date, BigDecimal balance) { + super("balance"); + this.date = date; + this.balance = balance; } - public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + public void setCurrency(String currency) { + this.currency = currency; } - public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + public void setBalances(List balances) { + this.balances = balances; } - public Date getEndDate() { - return endDate == null ? null : new Date(endDate.getTime()); + public String getCurrency() { + return currency; } - public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + public List getBalances() { + return balances; } - public String getCurrency() { - return currency; + public Date getDate() { + return date; } - public void setCurrency(String currency) { - this.currency = currency; + public BigDecimal getBalance() { + return balance; } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java new file mode 100644 index 000000000000..9f0a28389a9d --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java @@ -0,0 +1,82 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.api.response; + +import java.math.BigDecimal; +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaResourceStatementItemResponse extends BaseResponse { + + @SerializedName(ApiConstants.TARIFF_ID) + @Param(description = "ID of the tariff.") + private String tariffId; + + @SerializedName(ApiConstants.TARIFF_NAME) + @Param(description = "Name of the tariff.") + private String tariffName; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota used.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Item's start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Item's end date.") + private Date endDate; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "UUID of the resource's owner.") + private String accountId; + + public QuotaResourceStatementItemResponse() { + super("quotaresourcestatementitem"); + } + + public void setTariffId(String tariffId) { + this.tariffId = tariffId; + } + + public void setTariffName(String tariffName) { + this.tariffName = tariffName; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java new file mode 100644 index 000000000000..ce3ab1f177ba --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java @@ -0,0 +1,66 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.List; + +public class QuotaResourceStatementResponse extends BaseResponse { + + @SerializedName(ApiConstants.USAGE_NAME) + @Param(description = "Name of the usage type.") + private String usageName; + + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the usage type.") + private String unit; + + @SerializedName(ApiConstants.ITEMS) + @Param(description = "List of Quota tariff usages.", responseObject = QuotaResourceStatementItemResponse.class) + private List items; + + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total amount of quota used.") + private BigDecimal totalQuotaUsed; + + public QuotaResourceStatementResponse() { + super("quotaresourcestatement"); + } + + public void setUsageName(String usageName) { + this.usageName = usageName; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public void setQuotaUsageDetails(List items) { + this.items = items; + } + + public void setTotalQuotaUsed(BigDecimal totalQuotaUsed) { + this.totalQuotaUsed = totalQuotaUsed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index 67f75ebf82fb..c1a677da9355 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -19,19 +19,20 @@ import com.cloud.user.User; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; -import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; import java.util.Date; import java.util.List; @@ -48,23 +49,13 @@ public interface QuotaResponseBuilder { boolean isUserAllowedToSeeActivationRules(User user); - QuotaStatementResponse createQuotaStatementResponse(List quotaUsage); + QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd); - QuotaBalanceResponse createQuotaBalanceResponse(List quotaUsage, Date startDate, Date endDate); + QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd cmd); - Pair, Integer> createQuotaSummaryResponse(Boolean listAll); + Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd); - Pair, Integer> createQuotaSummaryResponse(Boolean listAll, String keyword, Long startIndex, Long pageSize); - - Pair, Integer> createQuotaSummaryResponse(String accountName, Long domainId); - - QuotaBalanceResponse createQuotaLastBalanceResponse(List quotaBalance, Date startDate); - - List getQuotaUsage(QuotaStatementCmd cmd); - - List getQuotaBalance(QuotaBalanceCmd cmd); - - QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce); + QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd); List listQuotaEmailTemplates(QuotaEmailTemplateListCmd cmd); @@ -94,4 +85,6 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd); QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd); + + QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 42e94c1c0170..6727ab58141f 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -21,41 +21,82 @@ import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.math.BigDecimal; -import java.math.RoundingMode; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.ListIterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.inject.Inject; -import com.cloud.exception.PermissionDeniedException; +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.VpnUserVO; +import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.LoadBalancerDao; +import com.cloud.network.dao.LoadBalancerVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.VpnUserDao; +import com.cloud.network.rules.PortForwardingRuleVO; +import com.cloud.network.rules.dao.PortForwardingRulesDao; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.network.security.dao.SecurityGroupDao; +import com.cloud.network.vpc.VpcVO; +import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.storage.BucketVO; +import com.cloud.storage.dao.BucketDao; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.SnapshotVO; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.user.dao.UserDao; import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.Ip; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; @@ -71,25 +112,37 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariables; +import org.apache.cloudstack.quota.activationrule.presetvariables.ResourceCounting; import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.constant.QuotaTypes; + import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaCreditsDao; import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao; import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; +import org.apache.cloudstack.quota.dao.QuotaSummaryDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; +import org.apache.cloudstack.quota.dao.VpcDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; +import org.apache.cloudstack.quota.vo.QuotaUsageResourceVO; +import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.compress.utils.Sets; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; @@ -97,19 +150,6 @@ import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; -import com.cloud.domain.DomainVO; -import com.cloud.domain.dao.DomainDao; -import com.cloud.event.ActionEvent; -import com.cloud.event.EventTypes; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.user.Account; -import com.cloud.user.AccountManager; -import com.cloud.user.AccountVO; -import com.cloud.user.dao.AccountDao; -import com.cloud.user.dao.UserDao; -import com.cloud.utils.Pair; -import com.cloud.utils.db.Filter; - @Component public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { protected Logger logger = LogManager.getLogger(getClass()); @@ -121,7 +161,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private QuotaCreditsDao quotaCreditsDao; @Inject - private QuotaUsageDao _quotaUsageDao; + private QuotaUsageDao quotaUsageDao; @Inject private QuotaEmailTemplatesDao _quotaEmailTemplateDao; @@ -134,27 +174,57 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private QuotaAccountDao quotaAccountDao; @Inject - private DomainDao _domainDao; + private DomainDao domainDao; @Inject private AccountManager _accountMgr; @Inject - private QuotaStatement _statement; + private QuotaStatement quotaStatement; @Inject private QuotaManager _quotaManager; @Inject private QuotaEmailConfigurationDao quotaEmailConfigurationDao; @Inject + private QuotaSummaryDao quotaSummaryDao; + @Inject private JsInterpreterHelper jsInterpreterHelper; @Inject private ApiDiscoveryService apiDiscoveryService; + @Inject + private IPAddressDao ipAddressDao; + @Inject + private NetworkDao networkDao; + @Inject + private VpcDao vpcDao; + @Inject + private NetworkOfferingDao networkOfferingDao; + @Inject + private SnapshotDao snapshotDao; + @Inject + private VMInstanceDao vmInstanceDao; + @Inject + private VMTemplateDao vmTemplateDao; + @Inject + private VolumeDao volumeDao; + @Inject + private BucketDao bucketDao; + @Inject + private VpnUserDao vpnUserDao; + @Inject + private LoadBalancerDao loadBalancerDao; + @Inject + private PortForwardingRulesDao portForwardingRulesDao; + @Inject + private SecurityGroupDao securityGroupDao; + @Inject + private QuotaUsageJoinDao quotaUsageJoinDao; + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + @Inject + private EntityManager entityMgr; - private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; + private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class, ResourceCounting.class}; - protected void checkActivationRulesAllowed(String activationRule) { - if (!_quotaService.isJsInterpretationEnabled() && StringUtils.isNotEmpty(activationRule)) { - throw new PermissionDeniedException("Quota Tariff Activation Rule cannot be set, as Javascript interpretation is disabled in the configuration."); - } - } + private Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); @Override public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) { @@ -180,243 +250,385 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole } @Override - public Pair, Integer> createQuotaSummaryResponse(final String accountName, final Long domainId) { - List result = new ArrayList(); + public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); - if (accountName != null && domainId != null) { - Account account = _accountDao.findActiveAccount(accountName, domainId); - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); + if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { + return getQuotaSummaryResponse(cmd.getEntityOwnerId(), null, null, cmd); } - return new Pair<>(result, result.size()); + return getQuotaSummaryResponseWithListAll(cmd, caller); } - @Override - public Pair, Integer> createQuotaSummaryResponse(Boolean listAll) { - return createQuotaSummaryResponse(listAll, null, null, null); + protected Pair, Integer> getQuotaSummaryResponseWithListAll(QuotaSummaryCmd cmd, Account caller) { + Long domainId = cmd.getDomainId(); + if (domainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + if (domain == null) { + throw new InvalidParameterValueException(String.format("Domain [%s] does not exist.", domainId)); + } + } + + String domainPath = getDomainPathByDomainIdForDomainAdmin(caller); + + Long accountId = cmd.getEntityOwnerId(); + if (accountId == -1) { + accountId = cmd.isListAll() ? null : caller.getAccountId(); + } + + return getQuotaSummaryResponse(accountId, domainId, domainPath, cmd); } - @Override - public Pair, Integer> createQuotaSummaryResponse(Boolean listAll, final String keyword, final Long startIndex, final Long pageSize) { - List result = new ArrayList(); - Integer count = 0; - if (listAll) { - Filter filter = new Filter(AccountVO.class, "accountName", true, startIndex, pageSize); - Pair, Integer> data = _accountDao.findAccountsLike(keyword, filter); - count = data.second(); - for (final AccountVO account : data.first()) { - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); - } - } else { - Pair, Integer> data = quotaAccountDao.listAllQuotaAccount(startIndex, pageSize); - count = data.second(); - for (final QuotaAccountVO quotaAccount : data.first()) { - AccountVO account = _accountDao.findById(quotaAccount.getId()); - if (account == null) { - continue; - } - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); - } + /** + * Retrieves the domain path of the caller's domain (if the caller is Domain Admin) for filtering in the quota summary query. + * @return null if the caller is an Admin or the domain path of the caller's domain if the caller is a Domain Admin. + * @throws InvalidParameterValueException if it cannot find the domain. + */ + protected String getDomainPathByDomainIdForDomainAdmin(Account caller) { + if (caller.getType() != Account.Type.DOMAIN_ADMIN) { + return null; } - return new Pair<>(result, count); + + Long domainId = caller.getDomainId(); + Domain domain = domainDao.findById(domainId); + _accountMgr.checkAccess(caller, domain); + + if (domain == null) { + throw new InvalidParameterValueException(String.format("Domain ID [%s] is invalid.", domainId)); + } + + return domain.getPath(); } - protected QuotaSummaryResponse getQuotaSummaryResponse(final Account account) { - Calendar[] period = _statement.getCurrentStatementTime(); - - if (account != null) { - QuotaSummaryResponse qr = new QuotaSummaryResponse(); - DomainVO domain = _domainDao.findById(account.getDomainId()); - BigDecimal curBalance = _quotaBalanceDao.lastQuotaBalance(account.getAccountId(), account.getDomainId(), period[1].getTime()); - BigDecimal quotaUsage = _quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, period[0].getTime(), period[1].getTime()); - - qr.setAccountId(account.getUuid()); - qr.setAccountName(account.getAccountName()); - qr.setDomainId(domain.getUuid()); - qr.setDomainName(domain.getName()); - qr.setBalance(curBalance); - qr.setQuotaUsage(quotaUsage); - qr.setState(account.getState()); - qr.setStartDate(period[0].getTime()); - qr.setEndDate(period[1].getTime()); - qr.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - qr.setQuotaEnabled(QuotaConfig.QuotaAccountEnabled.valueIn(account.getId())); - qr.setObjectName("summary"); - return qr; - } else { - return new QuotaSummaryResponse(); + /** + * Returns a List of QuotaSummaryResponse based on the provided parameters. + * @param accountId ID of the Account to return the summaries for. If -1, either because no specific + * Account was provided, or list all is disabled, then the summary is generated for the calling Account. + * @param domainId ID of the Domain to return the summaries for. + * @param domainPath path of the Domain to return the summaries for. + */ + protected Pair, Integer> getQuotaSummaryResponse(Long accountId, Long domainId, String domainPath, QuotaSummaryCmd cmd) { + if (accountId != null && accountId == -1) { + accountId = CallContext.current().getCallingAccountId(); } + + Pair, Integer> pairSummaries = quotaSummaryDao.listQuotaSummariesForAccountAndOrDomain(accountId, cmd.getKeyword(), domainId, domainPath, + cmd.getAccountStateToShow(), cmd.getStartIndex(), cmd.getPageSizeVal()); + List summaries = pairSummaries.first(); + + if (CollectionUtils.isEmpty(summaries)) { + logger.info("There are no summaries to list for parameters [{}].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "domainId", "listAll", "page", "pageSize")); + return new Pair<>(new ArrayList<>(), 0); + } + + List responses = summaries.stream().map(this::getQuotaSummaryResponse).collect(Collectors.toList()); + + return new Pair<>(responses, pairSummaries.second()); + } + + protected QuotaSummaryResponse getQuotaSummaryResponse(QuotaSummaryVO summary) { + QuotaSummaryResponse response = new QuotaSummaryResponse(); + Account account = _accountDao.findByUuidIncludingRemoved(summary.getAccountUuid()); + + Calendar[] period = quotaStatement.getCurrentStatementTime(); + Date startDate = period[0].getTime(); + Date endDate = period[1].getTime(); + BigDecimal quotaUsage = quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, startDate, endDate); + + response.setQuotaUsage(quotaUsage); + response.setStartDate(startDate); + response.setEndDate(endDate); + response.setAccountId(summary.getAccountUuid()); + response.setAccountName(summary.getAccountName()); + response.setDomainId(summary.getDomainUuid()); + response.setDomainPath(summary.getDomainPath()); + response.setBalance(summary.getQuotaBalance()); + response.setState(summary.getAccountState()); + response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + response.setQuotaEnabled(QuotaConfig.QuotaAccountEnabled.valueIn(account.getId())); + response.setDomainRemoved(summary.getDomainRemoved() != null); + response.setAccountRemoved(summary.getAccountRemoved() != null); + response.setObjectName("summary"); + + if (summary.getProjectUuid() != null) { + response.setProjectId(summary.getProjectUuid()); + response.setProjectName(summary.getProjectName()); + response.setProjectRemoved(summary.getProjectRemoved() != null); + } + + return response; } public boolean isUserAllowedToSeeActivationRules(User user) { - List apiList = (List) apiDiscoveryService.listApis(user, null).getResponses(); + List apiList = (List) apiDiscoveryService.listApis(user, null, null).getResponses(); return apiList.stream().anyMatch(response -> StringUtils.equalsAny(response.getName(), "quotaTariffCreate", "quotaTariffUpdate")); } @Override - public QuotaBalanceResponse createQuotaBalanceResponse(List quotaBalance, Date startDate, Date endDate) { - if (quotaBalance == null || quotaBalance.isEmpty()) { - throw new InvalidParameterValueException("The request period does not contain balance entries."); - } - Collections.sort(quotaBalance, new Comparator() { - @Override - public int compare(QuotaBalanceVO o1, QuotaBalanceVO o2) { - o1 = o1 == null ? new QuotaBalanceVO() : o1; - o2 = o2 == null ? new QuotaBalanceVO() : o2; - return o2.getUpdatedOn().compareTo(o1.getUpdatedOn()); // desc - } - }); - - boolean have_balance_entries = false; - //check that there is at least one balance entry - for (Iterator it = quotaBalance.iterator(); it.hasNext();) { - QuotaBalanceVO entry = it.next(); - if (entry.isBalanceEntry()) { - have_balance_entries = true; - break; - } + public QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd cmd) { + List quotaBalances = _quotaService.listQuotaBalancesForAccount(cmd.getEntityOwnerId(), cmd.getStartDate(), cmd.getEndDate()); + + List balances = quotaBalances.stream() + .map(balance -> new QuotaBalanceResponse(balance.getUpdatedOn(), balance.getCreditBalance())) + .collect(Collectors.toList()); + + QuotaBalanceResponse response = new QuotaBalanceResponse(); + response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + response.setBalances(balances); + + return response; + } + + @Override + public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd) { + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + List quotaUsages = _quotaService.getQuotaUsage(accountId, null, baseDomainAndFilteredDomains.second(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); + + logger.debug("Creating quota statement from [{}] usage records for parameters [{}].", quotaUsages.size(), + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "accountId", "projectId", "domainId", "startDate", "endDate", "usageType", "showResources")); + createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(quotaUsages, cmd.getUsageType()); + + Map> recordsPerUsageTypes = quotaUsages.stream() + .sorted(Comparator.comparingInt(QuotaUsageJoinVO::getUsageType)) + .collect(Collectors.groupingBy(QuotaUsageJoinVO::getUsageType)); + + List items = new ArrayList<>(); + recordsPerUsageTypes.forEach((key, value) -> items.add(createStatementItem(key, value, cmd.isShowResources()))); + + QuotaStatementResponse statement = new QuotaStatementResponse(); + statement.setLineItem(items); + statement.setTotalQuota(items.stream().map(QuotaStatementItemResponse::getQuotaUsed).reduce(BigDecimal.ZERO, BigDecimal::add)); + statement.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + statement.setObjectName("statement"); + + if (accountId != null) { + Account account = _accountDao.findByIdIncludingRemoved(accountId); + statement.setAccountId(account.getUuid()); + statement.setAccountName(account.getAccountName()); } - //if last entry is a credit deposit then remove that as that is already - //accounted for in the starting balance after that entry, note the sort is desc - if (have_balance_entries) { - ListIterator li = quotaBalance.listIterator(quotaBalance.size()); - // Iterate in reverse. - while (li.hasPrevious()) { - QuotaBalanceVO entry = li.previous(); - if (logger.isDebugEnabled()) { - logger.debug("createQuotaBalanceResponse: Entry=" + entry); - } - if (entry.getCreditsId() > 0) { - li.remove(); - } else { - break; - } - } + Long baseDomainId = baseDomainAndFilteredDomains.first(); + if (baseDomainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(baseDomainId); + statement.setDomainId(domain.getUuid()); } - int quota_activity = quotaBalance.size(); - QuotaBalanceResponse resp = new QuotaBalanceResponse(); - BigDecimal lastCredits = new BigDecimal(0); - boolean consecutive = true; - for (Iterator it = quotaBalance.iterator(); it.hasNext();) { - QuotaBalanceVO entry = it.next(); - if (logger.isDebugEnabled()) { - logger.debug("createQuotaBalanceResponse: All Credit Entry=" + entry); - } - if (entry.getCreditsId() > 0) { - if (consecutive) { - lastCredits = lastCredits.add(entry.getCreditBalance()); - } - resp.addCredits(entry); - it.remove(); - } else { - consecutive = false; - } + return statement; + } + + protected void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(List quotaUsages, Integer usageType) { + if (usageType != null) { + logger.debug("As the usage type [{}] was informed as parameter of the API quotaStatement, we will not create dummy records.", usageType); + return; + } - if (quota_activity > 0 && quotaBalance.size() > 0) { - // order is desc last item is the start item - QuotaBalanceVO startItem = quotaBalance.get(quotaBalance.size() - 1); - QuotaBalanceVO endItem = quotaBalance.get(0); - resp.setStartDate(startDate); - resp.setStartQuota(startItem.getCreditBalance()); - resp.setEndDate(endDate); - if (logger.isDebugEnabled()) { - logger.debug("createQuotaBalanceResponse: Start Entry=" + startItem); - logger.debug("createQuotaBalanceResponse: End Entry=" + endItem); - } - resp.setEndQuota(endItem.getCreditBalance().add(lastCredits)); - } else if (quota_activity > 0) { - // order is desc last item is the start item - resp.setStartDate(startDate); - resp.setStartQuota(new BigDecimal(0)); - resp.setEndDate(endDate); - resp.setEndQuota(new BigDecimal(0).add(lastCredits)); - } else { - resp.setStartDate(startDate); - resp.setEndDate(endDate); - resp.setStartQuota(new BigDecimal(0)); - resp.setEndQuota(new BigDecimal(0)); - } - resp.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - resp.setObjectName("balance"); - return resp; + for (Integer quotaType : QuotaTypes.listQuotaTypes().keySet()) { + QuotaUsageJoinVO dummy = new QuotaUsageJoinVO(); + dummy.setUsageType(quotaType); + dummy.setQuotaUsed(BigDecimal.ZERO); + quotaUsages.add(dummy); + } } - @Override - public QuotaStatementResponse createQuotaStatementResponse(final List quotaUsage) { - if (quotaUsage == null || quotaUsage.isEmpty()) { - throw new InvalidParameterValueException("There is no usage data found for period mentioned."); + protected QuotaStatementItemResponse createStatementItem(int usageType, List usageRecords, boolean showResources) { + QuotaUsageJoinVO firstRecord = usageRecords.get(0); + int type = firstRecord.getUsageType(); + + QuotaTypes quotaType = QuotaTypes.listQuotaTypes().get(type); + + QuotaStatementItemResponse item = new QuotaStatementItemResponse(type); + + BigDecimal quotaUsed = usageRecords.stream().map(QuotaUsageJoinVO::getQuotaUsed).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + item.setQuotaUsed(quotaUsed); + item.setUsageUnit(quotaType.getQuotaUnit()); + item.setUsageName(quotaType.getQuotaName()); + + if (showResources) { + setStatementItemResources(item, usageType, usageRecords); + } else { + List history = createQuotaConsumptionHistory(usageRecords, quotaUsed); + item.setHistory(history); } - QuotaStatementResponse statement = new QuotaStatementResponse(); + return item; + } + + protected void setStatementItemResources(QuotaStatementItemResponse statementItem, int usageType, List quotaUsageRecords) { + List itemDetails = new ArrayList<>(); - HashMap quotaTariffMap = new HashMap(); - Collection result = QuotaTypes.listQuotaTypes().values(); + Map> quotaUsagesAggregatedByResourceId = quotaUsageRecords + .stream() + .filter(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) != null) + .collect(Collectors.groupingBy(quotaUsageJoinVo -> getResourceIdByUsageType(quotaUsageJoinVo, usageType) + )); + + for (Map.Entry> entry : quotaUsagesAggregatedByResourceId.entrySet()) { + QuotaUsageResourceVO resource = getResourceFromIdAndType(entry.getKey(), usageType); + + QuotaStatementItemResourceResponse detail = new QuotaStatementItemResourceResponse(); + if (resource != null) { + detail.setResourceId(resource.getUuid()); + detail.setDisplayName(resource.getName()); + detail.setRemoved(resource.isRemoved()); + } else { + detail.setDisplayName(""); + } + BigDecimal quotaUsed = entry.getValue().stream() + .map(QuotaUsageJoinVO::getQuotaUsed) + .reduce(BigDecimal.ZERO, BigDecimal::add); + List history = createQuotaConsumptionHistory(entry.getValue(), quotaUsed); + detail.setQuotaUsed(quotaUsed); + detail.setHistory(history); - for (QuotaTypes quotaTariff : result) { - quotaTariffMap.put(quotaTariff.getQuotaType(), quotaTariff); - // add dummy record for each usage type - QuotaUsageVO dummy = new QuotaUsageVO(quotaUsage.get(0)); - dummy.setUsageType(quotaTariff.getQuotaType()); - dummy.setQuotaUsed(new BigDecimal(0)); - quotaUsage.add(dummy); + itemDetails.add(detail); } - if (logger.isDebugEnabled()) { - logger.debug( - "createQuotaStatementResponse Type=" + quotaUsage.get(0).getUsageType() + " usage=" + quotaUsage.get(0).getQuotaUsed().setScale(2, RoundingMode.HALF_EVEN) - + " rec.id=" + quotaUsage.get(0).getUsageItemId() + " SD=" + quotaUsage.get(0).getStartDate() + " ED=" + quotaUsage.get(0).getEndDate()); + statementItem.setResources(itemDetails); + } + + protected Long getResourceIdByUsageType(QuotaUsageJoinVO quotaUsageJoinVo, int usageType) { + switch (usageType) { + case QuotaTypes.NETWORK_BYTES_SENT: + case QuotaTypes.NETWORK_BYTES_RECEIVED: + return quotaUsageJoinVo.getNetworkId(); + case QuotaTypes.NETWORK_OFFERING: + return quotaUsageJoinVo.getOfferingId(); + default: + return quotaUsageJoinVo.getResourceId(); } + } - Collections.sort(quotaUsage, new Comparator() { - @Override - public int compare(QuotaUsageVO o1, QuotaUsageVO o2) { - if (o1.getUsageType() == o2.getUsageType()) { - return 0; + protected QuotaUsageResourceVO getResourceFromIdAndType(long resourceId, int usageType) { + switch (usageType) { + case QuotaTypes.ALLOCATED_VM: + case QuotaTypes.RUNNING_VM: + case QuotaTypes.BACKUP: + VMInstanceVO vmInstance = vmInstanceDao.findByIdIncludingRemoved(resourceId); + if (vmInstance != null) { + return new QuotaUsageResourceVO(vmInstance.getUuid(), vmInstance.getHostName(), vmInstance.getRemoved()); } - return o1.getUsageType() < o2.getUsageType() ? -1 : 1; - } - }); - - List items = new ArrayList(); - QuotaStatementItemResponse lineitem; - int type = -1; - BigDecimal usage = new BigDecimal(0); - BigDecimal totalUsage = new BigDecimal(0); - quotaUsage.add(new QuotaUsageVO());// boundary - QuotaUsageVO prev = quotaUsage.get(0); - if (logger.isDebugEnabled()) { - logger.debug("createQuotaStatementResponse record count=" + quotaUsage.size()); - } - for (final QuotaUsageVO quotaRecord : quotaUsage) { - if (type != quotaRecord.getUsageType()) { - if (type != -1) { - lineitem = new QuotaStatementItemResponse(type); - lineitem.setQuotaUsed(usage); - lineitem.setAccountId(prev.getAccountId()); - lineitem.setDomainId(prev.getDomainId()); - lineitem.setUsageUnit(quotaTariffMap.get(type).getQuotaUnit()); - lineitem.setUsageName(quotaTariffMap.get(type).getQuotaName()); - lineitem.setObjectName("quotausage"); - items.add(lineitem); - totalUsage = totalUsage.add(usage); - usage = new BigDecimal(0); + break; + case QuotaTypes.VOLUME: + case QuotaTypes.VOLUME_SECONDARY: + case QuotaTypes.VM_DISK_BYTES_READ: + case QuotaTypes.VM_DISK_BYTES_WRITE: + case QuotaTypes.VM_DISK_IO_READ: + case QuotaTypes.VM_DISK_IO_WRITE: + VolumeVO volume = volumeDao.findByIdIncludingRemoved(resourceId); + if (volume != null) { + return new QuotaUsageResourceVO(volume.getUuid(), volume.getName(), volume.getRemoved()); + } + break; + case QuotaTypes.VM_SNAPSHOT_ON_PRIMARY: + case QuotaTypes.VM_SNAPSHOT: + case QuotaTypes.SNAPSHOT: + SnapshotVO snapshot = snapshotDao.findByIdIncludingRemoved(resourceId); + if (snapshot != null) { + return new QuotaUsageResourceVO(snapshot.getUuid(), snapshot.getName(), snapshot.getRemoved()); + } + break; + case QuotaTypes.NETWORK_BYTES_SENT: + case QuotaTypes.NETWORK_BYTES_RECEIVED: + case QuotaTypes.NETWORK: + NetworkVO network = networkDao.findByIdIncludingRemoved(resourceId); + if (network != null) { + return new QuotaUsageResourceVO(network.getUuid(), network.getName(), network.getRemoved()); + } + break; + case QuotaTypes.VPC: + VpcVO vpc = vpcDao.findByIdIncludingRemoved(resourceId); + if (vpc != null) { + return new QuotaUsageResourceVO(vpc.getUuid(), vpc.getName(), vpc.getRemoved()); + } + break; + case QuotaTypes.TEMPLATE: + case QuotaTypes.ISO: + VMTemplateVO vmTemplate = vmTemplateDao.findByIdIncludingRemoved(resourceId); + if (vmTemplate != null) { + return new QuotaUsageResourceVO(vmTemplate.getUuid(), vmTemplate.getName(), vmTemplate.getRemoved()); + } + break; + case QuotaTypes.NETWORK_OFFERING: + NetworkOfferingVO networkOffering = networkOfferingDao.findByIdIncludingRemoved(resourceId); + if (networkOffering != null) { + return new QuotaUsageResourceVO(networkOffering.getUuid(), networkOffering.getName(), networkOffering.getRemoved()); + } + break; + case QuotaTypes.IP_ADDRESS: + IPAddressVO ipAddress = ipAddressDao.findByIdIncludingRemoved(resourceId); + if (ipAddress != null) { + return new QuotaUsageResourceVO(ipAddress.getUuid(), ipAddress.getName(), ipAddress.getRemoved()); + } + break; + case QuotaTypes.BUCKET: + BucketVO bucket = bucketDao.findByIdIncludingRemoved(resourceId); + if (bucket != null) { + return new QuotaUsageResourceVO(bucket.getUuid(), bucket.getName(), bucket.getRemoved()); + } + break; + case QuotaTypes.VPN_USERS: + VpnUserVO vpnUser = vpnUserDao.findByIdIncludingRemoved(resourceId); + if (vpnUser != null) { + return new QuotaUsageResourceVO(vpnUser.getUuid(), vpnUser.getUsername(), null); + } + break; + case QuotaTypes.SECURITY_GROUP: + SecurityGroupVO securityGroup = securityGroupDao.findByIdIncludingRemoved(resourceId); + if (securityGroup != null) { + return new QuotaUsageResourceVO(securityGroup.getUuid(), securityGroup.getName(), null); + } + break; + case QuotaTypes.LOAD_BALANCER_POLICY: + LoadBalancerVO loadBalancer = loadBalancerDao.findByIdIncludingRemoved(resourceId); + if (loadBalancer != null) { + return new QuotaUsageResourceVO(loadBalancer.getUuid(), loadBalancer.getName(), loadBalancer.getRemoved()); + } + break; + case QuotaTypes.PORT_FORWARDING_RULE: + PortForwardingRuleVO portForwardingRule = portForwardingRulesDao.findByIdIncludingRemoved(resourceId); + if (portForwardingRule == null) { + return null; + } + IPAddressVO source = ipAddressDao.findByIdIncludingRemoved(portForwardingRule.getSourceIpAddressId()); + Ip destination = portForwardingRule.getDestinationIpAddress(); + if (ObjectUtils.anyNull(source, destination)) { + return null; } - type = quotaRecord.getUsageType(); + String displayName = String.format("%s:%s-%s to %s:%s-%s", source.getAddress(), portForwardingRule.getSourcePortStart(), + portForwardingRule.getSourcePortEnd(), destination, portForwardingRule.getDestinationPortStart(), + portForwardingRule.getDestinationPortEnd()); + return new QuotaUsageResourceVO(portForwardingRule.getUuid(), displayName, portForwardingRule.getRemoved()); + } + return null; + } + + protected List createQuotaConsumptionHistory(List quotaUsage, BigDecimal quotaUsed) { + if (quotaUsed.equals(BigDecimal.ZERO)) { + logger.debug("Not generating Quota consumption history because the item has not consumed any Quota in the period."); + return null; + } + + Map history = new HashMap<>(); + for (QuotaUsageJoinVO record : quotaUsage) { + if (ObjectUtils.anyNull(record.getUsageItemId(), record.getQuotaUsed())) { + continue; + } + + QuotaStatementItemHistoryResponse item = history.computeIfAbsent( + record.getEndDate(), + key -> new QuotaStatementItemHistoryResponse() + ); + if (item.getStartDate() == null || item.getStartDate().after(record.getStartDate())) { + item.setStartDate(record.getStartDate()); } - prev = quotaRecord; - usage = usage.add(quotaRecord.getQuotaUsed()); + item.setEndDate(record.getEndDate()); + item.setQuotaConsumed(item.getQuotaConsumed().add(record.getQuotaUsed())); + + history.put(record.getEndDate(), item); } - statement.setLineItem(items); - statement.setTotalQuota(totalUsage); - statement.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - statement.setObjectName("statement"); - return statement; + return history.values().stream().sorted(Comparator.comparing(QuotaStatementItemHistoryResponse::getEndDate)).collect(Collectors.toList()); } @Override @@ -450,6 +662,7 @@ public QuotaTariffVO updateQuotaTariffPlan(QuotaTariffUpdateCmd cmd) { Integer position = cmd.getPosition(); warnQuotaTariffUpdateDeprecatedFields(cmd); + jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.ACTIVATION_RULE, StringUtils.isNotBlank(activationRule)); QuotaTariffVO currentQuotaTariff = _quotaTariffDao.findByName(name); @@ -457,8 +670,6 @@ public QuotaTariffVO updateQuotaTariffPlan(QuotaTariffUpdateCmd cmd) { throw new InvalidParameterValueException(String.format("There is no quota tariffs with name [%s].", name)); } - checkActivationRulesAllowed(activationRule); - Date currentQuotaTariffStartDate = currentQuotaTariff.getEffectiveOn(); currentQuotaTariff.setRemoved(now); @@ -473,14 +684,14 @@ public QuotaTariffVO updateQuotaTariffPlan(QuotaTariffUpdateCmd cmd) { } protected void warnQuotaTariffUpdateDeprecatedFields(QuotaTariffUpdateCmd cmd) { - String warnMessage = "The parameter 's%s' for API 'quotaTariffUpdate' is no longer needed and it will be removed in future releases."; + String warnMessage = "The parameter '{}' for API 'quotaTariffUpdate' is no longer needed and it will be removed in future releases."; if (cmd.getStartDate() != null) { - logger.warn(String.format(warnMessage,"startdate")); + logger.warn(warnMessage, "startdate"); } if (cmd.getUsageType() != null) { - logger.warn(String.format(warnMessage,"usagetype")); + logger.warn(warnMessage, "usagetype"); } } @@ -557,49 +768,88 @@ protected void validateEndDateOnCreatingNewQuotaTariff(QuotaTariffVO newQuotaTar } @Override - public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) { - Date depositedOn = new Date(); - QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn); + public QuotaCreditsResponse addQuotaCredits(QuotaCreditsCmd cmd) { + Double value = cmd.getValue(); + if (value == null) { + throw new InvalidParameterValueException("Please specify a valid amount of credits."); + } - if (qb != null) { - throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.", - depositedOn)); + Long accountId = _accountMgr.finalizeAccountId(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + AccountVO account = _accountDao.findById(accountId); + Long domainId = account.getDomainId(); + + Date depositedOn = new Date(); + boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value()); + + QuotaCreditsVO result; + try (TransactionLegacy ignored = TransactionLegacy.open(TransactionLegacy.USAGE_DB)) { + QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn); + if (qb != null) { + throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.", + depositedOn)); + } + result = persistQuotaCredits(cmd, value, depositedOn, account, lockAccountEnforcement); + } finally { + // Swap back to cloud + TransactionLegacy.open(TransactionLegacy.CLOUD_DB).close(); } - QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy); + UserVO creditor = getCreditorForQuotaCredits(result); + return createQuotaCreditsResponse(result, creditor); + } + + protected QuotaCreditsVO persistQuotaCredits(QuotaCreditsCmd cmd, Double value, Date depositedOn, AccountVO account, boolean lockAccountEnforcement) { + Long accountId = account.getId(); + Long domainId = account.getDomainId(); + long callingUserId = CallContext.current().getCallingUserId(); + QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(value), callingUserId); credits.setUpdatedOn(depositedOn); QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits); - if (result == null) { - logger.error("Unable to add credits to account ID [{}].", accountId); - throw new CloudRuntimeException("Unable to add credits to account."); - } - final AccountVO account = _accountDao.findById(accountId); - if (account == null) { - throw new InvalidParameterValueException("Account does not exist with account id " + accountId); - } - final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value()); - final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(depositedOn.getTime()))); - logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount, + BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId); + logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", value, DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance); - // update quota account with the balance _quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn); + + Boolean enforceQuota = cmd.getQuotaEnforce(); + if (enforceQuota != null) { + _quotaService.setLockAccount(accountId, enforceQuota); + } + + Double minBalance = cmd.getMinBalance(); + if (minBalance != null) { + _quotaService.setMinBalance(accountId, minBalance); + } + if (lockAccountEnforcement) { - if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) { - if (account.getState() == Account.State.LOCKED) { - logger.info("UnLocking account " + account.getAccountName() + " , due to positive balance " + currentAccountBalance); - _accountMgr.enableAccount(account.getAccountName(), domainId, accountId); - } - } else { // currentAccountBalance < 0 then lock the account - if (_quotaManager.isLockable(account) && account.getState() == Account.State.ENABLED && enforce) { - logger.info("Locking account " + account.getAccountName() + " , due to negative balance " + currentAccountBalance); - _accountMgr.lockAccount(account.getAccountName(), domainId, accountId); - } + // Need to open a transaction for the cloud data base, and then swap back to cloud_usage + try (TransactionLegacy ignored = TransactionLegacy.open(TransactionLegacy.CLOUD_DB)) { + lockOrUnlockAccountIfRequired(currentAccountBalance, account, enforceQuota); + } finally { + TransactionLegacy.open(TransactionLegacy.USAGE_DB).close(); } } - UserVO creditor = getCreditorForQuotaCredits(result); - return createQuotaCreditsResponse(result, creditor); + return result; + } + + protected void lockOrUnlockAccountIfRequired(BigDecimal currentAccountBalance, AccountVO account, Boolean enforceQuota) { + Long accountId = account.getId(); + Long domainId = account.getDomainId(); + String accountName = account.getAccountName(); + + if (currentAccountBalance.compareTo(BigDecimal.ZERO) >= 0) { + if (account.getState() == Account.State.LOCKED) { + logger.info("Unlocking Account [{}] due to positive balance.", accountName); + _accountMgr.enableAccount(accountName, domainId, accountId); + } + return; + } + + if (Boolean.TRUE.equals(enforceQuota) && account.getState() == Account.State.ENABLED && _quotaManager.isLockable(account)) { + logger.info("Locking Account [{}] due to negative balance.", accountName); + _accountMgr.lockAccount(accountName, domainId, accountId); + } } private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) { @@ -643,39 +893,6 @@ public boolean updateQuotaEmailTemplate(QuotaEmailTemplateUpdateCmd cmd) { return false; } - @Override - public QuotaBalanceResponse createQuotaLastBalanceResponse(List quotaBalance, Date startDate) { - if (quotaBalance == null) { - throw new InvalidParameterValueException("There are no balance entries on or before the requested date."); - } - if (startDate == null) { - startDate = new Date(); - } - QuotaBalanceResponse resp = new QuotaBalanceResponse(); - BigDecimal lastCredits = new BigDecimal(0); - for (QuotaBalanceVO entry : quotaBalance) { - logger.debug("createQuotaLastBalanceResponse Date={} balance={} credit={}", - DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), entry.getUpdatedOn()), - entry.getCreditBalance(), entry.getCreditsId()); - - lastCredits = lastCredits.add(entry.getCreditBalance()); - } - resp.setStartQuota(lastCredits); - resp.setStartDate(startDate); - resp.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - resp.setObjectName("balance"); - return resp; - } - - @Override - public List getQuotaUsage(QuotaStatementCmd cmd) { - return _quotaService.getQuotaUsage(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); - } - - @Override - public List getQuotaBalance(QuotaBalanceCmd cmd) { - return _quotaService.findQuotaBalanceVO(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getStartDate(), cmd.getEndDate()); - } @Override public Date startOfNextDay(Date date) { LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); @@ -707,14 +924,14 @@ public QuotaTariffVO createQuotaTariff(QuotaTariffCreateCmd cmd) { String activationRule = cmd.getActivationRule(); Integer position = ObjectUtils.defaultIfNull(cmd.getPosition(), 1); + jsInterpreterHelper.ensureInterpreterEnabledIfParameterProvided(ApiConstants.ACTIVATION_RULE, StringUtils.isNotBlank(activationRule)); + QuotaTariffVO currentQuotaTariff = _quotaTariffDao.findByName(name); if (currentQuotaTariff != null) { throw new InvalidParameterValueException(String.format("A quota tariff with name [%s] already exist.", name)); } - checkActivationRulesAllowed(activationRule); - if (startDate.compareTo(now) < 0) { throw new InvalidParameterValueException(String.format("The value passed as Quota tariff's start date is in the past: [%s]. " + "Please, inform a date in the future or do not pass the parameter to use the current date and time.", startDate)); @@ -965,26 +1182,16 @@ public Pair, Integer> createQuotaCreditsListResponse( } protected List getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) { - Long accountId = cmd.getAccountId(); - Long domainId = cmd.getDomainId(); + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); Date startDate = cmd.getStartDate(); Date endDate = cmd.getEndDate(); - boolean isRecursive = cmd.getRecursive(); - - if (ObjectUtils.allNull(accountId, domainId)) { - throw new InvalidParameterValueException("Please provide either account ID or domain ID."); - } if (startDate.after(endDate)) { throw new InvalidParameterValueException("The start date must be before the end date."); } - Account caller = CallContext.current().getCallingAccount(); - if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) { - throw new PermissionDeniedException("Regular users are not allowed to generate domain statements."); - } - - return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive); + return quotaCreditsDao.findCredits(accountId, baseDomainAndFilteredDomains.second(), startDate, endDate); } /** @@ -1044,7 +1251,7 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null); List usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList()); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { Map newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables); String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables); jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", ""))); @@ -1064,6 +1271,203 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA return createValidateActivationRuleResponse(activationRule, quotaName, false, message); } + @Override + public QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd) { + String resourceUuid = cmd.getId(); + Integer usageType = cmd.getUsageType(); + Date startDate = cmd.getStartDate(); + Date endDate = cmd.getEndDate(); + + if (startDate.after(endDate)) { + throw new InvalidParameterValueException(String.format("The start date [%s] must be before the end date [%s].", startDate, endDate)); + } + + InternalIdentity resource = retrieveResource(resourceUuid, usageType); + if (resource == null) { + logger.error("Could not find resource [{}] of type [{}]. Returning an empty list.", resourceUuid, usageType); + return createQuotaResourceStatementResponse(resourceUuid, usageType, new ArrayList<>(), new BigDecimal(0)); + } + + long id = resource.getId(); + + Long currentOwnerId = null; + if (resource instanceof ControlledEntity) { + currentOwnerId = ((ControlledEntity) resource).getAccountId(); + } + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), currentOwnerId); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + + Long resourceId = null; + Long networkId = null; + Long offeringId = null; + + switch (usageType) { + case UsageTypes.NETWORK_OFFERING: + case UsageTypes.BACKUP: + offeringId = id; + break; + case UsageTypes.NETWORK_BYTES_RECEIVED: + case UsageTypes.NETWORK_BYTES_SENT: + networkId = id; + break; + default: + resourceId = id; + } + + logger.debug("Attempting to find quota usages with parameters usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaUsageJoinList = quotaUsageJoinDao.findQuotaUsage(accountId, baseDomainAndFilteredDomains.second(), usageType, resourceId, networkId, offeringId, startDate, endDate, null); + + logger.debug("Found [{}] quota usages using as parameter usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + quotaUsageJoinList.size(), usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaResourceStatementItemResponseList = new ArrayList<>(); + BigDecimal totalQuotaUsed = new BigDecimal(0); + List quotaTariffs = _quotaTariffDao.listQuotaTariffs(null, null, usageType, null, null, true, null, null).first(); + + for (QuotaUsageJoinVO quotaUsageJoin : quotaUsageJoinList) { + Account account = _accountMgr.getAccount(quotaUsageJoin.getAccountId()); + String accountUuid = account.getUuid(); + + List quotaTariffUsageList = quotaTariffUsageDao.listQuotaTariffUsages(quotaUsageJoin.getId()); + logger.debug("Found [{}] quota tariff usages associated to the quota usage [{}] of resource [{}] and type [{}] between [{}] and [{}].", + quotaTariffUsageList.size(), quotaUsageJoin, resourceUuid, usageType, startDate, endDate); + for (QuotaTariffUsageVO quotaTariffUsage: quotaTariffUsageList) { + quotaResourceStatementItemResponseList.add(createQuotaResourceStatementItemResponse(quotaTariffUsage, quotaTariffs, quotaUsageJoin.getStartDate(), + quotaUsageJoin.getEndDate(), accountUuid)); + totalQuotaUsed = totalQuotaUsed.add(quotaTariffUsage.getQuotaUsed()); + } + } + logger.debug("The total quota used of type [{}] between [{}] and [{}] for the resource [{}] was [{}].", usageType, startDate, endDate, resourceUuid, totalQuotaUsed); + + return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed); + } + + /** + * Determines the appropriate Account ID to use for Quota statement-related operations while ensuring correct permissions. + * + * @param providedAccountId the ID of the Account provided to the command. + * @param fallbackAccountId the ID of a fallback Account to use for User Accounts if no specific Account ID was provided. + * If null, then we fallback to the User Account itself. + * @return the account ID to be used for the Quota statement, or null if no specific Account limitation is required. + */ + protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) { + Account caller = CallContext.current().getCallingAccount(); + + if (providedAccountId != -1L) { + Account account = _accountDao.findByIdIncludingRemoved(providedAccountId); + _accountMgr.checkAccess(caller, null, false, account); + logger.debug("Limiting the Quota resource statement for the provided Account [{}].", providedAccountId); + return providedAccountId; + } + + Account.Type callerType = caller.getType(); + if (Account.Type.ADMIN.equals(callerType) || Account.Type.DOMAIN_ADMIN.equals(callerType)) { + logger.debug("Not limiting the Quota resource statement for a specific Account, as no specific Account was provided and the caller is either an admin or domain admin."); + return null; + } + + if (fallbackAccountId != null) { + Account fallbackAccount = _accountDao.findByIdIncludingRemoved(fallbackAccountId); + _accountMgr.checkAccess(caller, null, false, fallbackAccount); + logger.debug("Limiting the Quota statement for the fallback Account [{}], as no specific Account was provided.", fallbackAccountId); + return fallbackAccountId; + } + + logger.debug("Limiting the Quota resource statement for the calling account, as no specific Account was provided and no fallback account was provided."); + return caller.getAccountId(); + } + + /** + * Determines the Domains for which a Quota statement should be generated while ensuring correct permissions. + * + * @param finalAccountId the Account ID determined via org.apache.cloudstack.api.response.QuotaResponseBuilderImpl#getAccountIdForQuotaStatement(long, java.lang.Long). + * @param providedDomainId the Domain ID provided to the command. + * @param isRecursive the recursion flag provided to the command. + * @return A pair containing: + * - The base Domain's ID as the first element. This can be null if we are not limiting by Domain. + * - A list containing the base Domain's ID and optionally its children if + * the recursion flag is true. Also nullable. + */ + protected Pair> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) { + if (finalAccountId != null) { + // Access to the provided account has already been validated + logger.debug("Not limiting the Quota statement for a specific Domain, as we are already limiting by Account."); + return new Pair<>(null, null); + } + + // User accounts will have already been limited to themselves + Account caller = CallContext.current().getCallingAccount(); + Long domainId = providedDomainId; + + if (domainId != null) { + Domain domain = domainDao.findByIdIncludingRemoved(domainId); + _accountMgr.checkAccess(caller, domain); + logger.debug("Limiting the Quota statement for the provided Domain [{}].", domainId); + } else { + domainId = caller.getDomainId(); + logger.debug("Limiting the Quota statement for the caller's Domain [{}], as no 'domainid' was provided.", domainId); + } + + if (isRecursive) { + logger.debug("Allowing the Quota statement for the Domain's children, as the 'isrecursive' parameter was provided."); + return new Pair<>(domainId, domainDao.getDomainAndChildrenIds(domainId)); + } + + return new Pair<>(domainId, List.of(domainId)); + } + + protected InternalIdentity retrieveResource(String resourceUuid, Integer usageType) { + Class clazz = QuotaTypes.getClazz(usageType); + if (clazz == null) { + throw new InvalidParameterValueException(String.format("Invalid usage type [%s] provided.", usageType)); + } + + logger.debug("Attempting to find a resource with ID [{}] and of type [{}].", resourceUuid, usageType); + Object object = entityMgr.findByUuidIncludingRemoved(clazz, resourceUuid); + if (object == null) { + return null; + } + return (InternalIdentity) object; + } + + protected QuotaResourceStatementItemResponse createQuotaResourceStatementItemResponse(QuotaTariffUsageVO quotaTariffUsage, List quotaTariffs, + Date startDate, Date endDate, String accountUuid) { + logger.trace("Creating quota resource statement item associated to quota tariff usage [{}].", quotaTariffUsage); + QuotaResourceStatementItemResponse quotaResourceStatementItemResponse = new QuotaResourceStatementItemResponse(); + + QuotaTariffVO quotaTariff = quotaTariffs.stream().filter(quotaTariffVO -> quotaTariffUsage.getTariffId().equals(quotaTariffVO.getId())).findAny().orElse(null); + + quotaResourceStatementItemResponse.setQuotaUsed(quotaTariffUsage.getQuotaUsed()); + quotaResourceStatementItemResponse.setStartDate(startDate); + quotaResourceStatementItemResponse.setEndDate(endDate); + quotaResourceStatementItemResponse.setAccountId(accountUuid); + if (quotaTariff != null) { + logger.trace("Quota usage details item will be associated to the quota tariff [{}].", quotaTariff); + quotaResourceStatementItemResponse.setTariffId(quotaTariff.getUuid()); + quotaResourceStatementItemResponse.setTariffName(quotaTariff.getName()); + } + + return quotaResourceStatementItemResponse; + } + + protected QuotaResourceStatementResponse createQuotaResourceStatementResponse(String resourceUuid, Integer usageType, + List quotaUsageDetailsItems, BigDecimal totalQuotaUsed) { + logger.trace("Creating quota usage details list response associated to the resource of UUID [{}], with an usage type of [{}], [{}] quota" + + " usage details items, and a total quota used of [{}].", resourceUuid, usageType, quotaUsageDetailsItems.size(), totalQuotaUsed); + QuotaResourceStatementResponse quotaResourceStatementResponse = new QuotaResourceStatementResponse(); + + QuotaTypes quotaType = QuotaTypes.getQuotaType(usageType); + quotaResourceStatementResponse.setUsageName(quotaType.getQuotaName()); + quotaResourceStatementResponse.setUnit(quotaType.getQuotaUnit()); + + quotaResourceStatementResponse.setQuotaUsageDetails(quotaUsageDetailsItems); + quotaResourceStatementResponse.setTotalQuotaUsed(totalQuotaUsed); + + return quotaResourceStatementResponse; + } + /** * Checks whether script variables are compatible with the usage type. First, we remove all script variables that correspond to the script's usage type variables. * Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise. diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java new file mode 100644 index 000000000000..7799326ea54b --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemHistoryResponse.java @@ -0,0 +1,68 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.Date; + +public class QuotaStatementItemHistoryResponse extends BaseResponse { + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Start date of the item.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "End date of the item.") + private Date endDate; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota consumed.") + private BigDecimal quotaConsumed = BigDecimal.ZERO; + + public QuotaStatementItemHistoryResponse() { + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public BigDecimal getQuotaConsumed() { + return quotaConsumed; + } + + public void setQuotaConsumed(BigDecimal quotaConsumed) { + this.quotaConsumed = quotaConsumed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java new file mode 100644 index 000000000000..68585a90d393 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResourceResponse.java @@ -0,0 +1,71 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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. +package org.apache.cloudstack.api.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaStatementItemResourceResponse extends BaseResponse { + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Quota consumed.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.RESOURCE_ID) + @Param(description = "Resources's ID.") + private String resourceId; + + @SerializedName(ApiConstants.DISPLAY_NAME) + @Param(description = "Resource's display name.") + private String displayName; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "Indicates whether the resource is removed or active.") + private boolean removed; + + @SerializedName(ApiConstants.HISTORY) + @Param(description = "Quota consumption history.") + private List history; + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setRemoved(boolean removed) { + this.removed = removed; + } + + public void setHistory(List history) { + this.history = history; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java index c370d82b3cc4..f04fffdf7790 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementItemResponse.java @@ -17,70 +17,43 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; -import java.math.RoundingMode; +import java.util.List; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; public class QuotaStatementItemResponse extends BaseResponse { - @SerializedName("type") - @Param(description = "Usage type") + @SerializedName(ApiConstants.TYPE) + @Param(description = "Usage type.") private int usageType; - @SerializedName("accountid") - @Param(description = "Account id") - private Long accountId; - - @SerializedName("account") - @Param(description = "Account name") - private String accountName; - - @SerializedName("domain") - @Param(description = "Domain id") - private Long domainId; - - @SerializedName("name") - @Param(description = "Usage type name") + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the Usage type.") private String usageName; - @SerializedName("unit") - @Param(description = "Usage unit") + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the Usage type.") private String usageUnit; - @SerializedName("quota") - @Param(description = "Quota consumed") + @SerializedName(ApiConstants.QUOTA) + @Param(description = "Quota consumed.") private BigDecimal quotaUsed; - public QuotaStatementItemResponse(final int usageType) { - this.usageType = usageType; - } - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public String getAccountName() { - return accountName; - } - - public void setAccountName(String accountName) { - this.accountName = accountName; - } + @SerializedName(ApiConstants.RESOURCES) + @Param(description = "Item's resources.") + private List resources; - public Long getDomainId() { - return domainId; - } + @SerializedName(ApiConstants.HISTORY) + @Param(description = "Quota consumption history.") + private List history; - public void setDomainId(Long domainId) { - this.domainId = domainId; + public QuotaStatementItemResponse(final int usageType) { + this.usageType = usageType; } public String getUsageName() { @@ -112,7 +85,18 @@ public BigDecimal getQuotaUsed() { } public void setQuotaUsed(BigDecimal quotaUsed) { - this.quotaUsed = quotaUsed.setScale(2, RoundingMode.HALF_EVEN); + this.quotaUsed = quotaUsed; } + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public void setHistory(List history) { + this.history = history; + } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java index 0a7ba4dbeb94..c30780582d36 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java @@ -18,56 +18,56 @@ import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Date; import java.util.List; public class QuotaStatementResponse extends BaseResponse { - @SerializedName("accountid") - @Param(description = "Account ID") - private Long accountId; + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "ID of the Account.") + private String accountId; - @SerializedName("account") - @Param(description = "Account name") + @SerializedName(ApiConstants.ACCOUNT_NAME) + @Param(description = "Name of the Account.") private String accountName; - @SerializedName("domain") - @Param(description = "Domain ID") - private Long domainId; + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "ID of the Domain.") + private String domainId; - @SerializedName("quotausage") - @Param(description = "List of quota usage under various types", responseObject = QuotaStatementItemResponse.class) + @SerializedName(ApiConstants.QUOTA_USAGE) + @Param(description = "List of Quota usage under various types.", responseObject = QuotaStatementItemResponse.class) private List lineItem; - @SerializedName("totalquota") - @Param(description = "Total quota used during this period") + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total Quota consumed during this period.") private BigDecimal totalQuota; - @SerializedName("startdate") - @Param(description = "Start date") + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Start date of the Quota statement.") private Date startDate = null; - @SerializedName("enddate") - @Param(description = "End date") + @SerializedName(ApiConstants.END_DATE) + @Param(description = "End date of the Quota statement.") private Date endDate = null; - @SerializedName("currency") - @Param(description = "Currency") + @SerializedName(ApiConstants.CURRENCY) + @Param(description = "Currency of the Quota statement.") private String currency; public QuotaStatementResponse() { super(); } - public Long getAccountId() { + public String getAccountId() { return accountId; } - public void setAccountId(Long accountId) { + public void setAccountId(String accountId) { this.accountId = accountId; } @@ -79,45 +79,36 @@ public void setAccountName(String accountName) { this.accountName = accountName; } - public Long getDomainId() { + public String getDomainId() { return domainId; } - public void setDomainId(Long domainId) { + public void setDomainId(String domainId) { this.domainId = domainId; } - public List getLineItem() { - return lineItem; - } - public void setLineItem(List lineItem) { this.lineItem = lineItem; } public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + return startDate; } public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + this.startDate = startDate; } public Date getEndDate() { - return endDate == null ? null : new Date(endDate.getTime()); + return endDate; } public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); - } - - - public BigDecimal getTotalQuota() { - return totalQuota; + this.endDate = endDate; } public void setTotalQuota(BigDecimal totalQuota) { - this.totalQuota = totalQuota.setScale(2, RoundingMode.HALF_EVEN); + this.totalQuota = totalQuota; } public String getCurrency() { diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java index f8f27b4813d8..d76202bfd889 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Date; import com.google.gson.annotations.SerializedName; @@ -30,40 +29,48 @@ public class QuotaSummaryResponse extends BaseResponse { @SerializedName("accountid") - @Param(description = "Account ID") + @Param(description = "Account's ID") private String accountId; @SerializedName("account") - @Param(description = "Account name") + @Param(description = "Account's name") private String accountName; @SerializedName("domainid") - @Param(description = "Domain ID") + @Param(description = "Domain's ID") private String domainId; @SerializedName("domain") - @Param(description = "Domain name") - private String domainName; + @Param(description = "Domain's path") + private String domainPath; @SerializedName("balance") - @Param(description = "Account balance") + @Param(description = "Account's balance") private BigDecimal balance; @SerializedName("state") - @Param(description = "Account state") + @Param(description = "Account's state") private State state; + @SerializedName("domainremoved") + @Param(description = "If the domain is removed or not", since = "4.23.0") + private boolean domainRemoved; + + @SerializedName("accountremoved") + @Param(description = "If the account is removed or not", since = "4.23.0") + private boolean accountRemoved; + @SerializedName("quota") - @Param(description = "Quota usage of this period") + @Param(description = "Quota consumed between the startdate and enddate") private BigDecimal quotaUsage; @SerializedName("startdate") - @Param(description = "Start date") - private Date startDate = null; + @Param(description = "Start date of the quota consumption") + private Date startDate; @SerializedName("enddate") - @Param(description = "End date") - private Date endDate = null; + @Param(description = "End date of the quota consumption") + private Date endDate; @SerializedName("currency") @Param(description = "Currency") @@ -73,9 +80,17 @@ public class QuotaSummaryResponse extends BaseResponse { @Param(description = "If the account has the quota config enabled") private boolean quotaEnabled; - public QuotaSummaryResponse() { - super(); - } + @SerializedName("projectname") + @Param(description = "Name of the project", since = "4.23.0") + private String projectName; + + @SerializedName("projectid") + @Param(description = "Project's id", since = "4.23.0") + private String projectId; + + @SerializedName("projectremoved") + @Param(description = "Whether the project is removed or not", since = "4.23.0") + private Boolean projectRemoved; public String getAccountId() { return accountId; @@ -101,28 +116,16 @@ public void setDomainId(String domainId) { this.domainId = domainId; } - public String getDomainName() { - return domainName; - } - - public void setDomainName(String domainName) { - this.domainName = domainName; - } - - public BigDecimal getQuotaUsage() { - return quotaUsage; - } - - public State getState() { - return state; + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; } public void setState(State state) { this.state = state; } - public void setQuotaUsage(BigDecimal startQuota) { - this.quotaUsage = startQuota.setScale(2, RoundingMode.HALF_EVEN); + public void setQuotaUsage(BigDecimal quotaUsage) { + this.quotaUsage = quotaUsage; } public BigDecimal getBalance() { @@ -130,38 +133,42 @@ public BigDecimal getBalance() { } public void setBalance(BigDecimal balance) { - this.balance = balance.setScale(2, RoundingMode.HALF_EVEN); + this.balance = balance; } - public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + public void setStartDate(Date startDate) { + this.startDate = startDate; } - public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + public void setEndDate(Date endDate) { + this.endDate = endDate; } - public Date getEndDate() { - return endDate == null ? null : new Date(endDate.getTime()); + public void setCurrency(String currency) { + this.currency = currency; } - public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + public void setQuotaEnabled(boolean quotaEnabled) { + this.quotaEnabled = quotaEnabled; } - public String getCurrency() { - return currency; + public void setProjectName(String projectName) { + this.projectName = projectName; } - public void setCurrency(String currency) { - this.currency = currency; + public void setProjectId(String projectId) { + this.projectId = projectId; } - public boolean getQuotaEnabled() { - return quotaEnabled; + public void setProjectRemoved(Boolean projectRemoved) { + this.projectRemoved = projectRemoved; } - public void setQuotaEnabled(boolean quotaEnabled) { - this.quotaEnabled = quotaEnabled; + public void setDomainRemoved(boolean domainRemoved) { + this.domainRemoved = domainRemoved; + } + + public void setAccountRemoved(boolean accountRemoved) { + this.accountRemoved = accountRemoved; } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index f6a34e01be8d..cc257c233def 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -21,16 +21,16 @@ import java.util.List; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import com.cloud.user.AccountVO; import com.cloud.utils.component.PluggableService; public interface QuotaService extends PluggableService { - List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); + List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate); - List findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate); + List listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate); void setLockAccount(Long accountId, Boolean state); @@ -40,6 +40,4 @@ public interface QuotaService extends PluggableService { boolean saveQuotaAccount(AccountVO account, BigDecimal aggrUsage, Date endDate); - boolean isJsInterpretationEnabled(); - } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index f455c3cba147..a788f0c8b01e 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -26,6 +26,9 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.projects.ProjectManager; +import com.cloud.user.AccountService; +import com.cloud.utils.DateUtil; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsCmd; @@ -35,6 +38,7 @@ import org.apache.cloudstack.api.command.QuotaEnabledCmd; import org.apache.cloudstack.api.command.QuotaListEmailConfigurationCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -51,23 +55,21 @@ import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; -import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.time.DateUtils; import org.springframework.stereotype.Component; import com.cloud.configuration.Config; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; -import com.cloud.exception.PermissionDeniedException; -import com.cloud.server.ManagementService; import com.cloud.user.Account; import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.component.ManagerBase; -import com.cloud.utils.db.Filter; @Component public class QuotaServiceImpl extends ManagerBase implements QuotaService, Configurable, QuotaConfig { @@ -75,9 +77,11 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi @Inject private AccountDao _accountDao; @Inject + private AccountService accountService; + @Inject private QuotaAccountDao _quotaAcc; @Inject - private QuotaUsageDao _quotaUsageDao; + private QuotaUsageJoinDao quotaUsageJoinDao; @Inject private DomainDao _domainDao; @Inject @@ -86,11 +90,11 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi private QuotaBalanceDao _quotaBalanceDao; @Inject private QuotaResponseBuilder _respBldr; + @Inject + private ProjectManager projectMgr; private TimeZone _usageTimezone; - private boolean jsInterpretationEnabled = false; - public QuotaServiceImpl() { super(); } @@ -102,8 +106,6 @@ public boolean configure(String name, Map params) throws Configu String timeZoneStr = ObjectUtils.defaultIfNull(_configDao.getValue(Config.UsageAggregationTimezone.toString()), "GMT"); _usageTimezone = TimeZone.getTimeZone(timeZoneStr); - jsInterpretationEnabled = ManagementService.JsInterpretationEnabled.value(); - return true; } @@ -130,6 +132,7 @@ public List> getCommands() { cmdList.add(QuotaListEmailConfigurationCmd.class); cmdList.add(QuotaPresetVariablesListCmd.class); cmdList.add(QuotaValidateActivationRuleCmd.class); + cmdList.add(QuotaResourceStatementCmd.class); return cmdList; } @@ -151,96 +154,66 @@ public Boolean isQuotaServiceEnabled() { } @Override - public List findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate) { - if ((accountId == null) && (accountName != null) && (domainId != null)) { - Account userAccount = null; - Account caller = CallContext.current().getCallingAccount(); - if (_domainDao.isChildDomain(caller.getDomainId(), domainId)) { - Filter filter = new Filter(AccountVO.class, "id", Boolean.FALSE, null, null); - List accounts = _accountDao.listAccounts(accountName, domainId, filter); - if (!accounts.isEmpty()) { - userAccount = accounts.get(0); - } - if (userAccount != null) { - accountId = userAccount.getId(); - } else { - throw new InvalidParameterValueException("Unable to find account " + accountName + " in domain " + domainId); - } - } else { - throw new PermissionDeniedException("Invalid Domain Id or Account"); - } + public List listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate) { + validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate); + + if (accountId == -1) { + accountId = CallContext.current().getCallingAccountId(); } + Account account = _accountDao.findByIdIncludingRemoved(accountId); + Long domainId = account.getDomainId(); - startDate = startDate == null ? new Date() : startDate; + if (startDate == null && endDate == null) { + logger.debug("Retrieving last quota balance for {}.", account); + QuotaBalanceVO lastQuotaBalance = _quotaBalanceDao.getLastQuotaBalanceEntry(accountId, domainId, null); - if (endDate == null) { - // adjust start date to end of day as there is no end date - startDate = _respBldr.startOfNextDay(startDate); - if (logger.isDebugEnabled()) { - logger.debug("getQuotaBalance1: Getting quota balance records for account: " + accountId + ", domainId: " + domainId + ", on or before " + startDate); - } - List qbrecords = _quotaBalanceDao.lastQuotaBalanceVO(accountId, domainId, startDate); - if (logger.isDebugEnabled()) { - logger.debug("Found records size=" + qbrecords.size()); - } - if (qbrecords.isEmpty()) { - logger.info("Incorrect Date there are no quota records before this date " + startDate); - return qbrecords; - } else { - return qbrecords; - } - } else { - if (startDate.before(endDate)) { - if (logger.isDebugEnabled()) { - logger.debug("getQuotaBalance2: Getting quota balance records for account: " + accountId + ", domainId: " + domainId + ", between " + startDate - + " and " + endDate); - } - List qbrecords = _quotaBalanceDao.findQuotaBalance(accountId, domainId, startDate, endDate); - if (logger.isDebugEnabled()) { - logger.debug("getQuotaBalance3: Found records size=" + qbrecords.size()); - } - if (qbrecords.isEmpty()) { - logger.info("There are no quota records between these dates start date " + startDate + " and end date:" + endDate); - return qbrecords; - } else { - return qbrecords; - } - } else { - throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); + if (lastQuotaBalance == null) { + logger.debug("Did not found a quota balance entry for {}.", account); + return new ArrayList<>(); } + + return List.of(lastQuotaBalance); + } + + if (endDate == null) { + endDate = DateUtils.addDays(new Date(), -1); + } + + List quotaBalances = _quotaBalanceDao.listQuotaBalances(accountId, domainId, startDate, endDate); + + if (quotaBalances.isEmpty()) { + logger.info("There are no quota balances for {} between [{}] and [{}].", account, + DateUtil.getOutputString(startDate), DateUtil.getOutputString(endDate)); } + + return quotaBalances; } - @Override - public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { - // if accountId is not specified, use accountName and domainId - if ((accountId == null) && (accountName != null) && (domainId != null)) { - Account userAccount = null; - Account caller = CallContext.current().getCallingAccount(); - if (_domainDao.isChildDomain(caller.getDomainId(), domainId)) { - Filter filter = new Filter(AccountVO.class, "id", Boolean.FALSE, null, null); - List accounts = _accountDao.listAccounts(accountName, domainId, filter); - if (!accounts.isEmpty()) { - userAccount = accounts.get(0); - } - if (userAccount != null) { - accountId = userAccount.getId(); - } else { - throw new InvalidParameterValueException("Unable to find account " + accountName + " in domain " + domainId); - } - } else { - throw new PermissionDeniedException("Invalid Domain Id or Account"); - } + protected void validateStartDateAndEndDateForListQuotaBalancesForAccount(Date startDate, Date endDate) { + if (startDate == null && endDate != null) { + throw new InvalidParameterValueException("Parameter \"enddate\" must be informed together with parameter \"startdate\"."); + } + + Date now = new Date(); + if (startDate != null && startDate.after(now)) { + throw new InvalidParameterValueException("The last balance can be at most from yesterday; therefore, the start date must be before today."); + } + + if (ObjectUtils.allNotNull(startDate, endDate) && startDate.after(endDate)) { + throw new InvalidParameterValueException("The start date cannot be after the end date."); } + } + @Override + public List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate) { if (startDate.after(endDate)) { throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); } - logger.debug("Getting quota records of type [{}] for account [{}] in domain [{}], between [{}] and [{}].", - usageType, accountId, domainId, startDate, endDate); + logger.debug("Getting quota records of type [{}] for account [{}] in domains [{}], between [{}] and [{}].", + usageType, accountId, domainIds, startDate, endDate); - return _quotaUsageDao.findQuotaUsage(accountId, domainId, usageType, startDate, endDate); + return quotaUsageJoinDao.findQuotaUsage(accountId, domainIds, usageType, null, null, null, startDate, endDate, null); } @Override @@ -292,9 +265,4 @@ public void setMinBalance(Long accountId, Double balance) { _quotaAcc.updateQuotaAccount(accountId, acc); } } - - @Override - public boolean isJsInterpretationEnabled() { - return jsInterpretationEnabled; - } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java index adabc694f25a..cd0eeb2c3b48 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java @@ -16,18 +16,15 @@ // under the License. package org.apache.cloudstack.api.command; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - import org.apache.cloudstack.api.response.QuotaBalanceResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; -import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import junit.framework.TestCase; @@ -36,31 +33,20 @@ public class QuotaBalanceCmdTest extends TestCase { @Mock - QuotaResponseBuilder responseBuilder; + QuotaResponseBuilder quotaResponseBuilderMock; - @Test - public void testQuotaBalanceCmd() throws NoSuchFieldException, IllegalAccessException { - QuotaBalanceCmd cmd = new QuotaBalanceCmd(); - Field rbField = QuotaBalanceCmd.class.getDeclaredField("_responseBuilder"); - rbField.setAccessible(true); - rbField.set(cmd, responseBuilder); + @InjectMocks + @Spy + QuotaBalanceCmd quotaBalanceCmdSpy; - List quotaBalanceVOList = new ArrayList(); - Mockito.when(responseBuilder.getQuotaBalance(Mockito.any(cmd.getClass()))).thenReturn(quotaBalanceVOList); - Mockito.when(responseBuilder.createQuotaLastBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class))).thenReturn(new QuotaBalanceResponse()); - Mockito.when(responseBuilder.createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class))).thenReturn(new QuotaBalanceResponse()); - Mockito.lenient().when(responseBuilder.startOfNextDay(Mockito.any(Date.class))).thenReturn(new Date()); + @Test + public void executeTestSetResponseObject() { + QuotaBalanceResponse expected = new QuotaBalanceResponse(); + Mockito.doReturn(expected).when(quotaResponseBuilderMock).createQuotaBalanceResponse(Mockito.eq(quotaBalanceCmdSpy)); - // end date not specified - cmd.setStartDate(new Date()); - cmd.setEndDate(null); - cmd.execute(); - Mockito.verify(responseBuilder, Mockito.times(1)).createQuotaLastBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class)); - Mockito.verify(responseBuilder, Mockito.times(0)).createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class)); + quotaBalanceCmdSpy.execute(); - // end date specified - cmd.setEndDate(new Date()); - cmd.execute(); - Mockito.verify(responseBuilder, Mockito.times(1)).createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class)); + Assert.assertEquals(expected, quotaBalanceCmdSpy.getResponseObject()); } + } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java index 06dd57ad41d3..b02cffbd2f0a 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsCmdTest.java @@ -16,16 +16,9 @@ // under the License. package org.apache.cloudstack.api.command; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.nullable; - import java.lang.reflect.Field; -import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.QuotaCreditsResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.quota.QuotaService; @@ -78,23 +71,13 @@ public void testQuotaCreditsCmd() throws NoSuchFieldException, IllegalAccessExce AccountVO acc = new AccountVO(); acc.setId(2L); - Mockito.when(accountService.getActiveAccountByName(nullable(String.class), nullable(Long.class))).thenReturn(acc); - - Mockito.when(responseBuilder.addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class))).thenReturn(new QuotaCreditsResponse()); - - // No value provided test - try { - cmd.execute(); - } catch (ServerApiException e) { - assertTrue(e.getErrorCode().equals(ApiErrorCode.PARAM_ERROR)); - } + Mockito.when(responseBuilder.addQuotaCredits(cmd)).thenReturn(new QuotaCreditsResponse()); // With value provided test cmd.setValue(11.80); cmd.execute(); - Mockito.verify(quotaService, Mockito.times(0)).setLockAccount(anyLong(), anyBoolean()); - Mockito.verify(quotaService, Mockito.times(1)).setMinBalance(anyLong(), anyDouble()); - Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(nullable(Long.class), nullable(Long.class), nullable(Double.class), nullable(Long.class), nullable(Boolean.class)); + + Mockito.verify(responseBuilder, Mockito.times(1)).addQuotaCredits(cmd); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java index d6f9f747fa84..e67638db632e 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaStatementCmdTest.java @@ -16,38 +16,29 @@ // under the License. package org.apache.cloudstack.api.command; -import junit.framework.TestCase; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaStatementResponse; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - @RunWith(MockitoJUnitRunner.class) -public class QuotaStatementCmdTest extends TestCase { +public class QuotaStatementCmdTest { @Mock - QuotaResponseBuilder responseBuilder; + QuotaResponseBuilder responseBuilderMock; @Test - public void testQuotaStatementCmd() throws NoSuchFieldException, IllegalAccessException { + public void executeTestVerifyCalls() { QuotaStatementCmd cmd = new QuotaStatementCmd(); cmd.setAccountName("admin"); + cmd.responseBuilder = responseBuilderMock; - Field rbField = QuotaStatementCmd.class.getDeclaredField("_responseBuilder"); - rbField.setAccessible(true); - rbField.set(cmd, responseBuilder); + Mockito.doReturn(new QuotaStatementResponse()).when(responseBuilderMock).createQuotaStatementResponse(Mockito.any()); - List quotaUsageVOList = new ArrayList(); - Mockito.when(responseBuilder.getQuotaUsage(Mockito.eq(cmd))).thenReturn(quotaUsageVOList); - Mockito.when(responseBuilder.createQuotaStatementResponse(Mockito.eq(quotaUsageVOList))).thenReturn(new QuotaStatementResponse()); cmd.execute(); - Mockito.verify(responseBuilder, Mockito.times(1)).getQuotaUsage(Mockito.eq(cmd)); + + Mockito.verify(responseBuilderMock).createQuotaStatementResponse(cmd); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 1f5480404e4b..bcc1c02cde74 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -16,13 +16,13 @@ // under the License. package org.apache.cloudstack.api.response; -import java.lang.reflect.Field; import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; -import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -31,23 +31,29 @@ import java.util.HashSet; import java.util.function.Consumer; +import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.PermissionDeniedException; import com.cloud.user.AccountManager; import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.discovery.ApiDiscoveryService; -import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper; +import org.apache.cloudstack.quota.QuotaManager; import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaStatement; import org.apache.cloudstack.quota.activationrule.presetvariables.PresetVariableDefinition; @@ -68,6 +74,7 @@ import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; +import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.commons.lang3.time.DateUtils; @@ -79,7 +86,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; @@ -89,8 +99,7 @@ import com.cloud.user.User; import junit.framework.TestCase; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; + @RunWith(MockitoJUnitRunner.class) public class QuotaResponseBuilderImplTest extends TestCase { @@ -150,10 +159,10 @@ public class QuotaResponseBuilderImplTest extends TestCase { Date date = new Date(); @Mock - Account accountMock; + AccountVO accountMock; @Mock - DomainVO domainVOMock; + DomainVO domainVoMock; @Mock QuotaConfigureEmailCmd quotaConfigureEmailCmdMock; @@ -161,6 +170,9 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock QuotaAccountVO quotaAccountVOMock; + @Mock + CallContext callContextMock; + @Mock QuotaEmailTemplatesVO quotaEmailTemplatesVoMock; @@ -179,22 +191,22 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock User callerUserMock; + @Mock + EntityManager entityManagerMock; + + @Mock + QuotaManager quotaManagerMock; + + @Mock + QuotaBalanceVO quotaBalanceVoMock; + @Before public void setup() { CallContext.register(callerUserMock, callerAccountMock); } - private void overrideDefaultQuotaEnabledConfigValue(final Object value) throws IllegalAccessException, NoSuchFieldException { - Field f = ConfigKey.class.getDeclaredField("_defaultValue"); - f.setAccessible(true); - f.set(QuotaConfig.QuotaAccountEnabled, value); - } - - private Calendar[] createPeriodForQuotaSummary() { - final Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - return new Calendar[] {calendar, calendar}; - } + @Mock + Pair, Integer> quotaSummaryResponseMock1, quotaSummaryResponseMock2; @Mock QuotaValidateActivationRuleCmd quotaValidateActivationRuleCmdMock = Mockito.mock(QuotaValidateActivationRuleCmd.class); @@ -239,28 +251,6 @@ public void createQuotaTariffResponseTestIfReturnsActivationRuleWithoutPermissio assertNull(tariffResponse.getActivationRule()); } - @Test - public void testAddQuotaCredits() { - final long accountId = 2L; - final long domainId = 1L; - final double amount = 11.0; - final long updatedBy = 2L; - - QuotaCreditsVO credit = new QuotaCreditsVO(); - credit.setCredit(new BigDecimal(amount)); - - Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit); - Mockito.when(quotaBalanceDaoMock.lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class))).thenReturn(new BigDecimal(111)); - Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit); - - AccountVO account = new AccountVO(); - account.setState(Account.State.LOCKED); - Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account); - - QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true); - assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0); - } - @Test public void testListQuotaEmailTemplates() { QuotaEmailTemplateListCmd cmd = new QuotaEmailTemplateListCmd(); @@ -296,33 +286,6 @@ public void testUpdateQuotaEmailTemplate() { assertTrue(quotaResponseBuilderSpy.updateQuotaEmailTemplate(cmd)); } - @Test - public void testCreateQuotaLastBalanceResponse() { - List quotaBalance = new ArrayList<>(); - // null balance test - try { - quotaResponseBuilderSpy.createQuotaLastBalanceResponse(null, new Date()); - } catch (InvalidParameterValueException e) { - assertTrue(e.getMessage().equals("There are no balance entries on or before the requested date.")); - } - - // empty balance test - try { - quotaResponseBuilderSpy.createQuotaLastBalanceResponse(quotaBalance, new Date()); - } catch (InvalidParameterValueException e) { - assertTrue(e.getMessage().equals("There are no balance entries on or before the requested date.")); - } - - // valid balance test - QuotaBalanceVO entry = new QuotaBalanceVO(); - entry.setAccountId(2L); - entry.setCreditBalance(new BigDecimal(100)); - quotaBalance.add(entry); - quotaBalance.add(entry); - QuotaBalanceResponse resp = quotaResponseBuilderSpy.createQuotaLastBalanceResponse(quotaBalance, null); - assertTrue(resp.getStartQuota().compareTo(new BigDecimal(200)) == 0); - } - @Test public void testStartOfNextDayWithoutParameters() { Date nextDate = quotaResponseBuilderSpy.startOfNextDay(); @@ -466,36 +429,6 @@ public void deleteQuotaTariffTestUpdateRemoved() { Mockito.verify(quotaTariffVoMock).setRemoved(Mockito.any(Date.class)); } - @Test - public void getQuotaSummaryResponseTestAccountIsNotNullQuotaIsDisabledShouldReturnFalse() throws NoSuchFieldException, IllegalAccessException { - Calendar[] period = createPeriodForQuotaSummary(); - overrideDefaultQuotaEnabledConfigValue("false"); - - Mockito.doReturn(period).when(quotaStatementMock).getCurrentStatementTime(); - Mockito.doReturn(domainVOMock).when(domainDaoMock).findById(Mockito.anyLong()); - Mockito.doReturn(BigDecimal.ZERO).when(quotaBalanceDaoMock).lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class)); - Mockito.doReturn(BigDecimal.ZERO).when(quotaUsageDaoMock).findTotalQuotaUsage(Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Date.class), Mockito.any(Date.class)); - - QuotaSummaryResponse quotaSummaryResponse = quotaResponseBuilderSpy.getQuotaSummaryResponse(accountMock); - - assertFalse(quotaSummaryResponse.getQuotaEnabled()); - } - - @Test - public void getQuotaSummaryResponseTestAccountIsNotNullQuotaIsEnabledShouldReturnTrue() throws NoSuchFieldException, IllegalAccessException { - Calendar[] period = createPeriodForQuotaSummary(); - overrideDefaultQuotaEnabledConfigValue("true"); - - Mockito.doReturn(period).when(quotaStatementMock).getCurrentStatementTime(); - Mockito.doReturn(domainVOMock).when(domainDaoMock).findById(Mockito.anyLong()); - Mockito.doReturn(BigDecimal.ZERO).when(quotaBalanceDaoMock).lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class)); - Mockito.doReturn(BigDecimal.ZERO).when(quotaUsageDaoMock).findTotalQuotaUsage(Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Date.class), Mockito.any(Date.class)); - - QuotaSummaryResponse quotaSummaryResponse = quotaResponseBuilderSpy.getQuotaSummaryResponse(accountMock); - - assertTrue(quotaSummaryResponse.getQuotaEnabled()); - } - @Test public void filterSupportedTypesTestReturnWhenQuotaTypeDoesNotMatch() throws NoSuchFieldException { List> variables = new ArrayList<>(); @@ -576,6 +509,63 @@ public void validateQuotaConfigureEmailCmdParametersTestWithTemplateNameAndEnabl quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock); } + @Test + public void createQuotaSummaryResponseTestNotListAllAndAllAccountTypesReturnsSingleRecord() { + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + + try(MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + for (Account.Type type : Account.Type.values()) { + Mockito.doReturn(type).when(accountMock).getType(); + + Pair, Integer> result = quotaResponseBuilderSpy.createQuotaSummaryResponse(cmd); + Assert.assertEquals(quotaSummaryResponseMock1, result); + } + + Mockito.verify(quotaResponseBuilderSpy, Mockito.times(Account.Type.values().length)).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any()); + }; + } + + @Test + public void getDomainPathByDomainIdForDomainAdminTestAccountNotDomainAdminReturnsNull() { + for (Account.Type type : Account.Type.values()) { + if (Account.Type.DOMAIN_ADMIN.equals(type)) { + continue; + } + + Mockito.doReturn(type).when(accountMock).getType(); + Assert.assertNull(quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock)); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void getDomainPathByDomainIdForDomainAdminTestDomainFromCallerIsNullThrowsInvalidParameterValueException() { + Mockito.doReturn(Account.Type.DOMAIN_ADMIN).when(accountMock).getType(); + Mockito.doReturn(null).when(domainDaoMock).findById(Mockito.anyLong()); + Mockito.lenient().doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + + quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock); + } + + @Test + public void getDomainPathByDomainIdForDomainAdminTestDomainFromCallerIsNotNullReturnsPath() { + String expected = "/test/"; + + Mockito.doReturn(Account.Type.DOMAIN_ADMIN).when(accountMock).getType(); + Mockito.doReturn(domainVoMock).when(domainDaoMock).findById(Mockito.anyLong()); + Mockito.doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.doReturn(expected).when(domainVoMock).getPath(); + + String result = quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock); + Assert.assertEquals(expected, result); + } + @Test public void getQuotaEmailConfigurationVoTestTemplateNameIsNull() { Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getTemplateName(); @@ -652,7 +642,7 @@ public void isUserAllowedToSeeActivationRulesTestWithPermissionToCreateTariff() ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -668,7 +658,7 @@ public void isUserAllowedToSeeActivationRulesTestWithPermissionToUpdateTariff() ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertTrue(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -684,7 +674,7 @@ public void isUserAllowedToSeeActivationRulesTestWithNoPermission() { ListResponse responseList = new ListResponse<>(); responseList.setResponses(cmdList); - Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null); + Mockito.doReturn(responseList).when(discoveryServiceMock).listApis(userMock, null, null); assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } @@ -706,33 +696,18 @@ public void createQuotaCreditsListResponseTestReturnsObject() { } private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() { - Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong()); - QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); - cmd.setAccountId(1L); - cmd.setDomainId(2L); + QuotaCreditsListCmd cmd = Mockito.mock(QuotaCreditsListCmd.class); + Mockito.doReturn(1L).when(cmd).getEntityOwnerId(); + Mockito.doReturn(2L).when(cmd).getDomainId(); + Mockito.doReturn(new Date()).when(cmd).getStartDate(); + Mockito.doReturn(new Date()).when(cmd).getEndDate(); return cmd; } - @Test(expected = InvalidParameterValueException.class) - public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() { - QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); - - quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); - } - @Test(expected = InvalidParameterValueException.class) public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() { QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); - cmd.setStartDate(new Date()); - cmd.setEndDate(DateUtils.addDays(new Date(), -1)); - - quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); - } - - @Test(expected = PermissionDeniedException.class) - public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() { - QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); - Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong()); + Mockito.doReturn(DateUtils.addDays(new Date(), -1)).when(cmd).getEndDate(); quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); } @@ -743,7 +718,7 @@ public void getCreditsForQuotaCreditsListTestReturnsData() { List expected = new ArrayList<>(); expected.add(new QuotaCreditsVO()); - Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.any()); List result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); @@ -892,4 +867,505 @@ public void injectUsageTypeVariablesTestReturnInjectedVariables() { Assert.assertTrue(formattedVariables.containsValue("accountname")); Assert.assertTrue(formattedVariables.containsValue("zonename")); } + + private List getQuotaBalancesForTest() { + List balances = new ArrayList<>(); + + QuotaBalanceVO balance = new QuotaBalanceVO(); + balance.setUpdatedOn(new Date()); + balance.setCreditBalance(BigDecimal.valueOf(-10.42)); + balances.add(balance); + + balance = new QuotaBalanceVO(); + balance.setUpdatedOn(new Date()); + balance.setCreditBalance(BigDecimal.valueOf(-18.94)); + balances.add(balance); + + balance = new QuotaBalanceVO(); + balance.setUpdatedOn(new Date()); + balance.setCreditBalance(BigDecimal.valueOf(-29.37)); + balances.add(balance); + + return balances; + } + + @Test + public void createQuotaBalancesResponseTestCreateResponse() { + List balances = getQuotaBalancesForTest(); + + QuotaBalanceResponse expected = new QuotaBalanceResponse(); + expected.setObjectName("balance"); + expected.setCurrency("$"); + + Mockito.doReturn(balances).when(quotaServiceMock).listQuotaBalancesForAccount(Mockito.any(), Mockito.any(), Mockito.any()); + QuotaBalanceResponse result = quotaResponseBuilderSpy.createQuotaBalanceResponse(new QuotaBalanceCmd()); + + Assert.assertEquals(expected.getCurrency(), result.getCurrency()); + + for (int i = 0; i < balances.size(); i++) { + Assert.assertEquals(balances.get(i).getUpdatedOn(), result.getBalances().get(i).getDate()); + Assert.assertEquals(balances.get(i).getCreditBalance(), result.getBalances().get(i).getBalance()); + } + } + + @Test + public void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformedTestUsageTypeDifferentFromNullDoNothing() { + List listUsage = new ArrayList<>(); + + quotaResponseBuilderSpy.createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(listUsage, 1); + + Assert.assertTrue(listUsage.isEmpty()); + } + + @Test + public void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformedTestUsageTypeIsNullAddDummyForAllQuotaTypes() { + List listUsage = new ArrayList<>(); + listUsage.add(new QuotaUsageJoinVO()); + + quotaResponseBuilderSpy.createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(listUsage, null); + + Assert.assertEquals(QuotaTypes.listQuotaTypes().size() + 1, listUsage.size()); + + QuotaTypes.listQuotaTypes().entrySet().forEach(entry -> { + Assert.assertTrue(listUsage.stream().anyMatch(usage -> usage.getUsageType() == entry.getKey() && usage.getQuotaUsed().equals(BigDecimal.ZERO))); + }); + } + + private List getQuotaUsagesForTest() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + List quotaUsages = new ArrayList<>(); + + QuotaUsageJoinVO quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(1l); + quotaUsage.setDomainId(2l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(BigDecimal.valueOf(10)); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-01")); + quotaUsage.setEndDate(sdf.parse("2022-01-02")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(4l); + quotaUsage.setDomainId(5l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(null); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-03")); + quotaUsage.setEndDate(sdf.parse("2022-01-04")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + quotaUsage = new QuotaUsageJoinVO(); + quotaUsage.setAccountId(6l); + quotaUsage.setDomainId(7l); + quotaUsage.setUsageType(3); + quotaUsage.setQuotaUsed(BigDecimal.valueOf(5)); + try { + quotaUsage.setStartDate(sdf.parse("2022-01-05")); + quotaUsage.setEndDate(sdf.parse("2022-01-06")); + } catch (ParseException ignored) { + } + quotaUsages.add(quotaUsage); + + return quotaUsages; + } + + @Test + public void createStatementItemTestReturnItem() { + List quotaUsages = getQuotaUsagesForTest(); + + QuotaStatementItemResponse result = quotaResponseBuilderSpy.createStatementItem(0, quotaUsages, false); + + QuotaUsageJoinVO expected = quotaUsages.get(0); + QuotaTypes quotaTypeExpected = QuotaTypes.listQuotaTypes().get(expected.getUsageType()); + Assert.assertEquals(BigDecimal.valueOf(15), result.getQuotaUsed()); + Assert.assertEquals(quotaTypeExpected.getQuotaUnit(), result.getUsageUnit()); + Assert.assertEquals(quotaTypeExpected.getQuotaName(), result.getUsageName()); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() { + long providedAccountId = 200L; + + Mockito.when(accountDaoMock.findByIdIncludingRemoved(providedAccountId)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(providedAccountId, null); + + Assert.assertEquals(200L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.ADMIN); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); + + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsDomainAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.DOMAIN_ADMIN); + + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); + + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsFallbackAccountWhenNoAccountProvidedAndCallerIsNotAdmin() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.NORMAL); + + Mockito.when(accountDaoMock.findByIdIncludingRemoved(300L)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, 300L); + + assertEquals(300L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + } + + @Test + public void getAccountIdForQuotaStatementTestAccessDeniedForProvidedAccount() { + Mockito.when(accountDaoMock.findByIdIncludingRemoved(200L)).thenReturn(accountMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getAccountIdForQuotaStatement(200L, null)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsNullPairWhenAccountIsProvided() { + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(100L, null, false); + + assertNull(result.first()); + assertNull(result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainIdNonRecursively() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false); + + assertEquals(5L, (long) result.first()); + assertEquals(List.of(5L), result.second()); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsCallerDomainNonRecursively() { + Mockito.when(callerAccountMock.getDomainId()).thenReturn(7L); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, false); + + assertEquals(7L, (long) result.first()); + assertEquals(List.of(7L), result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainRecursively() { + List domainAndChildren = List.of(5L, 10L, 15L); + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(5L)).thenReturn(domainAndChildren); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, true); + + assertEquals(5L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(5L); + } + + @Test + public void getDomainIdsForQuotaStatementReturnsCallerDomainRecursively() { + List domainAndChildren = List.of(1L, 2L, 3L); + Mockito.when(callerAccountMock.getDomainId()).thenReturn(1L); + + Mockito.when(domainDaoMock.getDomainAndChildrenIds(1L)).thenReturn(domainAndChildren); + + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, true); + + assertEquals(1L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(1L); + } + + @Test + public void getDomainIdsForQuotaStatementTestThrowsAccessDeniedForProvidedDomain() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void retrieveResourceTestThrowsExceptionForInvalidUsageType() { + Integer invalidUsageType = 999; + quotaResponseBuilderSpy.retrieveResource("validUuid", invalidUsageType); + } + + @Test + public void retrieveResourceTestReturnsNullForNonexistentResource() { + String invalidUuid = "nonexistentUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + + Mockito.doReturn(null).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(invalidUuid)); + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(invalidUuid, validUsageType); + + Assert.assertNull(result); + } + + @Test + public void retrieveResourceTestReturnsCorrectResource() { + String validUuid = "validUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + InternalIdentity mockResource = Mockito.mock(InternalIdentity.class); + + Mockito.doReturn(mockResource).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(validUuid)); + + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(validUuid, validUsageType); + + Assert.assertNotNull(result); + Assert.assertEquals(mockResource, result); + } + + @Test + public void lockOrUnlockAccountIfRequiredTestPositiveBalanceUnlocksAccount() { + Mockito.doReturn(Account.State.LOCKED).when(accountMock).getState(); + + quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.TEN, accountMock, true); + + Mockito.verify(accountManagerMock).enableAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId()); + Mockito.verify(accountManagerMock, Mockito.never()).lockAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void lockOrUnlockAccountIfRequiredTestNegativeBalanceLocksAccount() { + Mockito.doReturn(Account.State.ENABLED).when(accountMock).getState(); + Mockito.doReturn(true).when(quotaManagerMock).isLockable(accountMock); + + quotaResponseBuilderSpy.lockOrUnlockAccountIfRequired(BigDecimal.valueOf(-10), accountMock, true); + + Mockito.verify(accountManagerMock).lockAccount(accountMock.getAccountName(), domainVoMock.getId(), accountMock.getId()); + Mockito.verify(accountManagerMock, Mockito.never()).enableAccount(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void addQuotaCreditsTestValidParameters() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(10D).when(cmd).getValue(); + Mockito.doReturn(BigDecimal.TEN).when(quotaCreditsVoMock).getCredit(); + Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(null).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), + Mockito.any()); + Mockito.doReturn(quotaCreditsVoMock).when(quotaResponseBuilderSpy).persistQuotaCredits(Mockito.any(), Mockito.anyDouble(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(Mockito.any()); + + QuotaCreditsResponse response = quotaResponseBuilderSpy.addQuotaCredits(cmd); + + Assert.assertEquals(BigDecimal.TEN, response.getCredit()); + } + + @Test(expected = InvalidParameterValueException.class) + public void addQuotaCreditsTestThrowsExceptionWhenValueIsNull() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(null).when(cmd).getValue(); + + quotaResponseBuilderSpy.addQuotaCredits(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void addQuotaCreditsTestThrowsExceptionWhenDepositDateIsIncorrect() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Mockito.doReturn(100.0).when(cmd).getValue(); + Mockito.doReturn(accountMock).when(accountDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoMock).findLaterBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + + quotaResponseBuilderSpy.addQuotaCredits(cmd); + } + + @Test + public void persistQuotaCreditsTestSavesCreditsAndBalanceSuccessfully() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Long domainId = 2L; + Double value = 10D; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + BigDecimal currentBalance = BigDecimal.ZERO; + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.doReturn(domainId).when(account).getDomainId(); + Mockito.doReturn(null).when(cmd).getQuotaEnforce(); + Mockito.doReturn(null).when(cmd).getMinBalance(); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance); + + QuotaCreditsVO result = quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Assert.assertNotNull(result); + Assert.assertEquals(BigDecimal.TEN, result.getCredit()); + Assert.assertEquals(accountId, result.getAccountId()); + Assert.assertEquals(depositedOn, result.getUpdatedOn()); + Mockito.verify(quotaServiceMock).saveQuotaAccount(account, currentBalance, depositedOn); + Mockito.verify(quotaServiceMock, Mockito.never()).setLockAccount(Mockito.anyLong(), Mockito.anyBoolean()); + Mockito.verify(quotaServiceMock, Mockito.never()).setMinBalance(Mockito.anyLong(), Mockito.anyDouble()); + Mockito.verify(quotaResponseBuilderSpy, Mockito.never()).lockOrUnlockAccountIfRequired(Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + } + + @Test + public void persistQuotaCreditsTestCallsSetLockAccountWhenQuotaEnforceProvided() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.when(cmd.getQuotaEnforce()).thenReturn(Boolean.TRUE); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Mockito.verify(quotaServiceMock).setLockAccount(accountId, Boolean.TRUE); + } + + @Test + public void persistQuotaCreditsTestCallsSetMinBalanceWhenProvided() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + + Mockito.when(cmd.getMinBalance()).thenReturn(50.0); + Mockito.doReturn(accountId).when(account).getId(); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, false); + + Mockito.verify(quotaServiceMock).setMinBalance(accountId, 50.0); + } + + @Test + public void persistQuotaCreditsTestLocksOrUnlocksAccountWhenEnforcementIsEnabledGlobally() { + QuotaCreditsCmd cmd = Mockito.mock(QuotaCreditsCmd.class); + Long accountId = 1L; + Long domainId = 2L; + Double value = 100.0; + Date depositedOn = new Date(); + AccountVO account = Mockito.mock(AccountVO.class); + BigDecimal currentBalance = BigDecimal.ZERO; + + Mockito.doReturn(accountId).when(account).getId(); + Mockito.doReturn(domainId).when(account).getDomainId(); + Mockito.when(cmd.getMinBalance()).thenReturn(50.0); + Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(quotaBalanceDaoMock.getLastQuotaBalance(accountId, domainId)).thenReturn(currentBalance); + + quotaResponseBuilderSpy.persistQuotaCredits(cmd, value, depositedOn, account, true); + + Mockito.verify(quotaResponseBuilderSpy).lockOrUnlockAccountIfRequired(currentBalance, account, false); + } + + @Test + public void createQuotaConsumptionHistoryTestReturnsNullForZeroQuotaUsed() { + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(new ArrayList<>(), BigDecimal.ZERO); + + Assert.assertNull(result); + } + + @Test + public void createQuotaConsumptionHistoryTestIgnoresNullQuotaUsed() { + Date now = new Date(); + + List usageRecords = new ArrayList<>(); + QuotaUsageJoinVO record1 = new QuotaUsageJoinVO(); + record1.setStartDate(now); + record1.setEndDate(now); + record1.setQuotaUsed(null); + record1.setUsageItemId(10L); + + QuotaUsageJoinVO record2 = new QuotaUsageJoinVO(); + record2.setStartDate(new Date(now.getTime() + 1000)); + record2.setEndDate(new Date(now.getTime() + 1000)); + record2.setQuotaUsed(BigDecimal.valueOf(10)); + record2.setUsageItemId(11L); + + usageRecords.add(record1); + usageRecords.add(record2); + + BigDecimal totalQuotaUsed = BigDecimal.valueOf(10); + + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(BigDecimal.valueOf(10), result.get(0).getQuotaConsumed()); + } + + @Test + public void createQuotaConsumptionHistoryTestCorrectlyAggregatesRecords() { + List usageRecords = new ArrayList<>(); + Date now = new Date(); + + QuotaUsageJoinVO record1 = new QuotaUsageJoinVO(); + record1.setStartDate(now); + record1.setEndDate(new Date(now.getTime() + 1000)); + record1.setQuotaUsed(BigDecimal.valueOf(5)); + record1.setUsageItemId(10L); + + QuotaUsageJoinVO record2 = new QuotaUsageJoinVO(); + record2.setStartDate(new Date(now.getTime() + 2000)); + record2.setEndDate(new Date(now.getTime() + 3000)); + record2.setQuotaUsed(BigDecimal.valueOf(15)); + record2.setUsageItemId(11L); + + QuotaUsageJoinVO record3 = new QuotaUsageJoinVO(); + record3.setStartDate(new Date(now.getTime() + 2000)); + record3.setEndDate(new Date(now.getTime() + 3000)); + record3.setQuotaUsed(BigDecimal.valueOf(5)); + record3.setUsageItemId(11L); + + usageRecords.add(record1); + usageRecords.add(record2); + usageRecords.add(record3); + + BigDecimal totalQuotaUsed = BigDecimal.valueOf(25); + + List result = quotaResponseBuilderSpy.createQuotaConsumptionHistory(usageRecords, totalQuotaUsed); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + + QuotaStatementItemHistoryResponse firstHistory = result.get(0); + QuotaStatementItemHistoryResponse secondHistory = result.get(1); + + Assert.assertEquals(BigDecimal.valueOf(5), firstHistory.getQuotaConsumed()); + Assert.assertEquals(record1.getStartDate(), firstHistory.getStartDate()); + Assert.assertEquals(record1.getEndDate(), firstHistory.getEndDate()); + + Assert.assertEquals(BigDecimal.valueOf(20), secondHistory.getQuotaConsumed()); + Assert.assertEquals(record2.getStartDate(), secondHistory.getStartDate()); + Assert.assertEquals(record2.getEndDate(), secondHistory.getEndDate()); + } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 19e756d1d973..c0ee6c5fc3fe 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -18,6 +18,8 @@ import com.cloud.configuration.Config; import com.cloud.domain.dao.DomainDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.db.TransactionLegacy; import junit.framework.TestCase; @@ -27,19 +29,23 @@ import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.commons.lang3.time.DateUtils; import org.joda.time.DateTime; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import javax.naming.ConfigurationException; import java.lang.reflect.Field; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -48,7 +54,9 @@ public class QuotaServiceImplTest extends TestCase { @Mock - AccountDao accountDao; + AccountVO accountVoMock; + @Mock + AccountDao accountDaoMock; @Mock QuotaAccountDao quotaAcc; @Mock @@ -60,9 +68,13 @@ public class QuotaServiceImplTest extends TestCase { @Mock QuotaBalanceDao quotaBalanceDao; @Mock + QuotaUsageJoinDao quotaUsageJoinDaoMock; + @Mock QuotaResponseBuilder respBldr; + @Spy + @InjectMocks + QuotaServiceImpl quotaServiceImplSpy; - QuotaServiceImpl quotaService = new QuotaServiceImpl(); @Before public void setup() throws IllegalAccessException, NoSuchFieldException, ConfigurationException { @@ -71,70 +83,47 @@ public void setup() throws IllegalAccessException, NoSuchFieldException, Configu Field accountDaoField = QuotaServiceImpl.class.getDeclaredField("_accountDao"); accountDaoField.setAccessible(true); - accountDaoField.set(quotaService, accountDao); + accountDaoField.set(quotaServiceImplSpy, accountDaoMock); Field quotaAccountDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaAcc"); quotaAccountDaoField.setAccessible(true); - quotaAccountDaoField.set(quotaService, quotaAcc); + quotaAccountDaoField.set(quotaServiceImplSpy, quotaAcc); - Field quotaUsageDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaUsageDao"); + Field quotaUsageDaoField = QuotaServiceImpl.class.getDeclaredField("quotaUsageJoinDao"); quotaUsageDaoField.setAccessible(true); - quotaUsageDaoField.set(quotaService, quotaUsageDao); + quotaUsageDaoField.set(quotaServiceImplSpy, quotaUsageJoinDaoMock); Field domainDaoField = QuotaServiceImpl.class.getDeclaredField("_domainDao"); domainDaoField.setAccessible(true); - domainDaoField.set(quotaService, domainDao); + domainDaoField.set(quotaServiceImplSpy, domainDao); Field configDaoField = QuotaServiceImpl.class.getDeclaredField("_configDao"); configDaoField.setAccessible(true); - configDaoField.set(quotaService, configDao); + configDaoField.set(quotaServiceImplSpy, configDao); Field balanceDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaBalanceDao"); balanceDaoField.setAccessible(true); - balanceDaoField.set(quotaService, quotaBalanceDao); + balanceDaoField.set(quotaServiceImplSpy, quotaBalanceDao); Field QuotaResponseBuilderField = QuotaServiceImpl.class.getDeclaredField("_respBldr"); QuotaResponseBuilderField.setAccessible(true); - QuotaResponseBuilderField.set(quotaService, respBldr); + QuotaResponseBuilderField.set(quotaServiceImplSpy, respBldr); Mockito.when(configDao.getValue(Mockito.eq(Config.UsageAggregationTimezone.toString()))).thenReturn("IST"); - quotaService.configure("randomName", null); - } - - @Test - public void testFindQuotaBalanceVO() { - final long accountId = 2L; - final String accountName = "admin123"; - final long domainId = 1L; - final Date startDate = new DateTime().minusDays(2).toDate(); - final Date endDate = new Date(); - - List records = new ArrayList<>(); - QuotaBalanceVO qb = new QuotaBalanceVO(); - qb.setCreditBalance(new BigDecimal(100)); - qb.setAccountId(accountId); - records.add(qb); - - Mockito.when(respBldr.startOfNextDay(Mockito.any(Date.class))).thenReturn(startDate); - Mockito.when(quotaBalanceDao.findQuotaBalance(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.any(Date.class), Mockito.any(Date.class))).thenReturn(records); - Mockito.when(quotaBalanceDao.lastQuotaBalanceVO(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.any(Date.class))).thenReturn(records); - - // with enddate - assertTrue(quotaService.findQuotaBalanceVO(accountId, accountName, domainId, startDate, endDate).get(0).equals(qb)); - // without enddate - assertTrue(quotaService.findQuotaBalanceVO(accountId, accountName, domainId, startDate, null).get(0).equals(qb)); + quotaServiceImplSpy.configure("randomName", null); } @Test public void testGetQuotaUsage() { final long accountId = 2L; final String accountName = "admin123"; - final long domainId = 1L; + final List domainIds = List.of(1L); final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - quotaService.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); - Mockito.verify(quotaUsageDao, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(Date.class), Mockito.any(Date.class)); + quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainIds, QuotaTypes.IP_ADDRESS, startDate, endDate); + Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainIds), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.any(Date.class), Mockito.any(Date.class), Mockito.any()); } @Test @@ -142,13 +131,13 @@ public void testSetLockAccount() { // existing account QuotaAccountVO quotaAccountVO = new QuotaAccountVO(); Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(quotaAccountVO); - quotaService.setLockAccount(2L, true); + quotaServiceImplSpy.setLockAccount(2L, true); Mockito.verify(quotaAcc, Mockito.times(0)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); Mockito.verify(quotaAcc, Mockito.times(1)).updateQuotaAccount(Mockito.anyLong(), Mockito.any(QuotaAccountVO.class)); // new account Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(null); - quotaService.setLockAccount(2L, true); + quotaServiceImplSpy.setLockAccount(2L, true); Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); } @@ -160,13 +149,76 @@ public void testSetMinBalance() { // existing account setting QuotaAccountVO quotaAccountVO = new QuotaAccountVO(); Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(quotaAccountVO); - quotaService.setMinBalance(accountId, balance); + quotaServiceImplSpy.setMinBalance(accountId, balance); Mockito.verify(quotaAcc, Mockito.times(0)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); Mockito.verify(quotaAcc, Mockito.times(1)).updateQuotaAccount(Mockito.anyLong(), Mockito.any(QuotaAccountVO.class)); // no account with limit set Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(null); - quotaService.setMinBalance(accountId, balance); + quotaServiceImplSpy.setMinBalance(accountId, balance); Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); } + + @Test(expected = InvalidParameterValueException.class) + public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsNullAndEndDateIsNotNullThrowsInvalidParameterException() { + quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(null, new Date()); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsAfterNowThrowsInvalidParameterValueException() { + Date startDate = DateUtils.addMinutes(new Date(), 1); + quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, null); + } + + @Test + public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestEndDateIsAfterNowDoesNothing() { + Date startDate = DateUtils.addMinutes(new Date(), -1); + Date endDate = DateUtils.addMinutes(new Date(), 1); + quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsAfterEndDateThrowsInvalidParameterValueException() { + Date startDate = DateUtils.addMinutes(new Date(), -10); + Date endDate = DateUtils.addMinutes(new Date(), -15); + quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate); + } + + @Test + public void listQuotaBalancesForAccountTestLastQuotaBalanceIsNullReturnsEmptyList() { + Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any()); + Mockito.doReturn(null).when(quotaBalanceDao).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + List result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, null, null); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void listQuotaBalancesForAccountTestLastQuotaBalanceIsNotNullReturnsIt() { + QuotaBalanceVO expected = new QuotaBalanceVO(); + + Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any()); + Mockito.doReturn(expected).when(quotaBalanceDao).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any()); + Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + List result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, null, null); + + Assert.assertEquals(expected, result.get(0)); + } + + @Test + public void listQuotaBalancesForAccountTestReturnsQuotaBalances() { + List expected = new ArrayList<>(); + + Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any()); + Mockito.doReturn(expected).when(quotaBalanceDao).listQuotaBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any()); + Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + List result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, new Date(), null); + + Assert.assertEquals(expected, result); + } + } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateClusterCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateClusterCmd.java index f154d6e357fc..049a0227f359 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateClusterCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateClusterCmd.java @@ -81,7 +81,13 @@ public String getEventType() { @Override public String getEventDescription() { - return "dedicating a cluster"; + String baseDescription = "Dedicating cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID) + " to domain with ID: " + getResourceUuid(ApiConstants.DOMAIN_ID); + + if (accountName != null) { + baseDescription = baseDescription + " and account " + accountName; + } + + return baseDescription; } ///////////////////////////////////////////////////// diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateHostCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateHostCmd.java index 63324bcbbe85..0a953357cc1e 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateHostCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateHostCmd.java @@ -111,6 +111,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "dedicating a host"; + String baseDescription = "Dedicating host with ID: " + getResourceUuid(ApiConstants.ID) + " to domain with ID: " + getResourceUuid(ApiConstants.DOMAIN_ID); + + if (accountName != null) { + baseDescription += " and to account " + accountName; + } + + return baseDescription; } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicatePodCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicatePodCmd.java index 74cd7f06960e..6cd997a85065 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicatePodCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicatePodCmd.java @@ -112,6 +112,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "dedicating a pod"; + String baseDescription = "Dedicating pod with ID:" + getResourceUuid(ApiConstants.POD_ID) + " to domain with ID: " + getResourceUuid(ApiConstants.DOMAIN_ID); + + if (accountName != null) { + baseDescription += baseDescription + " and account " + accountName; + } + + return baseDescription; } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateZoneCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateZoneCmd.java index d8530421c2e7..48e98ac24158 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateZoneCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/DedicateZoneCmd.java @@ -112,6 +112,12 @@ public String getEventType() { @Override public String getEventDescription() { - return "dedicating a zone"; + String baseDescription = "Dedicating zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID) + " to domain with ID: " + getResourceUuid(ApiConstants.DOMAIN_ID); + + if (accountName != null) { + baseDescription += " and to account " + accountName; + } + + return baseDescription; } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedClusterCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedClusterCmd.java index 69b31c3515b3..2b51f02ea248 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedClusterCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedClusterCmd.java @@ -81,6 +81,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "releasing dedicated cluster"; + return "Releasing dedicated cluster with ID: " + getResourceUuid(ApiConstants.CLUSTER_ID); } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedHostCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedHostCmd.java index 0a218302fe2b..199eb65c13c6 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedHostCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedHostCmd.java @@ -81,6 +81,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "releasing dedicated host"; + return "Releasing dedicated host with ID: " + getResourceUuid(ApiConstants.HOST_ID); } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedPodCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedPodCmd.java index eeddd9c4eb19..0aad33264170 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedPodCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedPodCmd.java @@ -81,6 +81,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "releasing dedicated pod"; + return "Releasing dedicated pod with ID: " + getResourceUuid(ApiConstants.POD_ID); } } diff --git a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedZoneCmd.java b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedZoneCmd.java index f4dfbcb264a3..ba6902deeb1f 100644 --- a/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedZoneCmd.java +++ b/plugins/dedicated-resources/src/main/java/org/apache/cloudstack/api/commands/ReleaseDedicatedZoneCmd.java @@ -81,6 +81,6 @@ public String getEventType() { @Override public String getEventDescription() { - return "releasing dedicated zone"; + return "Releasing dedicated zone with ID: " + getResourceUuid(ApiConstants.ZONE_ID); } } diff --git a/plugins/host-allocators/random/pom.xml b/plugins/dns/powerdns/pom.xml similarity index 83% rename from plugins/host-allocators/random/pom.xml rename to plugins/dns/powerdns/pom.xml index 061caf1573ff..3edb74f1ce60 100644 --- a/plugins/host-allocators/random/pom.xml +++ b/plugins/dns/powerdns/pom.xml @@ -17,10 +17,10 @@ under the License. --> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - cloud-plugin-host-allocator-random - Apache CloudStack Plugin - Host Allocator Random + cloud-plugin-dns-powerdns + Apache CloudStack Plugin - PowerDNS org.apache.cloudstack cloudstack-plugins diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java new file mode 100644 index 000000000000..4b28fab17ab1 --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java @@ -0,0 +1,390 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.powerdns; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.exception.DnsAuthenticationException; +import org.apache.cloudstack.dns.exception.DnsConflictException; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.exception.DnsOperationException; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.apache.cloudstack.dns.exception.DnsTransportException; +import org.apache.commons.collections.CollectionUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cloud.utils.StringUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class PowerDnsClient implements AutoCloseable { + public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final int CONNECT_TIMEOUT_MS = 5_000; + private static final int SOCKET_TIMEOUT_MS = 10_000; + private static final int MAX_CONNECTIONS_TOTAL = 50; + private static final int MAX_CONNECTIONS_PER_ROUTE = 10; + private static final String API_PREFIX = "/api/v1"; + public static final String DEFAULT_SERVER_NAME = "localhost"; + + private final CloseableHttpClient httpClient; + + public PowerDnsClient() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL); + connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .setConnectionRequestTimeout(CONNECT_TIMEOUT_MS) + .setSocketTimeout(SOCKET_TIMEOUT_MS) + .build(); + + this.httpClient = HttpClientBuilder.create() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .evictIdleConnections(30, TimeUnit.SECONDS) + .disableCookieManagement() + .build(); + } + + public String resolveServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + if (StringUtils.isNotBlank(externalServerId)) { + return validateServerId(baseUrl, port, apiKey, externalServerId); + } + return discoverAuthoritativeServerId(baseUrl, port, apiKey); + } + + public String validateServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + String encodedServer = URLEncoder.encode(externalServerId, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + encodedServer)); + JsonNode server = execute(request, apiKey, 200); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) { + throw new DnsOperationException(String.format("Server %s is not authoritative type=%s", externalServerId, + server.path("daemon_type").asText(null))); + } + return externalServerId; + } + + public String discoverAuthoritativeServerId(String baseUrl, Integer port, String apiKey) throws DnsProviderException { + String url = buildUrl(baseUrl, port , "/servers"); + HttpGet request = new HttpGet(url); + JsonNode servers = execute(request, apiKey, 200); + if (servers == null || !servers.isArray() || servers.isEmpty()) { + throw new DnsOperationException("No servers returned by PowerDNS API"); + } + String fallbackId = null; + for (JsonNode server : servers) { + String daemonType = server.path("daemon_type").asText(null); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(daemonType)) { + continue; + } + String serverId = server.path(ApiConstants.ID).asText(null); + if (StringUtils.isBlank(serverId)) { + continue; + } + // Prefer localhost if present + if (DEFAULT_SERVER_NAME.equals(serverId)) { + return serverId; + } + if (fallbackId == null) { + fallbackId = serverId; + } + } + if (fallbackId != null) { + return fallbackId; + } + throw new DnsOperationException("No authoritative PowerDNS server found"); + } + + public String createZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, boolean dnsSecFlag, List nameServers) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + ObjectNode json = MAPPER.createObjectNode(); + json.put(ApiConstants.NAME, normalizedZone); + json.put(ApiConstants.KIND, zoneKind); + json.put(ApiConstants.DNS_SEC, dnsSecFlag); + if (!CollectionUtils.isEmpty(nameServers)) { + ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS); + for (String ns : nameServers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); + } + } + HttpPost request = new HttpPost(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones")); + request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8)); + JsonNode response = execute(request, apiKey, 201); + if (response == null) { + throw new DnsOperationException("Empty response from DNS server"); + } + String zoneId = response.path(ApiConstants.ID).asText(); + if (StringUtils.isBlank(zoneId)) { + throw new DnsOperationException("PowerDNS returned empty zone id"); + } + return zoneId; + } + + public void updateZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, Boolean dnsSecFlag, List nameServers) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildUrl(baseUrl, port,"/servers/" + externalServerId + "/zones/" + encodedZone); + + ObjectNode json = MAPPER.createObjectNode(); + if (dnsSecFlag != null) { + json.put(ApiConstants.DNS_SEC, dnsSecFlag); + } + if (StringUtils.isNotBlank(zoneKind)) { + json.put(ApiConstants.KIND, zoneKind); + } + if (!CollectionUtils.isEmpty(nameServers)) { + ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS); + for (String ns : nameServers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); + } + } + HttpPut request = new HttpPut(url); + request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8)); + execute(request, apiKey, 204); + } + + public void deleteZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpDelete request = new HttpDelete(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + execute(request, apiKey, 204, 404); + } + + public String modifyRecord(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String recordName, String type, long ttl, List contents, String changeType) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String normalizedRecord = normalizeRecordName(recordName, normalizedZone); + ObjectNode root = MAPPER.createObjectNode(); + ArrayNode rrsets = root.putArray(ApiConstants.RR_SETS); + ObjectNode rrset = rrsets.addObject(); + rrset.put(ApiConstants.NAME, normalizedRecord); + rrset.put(ApiConstants.TYPE, type.toUpperCase()); + rrset.put(ApiConstants.TTL, ttl); + rrset.put(ApiConstants.CHANGE_TYPE, changeType); + ArrayNode records = rrset.putArray(ApiConstants.RECORDS); + if (!CollectionUtils.isEmpty(contents)) { + for (String content : contents) { + ObjectNode record = records.addObject(); + record.put(ApiConstants.CONTENT, content); + record.put(ApiConstants.DISABLED, false); + } + } + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpPatch request = new HttpPatch(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + request.setEntity(new StringEntity(root.toString(), StandardCharsets.UTF_8)); + execute(request, apiKey, 204); + return normalizedRecord.endsWith(".") ? normalizedRecord.substring(0, normalizedRecord.length() - 1) : normalizedRecord; + } + + public Iterable listRecords(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + JsonNode zoneNode = execute(request, apiKey, 200); + if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) { + return Collections.emptyList(); + } + JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS); + return rrsets.isArray() ? rrsets : Collections.emptyList(); + } + + public boolean zoneExists(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) { + try { + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + execute(request, apiKey, 200); + return true; + } catch (DnsProviderException | IllegalArgumentException e) { + return false; + } + } + + public boolean recordExists(String baseUrl, Integer port, String apiKey, + String externalServerId, String zoneName, + String recordName, String type) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String normalizedRecord = normalizeRecordName(recordName, normalizedZone); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String urlPath = "/servers/" + externalServerId + "/zones/" + encodedZone + + "?rrset_name=" + URLEncoder.encode(normalizedRecord, StandardCharsets.UTF_8) + + "&rrset_type=" + type.toUpperCase(); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, urlPath)); + JsonNode zoneNode = execute(request, apiKey, 200); + if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) { + return false; + } + JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS); + return rrsets.isArray() && !rrsets.isEmpty(); + } + + private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedStatus) throws DnsProviderException { + request.addHeader(ApiConstants.X_API_KEY, apiKey); + request.addHeader("Accept", "application/json"); + request.addHeader(ApiConstants.CONTENT_TYPE, "application/json"); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int status = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + + for (int expected : expectedStatus) { + if (status == expected) { + if (body != null && !body.isEmpty()) { + return MAPPER.readTree(body); + } else { + return null; + } + } + } + if (status == 404) { + throw new DnsNotFoundException("Resource not found: " + body); + } else if (status == 401 || status == 403) { + throw new DnsAuthenticationException("Invalid API key"); + } else if (status == 409) { + throw new DnsConflictException("Conflict: " + body); + } + throw new DnsOperationException("Unexpected PowerDNS response: HTTP " + status + " Body: " + body); + } catch (IOException ex) { + throw new DnsTransportException("Error communicating with PowerDNS", ex); + } + } + + private String buildUrl(String baseUrl, Integer port, String path) { + String fullUrl = normalizeBaseUrl(baseUrl); + if (port != null && port > 0) { + try { + URI uri = new URI(fullUrl); + if (uri.getPort() == -1) { + fullUrl = fullUrl + ":" + port; + } + } catch (URISyntaxException e) { + fullUrl = fullUrl + ":" + port; + } + } + if (!path.startsWith("/")) { + path = "/" + path; + } + return fullUrl + API_PREFIX + path; + } + + private String normalizeBaseUrl(String baseUrl) { + if (baseUrl == null) { + throw new IllegalArgumentException("Base URL cannot be null"); + } + String url = baseUrl.trim(); + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "http://" + url; + } + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + return url; + } + + private String normalizeZone(String zoneName) { + if (StringUtils.isBlank(zoneName)) { + throw new IllegalArgumentException("Zone name must not be null or empty"); + } + String zone = zoneName.trim().toLowerCase(); + if (!zone.endsWith(".")) { + zone = zone + "."; + } + if (zone.length() < 2) { + throw new IllegalArgumentException("Zone name is too short"); + } + return zone; + } + + String normalizeRecordName(String recordName, String zoneName) { + if (recordName == null) { + throw new IllegalArgumentException("Record name must not be null"); + } + String normalizedZone = normalizeZone(zoneName); + String name = recordName.trim().toLowerCase(); + // Apex of the zone + if (name.equals("@") || name.isEmpty()) { + return normalizedZone; + } + + String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1); + // Already absolute (ends with dot) + if (name.endsWith(".")) { + // Check if the record belongs to the zone + if (!name.equals(normalizedZone) && !name.endsWith("." + zoneWithoutDot + ".")) { + throw new IllegalArgumentException( + String.format("Record '%s' does not belong to zone '%s'", recordName, zoneName) + ); + } + return name; + } + if (name.contains(".")) { + return name + "."; + } + // Relative name → append zone + return name + "." + normalizedZone; + } + + @Override + public void close() { + try { + httpClient.close(); + } catch (IOException e) { + logger.warn("Failed to close PowerDNS HTTP client", e); + } + } +} diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java new file mode 100644 index 000000000000..9996f705e481 --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java @@ -0,0 +1,202 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.powerdns; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsProvider; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.apache.logging.log4j.util.Strings; + +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.AdapterBase; +import com.fasterxml.jackson.databind.JsonNode; + +public class PowerDnsProvider extends AdapterBase implements DnsProvider { + private PowerDnsClient client; + static String PDNS_SERVER_ID = "pdnsServerId"; + + @Override + public DnsProviderType getProviderType() { + return DnsProviderType.PowerDNS; + } + + public void validate(DnsServer server) throws DnsProviderException { + validateRequiredServerFields(server); + client.validateServerId(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID)); + } + + @Override + public String validateAndResolveServer(DnsServer server) throws Exception { + validateRequiredServerFields(server); + String resolvedDnsServerId = client.resolveServerId(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID)); + if (Strings.isNotBlank(resolvedDnsServerId)) { + server.appendDetails(PDNS_SERVER_ID, resolvedDnsServerId); + } + return resolvedDnsServerId; + } + + @Override + public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return client.createZone( + server.getUrl(), + server.getPort(), + server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), + zone.getName(), + ApiConstants.NATIVE_ZONE, false, server.getNameServers() + ); + } + + @Override + public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + client.deleteZone(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID), zone.getName()); + } + + @Override + public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + client.updateZone( + server.getUrl(), + server.getPort(), + server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), + zone.getName(), ApiConstants.NATIVE_ZONE, false, server.getNameServers()); + } + + public enum ChangeType { + REPLACE, DELETE + } + + @Override + public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return applyRecord( + server.getUrl(), + server.getPort(), + server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), + zone.getName(), record, ChangeType.REPLACE); + } + + @Override + public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return addRecord(server, zone, record); + } + + @Override + public String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return applyRecord(server.getUrl(), + server.getPort(), + server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), + zone.getName(), record, ChangeType.DELETE); + } + + public String applyRecord(String serverUrl, Integer port, String apiKey, String externalServerId, String zoneName, + DnsRecord record, ChangeType changeType) throws DnsProviderException { + + return client.modifyRecord(serverUrl, port, apiKey, externalServerId, zoneName, record.getName(), + record.getType().name(), record.getTtl(), record.getContents(), changeType.name()); + } + + @Override + public List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + List records = new ArrayList<>(); + Iterable rrsetNodes = client.listRecords(server.getUrl(), server.getPort(), server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), zone.getName()); + + for (JsonNode rrset : rrsetNodes) { + String name = rrset.path(ApiConstants.NAME).asText(); + String typeStr = rrset.path(ApiConstants.TYPE).asText(); + int ttl = rrset.path(ApiConstants.TTL).asInt(0); + if (!"SOA".equalsIgnoreCase(typeStr)) { + try { + List contents = new ArrayList<>(); + JsonNode recordsNode = rrset.path(ApiConstants.RECORDS); + if (recordsNode.isArray()) { + for (JsonNode rec : recordsNode) { + String content = rec.path(ApiConstants.CONTENT).asText(); + if (!content.isEmpty()) { + contents.add(content); + } + } + } + records.add(new DnsRecord(name, DnsRecord.RecordType.valueOf(typeStr), contents, ttl)); + } catch (Exception ignored) { + // Skip unsupported record types + } + } + } + return records; + } + + public boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException { + return client.recordExists(server.getUrl(), server.getPort(), server.getDnsApiKey(), + server.getDetail(PDNS_SERVER_ID), zone.getName(), recordName, recordType); + } + + @Override + public boolean dnsZoneExists(DnsServer server, DnsZone zone) { + return client.zoneExists(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID), zone.getName()); + } + + void validateRequiredServerAndZoneFields(DnsServer server, DnsZone zone) { + validateRequiredServerFields(server); + if (StringUtils.isBlank(zone.getName())) { + throw new IllegalArgumentException("Zone name cannot be empty"); + } + } + + void validateRequiredServerFields(DnsServer server) { + if (StringUtils.isBlank(server.getUrl())) { + throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); + } + if (StringUtils.isBlank(server.getDnsApiKey())) { + throw new IllegalArgumentException("PowerDNS API key cannot be empty"); + } + } + + @Override + public boolean configure(String name, Map params) { + if (client == null) { + client = new PowerDnsClient(); + } + return true; + } + + @Override + public boolean stop() { + if (client != null) { + client.close(); + } + return true; + } +} diff --git a/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties new file mode 100644 index 000000000000..baec3fde6a6a --- /dev/null +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +name=powerdns +parent=dns diff --git a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml similarity index 87% rename from plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml rename to plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml index d84eaafaa5a7..c9e4937dac55 100644 --- a/plugins/host-allocators/random/src/main/resources/META-INF/cloudstack/host-allocator-random/spring-host-allocator-random-context.xml +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml @@ -21,14 +21,11 @@ xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans - http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd + http://www.springframework.org/schema/beans/spring-beans-3.0.xsd + http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/context - http://www.springframework.org/schema/context/spring-context.xsd" + http://www.springframework.org/schema/context/spring-context-3.0.xsd" > - - - + diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java new file mode 100644 index 000000000000..a2b571b154f9 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import static org.apache.cloudstack.dns.DnsProviderUtil.appendPublicSuffixToZone; +import static org.apache.cloudstack.dns.DnsProviderUtil.normalizeDomainForDb; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.logging.log4j.util.Strings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DnsProviderUtilTest { + private final String userZoneName; + private final String publicSuffix; + private final String expectedResult; + private final boolean expectException; + + public DnsProviderUtilTest(String userZoneName, + String publicSuffix, + String expectedResult, + boolean expectException) { + this.userZoneName = userZoneName; + this.publicSuffix = publicSuffix; + this.expectedResult = expectedResult; + this.expectException = expectException; + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"tenant1.com", "example.com", "tenant1.example.com", false}, + {"dev.tenant2.com", "example.com", "dev.tenant2.example.com", false}, + {"tenant3.example.com", "example.com", "tenant3.example.com", false}, + {"Tenant1.CoM", "ExAmple.CoM", "tenant1.example.com", false}, + {"tenant1.com.", "example.com.", "tenant1.example.com", false}, + {"tenant1.com", "", "tenant1.com", false}, + {"tenant1.com", null, "tenant1.com", false}, + {"test.abc.com", "abc.com", "test.abc.com", false}, + {"sub.test.abc.com", "abc.com", "sub.test.abc.com", false}, + {"test.ai.abc.com", "abc.com", "test.ai.abc.com", false}, + {"deep.sub.abc.com", "abc.com", "deep.sub.abc.com", false}, + {"abc.com", "xyz.com", "abc.xyz.com", false}, + {"test.xyz.com", "xyz.com", "test.xyz.com", false}, + {"test.com.xyz.com", "xyz.com", "test.com.xyz.com", false}, + {"tenant", "example.com", null, true}, // single label + {"test", "abc.com", null, true}, + {"example.com.", "example.com", null, true}, + {"example.com", "example.com", null, true}, // root level forbidden + {"abc.com", "abc.com", null, true}, // root level forbidden + {"tenant1.org", "example.com", null, true}, // TLD mismatch + {"test.ai", "abc.com", null, true}, // TLD mismatch + {null, "example.com", null, true}, + }); + } + + @Test + public void testAppendPublicSuffix() { + if (expectException) { + try { + executeAppendSuffixTest(userZoneName, publicSuffix); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ignored) { + // noop + } + } else { + String result; + if (Strings.isNotBlank(publicSuffix)) { + result = executeAppendSuffixTest(userZoneName, publicSuffix); + } else { + result = appendPublicSuffixToZone(normalizeDomainForDb(userZoneName), publicSuffix); + } + assertEquals(expectedResult, result); + } + } + + String executeAppendSuffixTest(String zoneName, String domainSuffix) { + return appendPublicSuffixToZone(normalizeDomainForDb(zoneName), domainSuffix); + } +} diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java new file mode 100644 index 000000000000..f5bd16a81908 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java @@ -0,0 +1,197 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns; + +import static org.apache.cloudstack.dns.DnsRecord.RecordType.A; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.AAAA; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.CNAME; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.MX; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.NS; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.PTR; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.SRV; +import static org.apache.cloudstack.dns.DnsRecord.RecordType.TXT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class NormalizeDnsRecordValueTest { + + private final String description; + private final String input; + private final DnsRecord.RecordType recordType; + private final String expected; + private final boolean expectException; + + public NormalizeDnsRecordValueTest(String description, String input, + DnsRecord.RecordType recordType, + String expected, boolean expectException) { + this.description = description; + this.input = input; + this.recordType = recordType; + this.expected = expected; + this.expectException = expectException; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + + // ---------------------------------------------------------------- + // Guard: blank/null value — all record types should throw + // ---------------------------------------------------------------- + {"null value, A record", null, A, null, true}, + {"empty value, A record", "", A, null, true}, + {"blank value, A record", " ", A, null, true}, + + {"null value, AAAA record", null, AAAA, null, true}, + {"null value, CNAME record", null, CNAME, null, true}, + {"null value, MX record", null, MX, null, true}, + {"null value, TXT record", null, TXT, null, true}, + {"null value, SRV record", null, SRV, null, true}, + {"null value, NS record", null, NS, null, true}, + {"null value, PTR record", null, PTR, null, true}, + + // ---------------------------------------------------------------- + // A record + // ---------------------------------------------------------------- + {"A: valid IPv4", "93.184.216.34", A, "93.184.216.34", false}, + {"A: valid IPv4 with whitespace", " 93.184.216.34 ", A, "93.184.216.34", false}, + {"A: loopback", "127.0.0.1", A, "127.0.0.1", false}, + {"A: all-zeros", "0.0.0.0", A, "0.0.0.0", false}, + {"A: broadcast", "255.255.255.255", A, "255.255.255.255", false}, + {"A: private 10.x", "10.0.0.1", A, "10.0.0.1", false}, + {"A: private 192.168.x", "192.168.1.1", A, "192.168.1.1", false}, + + {"A: IPv6 rejected", "2001:db8::1", A, null, true}, + {"A: domain rejected", "example.com", A, null, true}, + {"A: partial IP rejected", "192.168.1", A, null, true}, + {"A: trailing dot rejected", "93.184.216.34.", A, null, true}, + {"A: octet out of range rejected", "256.0.0.1", A, null, true}, + + // ---------------------------------------------------------------- + // AAAA record + // ---------------------------------------------------------------- + {"AAAA: full IPv6", "2001:0db8:0000:0000:0000:0000:0000:0001", AAAA, + "2001:0db8:0000:0000:0000:0000:0000:0001", false}, + + {"AAAA: compressed IPv6", "2001:db8::1", AAAA, "2001:db8::1", false}, + {"AAAA: loopback", "::1", AAAA, "::1", false}, + {"AAAA: all zeros", "::", AAAA, "::", false}, + {"AAAA: whitespace", " 2001:db8::1 ", AAAA, "2001:db8::1", false}, + + {"AAAA: IPv4 rejected", "93.184.216.34", AAAA, null, true}, + {"AAAA: domain rejected", "example.com", AAAA, null, true}, + {"AAAA: invalid hex rejected", "2001:db8::xyz", AAAA, null, true}, + + // ---------------------------------------------------------------- + // CNAME record + // ---------------------------------------------------------------- + {"CNAME: basic", "target.example.com", CNAME, "target.example.com.", false}, + {"CNAME: uppercase", "TARGET.EXAMPLE.COM", CNAME, "target.example.com.", false}, + {"CNAME: trailing dot", "target.example.com.", CNAME, "target.example.com.", false}, + {"CNAME: whitespace", " target.example.com ", CNAME, "target.example.com.", false}, + {"CNAME: subdomain", "sub.target.example.com", CNAME, "sub.target.example.com.", false}, + + {"CNAME: IP rejected", "192.168.1.1", CNAME, null, true}, + {"CNAME: invalid label", "-bad.example.com", CNAME, null, true}, + + // ---------------------------------------------------------------- + // NS record + // ---------------------------------------------------------------- + {"NS: basic", "ns1.example.com", NS, "ns1.example.com.", false}, + {"NS: uppercase", "NS1.EXAMPLE.COM", NS, "ns1.example.com.", false}, + {"NS: trailing dot", "ns1.example.com.", NS, "ns1.example.com.", false}, + {"NS: subdomain", "ns1.sub.example.com", NS, "ns1.sub.example.com.", false}, + + {"NS: IP rejected", "8.8.8.8", NS, null, true}, + {"NS: invalid label", "ns1-.example.com", NS, null, true}, + + // ---------------------------------------------------------------- + // PTR record + // ---------------------------------------------------------------- + {"PTR: basic", "host.example.com", PTR, "host.example.com.", false}, + {"PTR: in-addr.arpa", "1.168.192.in-addr.arpa", PTR, "1.168.192.in-addr.arpa.", false}, + {"PTR: uppercase", "HOST.EXAMPLE.COM", PTR, "host.example.com.", false}, + {"PTR: trailing dot", "host.example.com.", PTR, "host.example.com.", false}, + + {"PTR: IP rejected", "192.168.1.1", PTR, null, true}, + {"PTR: invalid label", "-host.example.com", PTR, null, true}, + + // ---------------------------------------------------------------- + // MX record + // ---------------------------------------------------------------- + {"MX: standard", "10 mail.example.com", MX, "10 mail.example.com.", false}, + {"MX: zero priority", "0 mail.example.com", MX, "0 mail.example.com.", false}, + {"MX: max priority", "65535 mail.example.com", MX, "65535 mail.example.com.", false}, + {"MX: uppercase", "10 MAIL.EXAMPLE.COM", MX, "10 mail.example.com.", false}, + {"MX: trailing dot", "10 mail.example.com.", MX, "10 mail.example.com.", false}, + {"MX: extra whitespace", "10 mail.example.com", MX, "10 mail.example.com.", false}, + + {"MX: missing domain", "10", MX, null, true}, + {"MX: priority out of range", "65536 mail.example.com", MX, null, true}, + {"MX: non-numeric priority", "abc mail.example.com", MX, null, true}, + {"MX: IP rejected", "10 192.168.1.1", MX, null, true}, + + // ---------------------------------------------------------------- + // SRV record + // ---------------------------------------------------------------- + {"SRV: standard", "10 20 443 target.example.com", SRV, "10 20 443 target.example.com.", false}, + {"SRV: zeros", "0 0 1 target.example.com", SRV, "0 0 1 target.example.com.", false}, + {"SRV: max values", "65535 65535 65535 target.example.com", SRV, "65535 65535 65535 target.example.com.", false}, + {"SRV: uppercase", "10 20 443 TARGET.EXAMPLE.COM", SRV, "10 20 443 target.example.com.", false}, + {"SRV: trailing dot", "10 20 443 target.example.com.", SRV, "10 20 443 target.example.com.", false}, + + {"SRV: missing target", "10 20 443", SRV, null, true}, + {"SRV: port 0", "10 20 0 target.example.com", SRV, null, true}, + {"SRV: priority out of range", "65536 20 443 target.example.com", SRV, null, true}, + {"SRV: IP rejected", "10 20 443 192.168.1.1", SRV, null, true}, + + // ---------------------------------------------------------------- + // TXT record + // ---------------------------------------------------------------- + {"TXT: trim", " hello world ", TXT, "hello world", false}, + {"TXT: already clean", "v=spf1 include:example.com ~all", TXT, "v=spf1 include:example.com ~all", false}, + {"TXT: special chars", "v=DKIM1; k=rsa; p=MIGf", TXT, "v=DKIM1; k=rsa; p=MIGf", false}, + {"TXT: unicode", "héllo wörld", TXT, "héllo wörld", false}, + {"TXT: multiple spaces", "key=value with spaces", TXT, "key=value with spaces", false}, + {"TXT: quoted", "\"quoted value\"", TXT, "\"quoted value\"", false}, + {"TXT: blank", " ", TXT, null, true}, + {"TXT: newline", "\n", TXT, null, true}, + }); + } + + @Test + public void testNormalizeDnsRecordValue() { + if (expectException) { + try { + DnsProviderUtil.normalizeDnsRecordValue(input, recordType); + fail("Expected IllegalArgumentException for [" + description + "] input='" + input + "'"); + } catch (IllegalArgumentException ignored) {} + } else { + String result = DnsProviderUtil.normalizeDnsRecordValue(input, recordType); + assertEquals(description, expected, result); + } + } +} diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java new file mode 100644 index 000000000000..5b55368770b7 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java @@ -0,0 +1,432 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.powerdns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.apache.cloudstack.dns.exception.DnsOperationException; +import org.apache.cloudstack.dns.exception.DnsAuthenticationException; +import org.apache.cloudstack.dns.exception.DnsConflictException; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.test.util.ReflectionTestUtils; + +import com.fasterxml.jackson.databind.JsonNode; + +@RunWith(MockitoJUnitRunner.class) +public class PowerDnsClientTest { + + PowerDnsClient client; + CloseableHttpClient httpClientMock; + + @Before + public void setUp() { + client = new PowerDnsClient(); + httpClientMock = mock(CloseableHttpClient.class); + ReflectionTestUtils.setField(client, "httpClient", httpClientMock); + } + + private CloseableHttpResponse createResponse(int statusCode, String jsonBody) { + CloseableHttpResponse responseMock = mock(CloseableHttpResponse.class); + StatusLine statusLineMock = mock(StatusLine.class); + when(responseMock.getStatusLine()).thenReturn(statusLineMock); + when(statusLineMock.getStatusCode()).thenReturn(statusCode); + + if (jsonBody != null) { + when(responseMock.getEntity()).thenReturn(new StringEntity(jsonBody, StandardCharsets.UTF_8)); + } + + return responseMock; + } + + private void mockHttpResponse(int statusCode, String jsonBody) throws IOException { + CloseableHttpResponse response = createResponse(statusCode, jsonBody); + when(httpClientMock.execute(any(HttpUriRequest.class))).thenReturn(response); + } + + @Test + public void testNormalizeApexRecord() { + String result = client.normalizeRecordName("@", "example.com"); + assertEquals("example.com.", result); + + result = client.normalizeRecordName("", "example.com"); + assertEquals("example.com.", result); + } + + @Test + public void testNormalizeRelativeRecord() { + String result = client.normalizeRecordName("www", "example.com"); + assertEquals("www.example.com.", result); + + result = client.normalizeRecordName("WWW", "example.com"); // test case-insensitive + assertEquals("www.example.com.", result); + } + + @Test + public void testNormalizeAbsoluteRecordWithinZone() { + String result = client.normalizeRecordName("www.example.com.", "example.com"); + assertEquals("www.example.com.", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeAbsoluteRecordOutsideZoneThrows() { + client.normalizeRecordName("other.com.", "example.com"); + } + + @Test + public void testNormalizeDottedNameWithoutTrailingDot() { + String result = client.normalizeRecordName("api.test.com", "example.com"); + assertEquals("api.test.com.", result); + } + + @Test + public void testNormalizeRelativeSubdomain() { + String result = client.normalizeRecordName("mail", "example.com"); + assertEquals("mail.example.com.", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeNullRecordNameThrows() { + client.normalizeRecordName(null, "example.com"); + } + + @Test + public void testNormalizeZoneNormalization() { + String result = client.normalizeRecordName("www", "Example.Com"); + assertEquals("www.example.com.", result); + + result = client.normalizeRecordName("www", "example.com."); + assertEquals("www.example.com.", result); + } + + @Test + public void testDiscoverAuthoritativeServerIdSuccess() throws Exception { + mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]"); + String result = client.discoverAuthoritativeServerId("http://pdns:8081", null, "apikey"); + assertEquals("localhost", result); + } + + @Test + public void testDiscoverAuthoritativeServerIdFallback() throws Exception { + mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}, {\"id\":\"server2\", \"daemon_type\":\"authoritative\"}]"); + String result = client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + assertEquals("server2", result); + } + + @Test(expected = DnsOperationException.class) + public void testDiscoverAuthoritativeServerIdEmpty() throws Exception { + mockHttpResponse(200, "[]"); + client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + } + + @Test(expected = DnsOperationException.class) + public void testDiscoverAuthoritativeServerIdNoAuthoritative() throws Exception { + mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}]"); + client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + } + + @Test + public void testValidateServerIdSuccess() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + String result = client.validateServerId("http://pdns", 8081, "apikey", "abc"); + assertEquals("abc", result); + } + + @Test(expected = DnsOperationException.class) + public void testValidateServerIdNotAuthoritative() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"recursor\"}"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test + public void testResolveServerIdWithExternalId() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + String result = client.resolveServerId("http://pdns", 8081, "apikey", "abc"); + assertEquals("abc", result); + } + + @Test + public void testResolveServerIdWithoutExternalId() throws Exception { + mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]"); + String result = client.resolveServerId("http://pdns", 8081, "apikey", null); + assertEquals("localhost", result); + } + + @Test + public void testCreateZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("POST")) { + return createResponse(201, "{\"id\":\"example.com.\"}"); + } + return createResponse(500, null); + } + }); + + String result = client.createZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", false, Arrays.asList("ns1.com")); + assertEquals("example.com.", result); + } + + @Test + public void testUpdateZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PUT")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + client.updateZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", true, Arrays.asList("ns1.com")); + // No exception means success + } + + @Test + public void testDeleteZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("DELETE")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + client.deleteZone("http://pdns", 8081, "apikey", "abc", "example.com"); + } + + @Test + public void testModifyRecord() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PATCH")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE"); + assertEquals("www.example.com", result); + } + + @Test + public void testModifyRecordApex() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PATCH")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "@", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE"); + assertEquals("example.com", result); + } + + @Test + public void testListRecords() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + // validateServerId uses /servers/abc + // listRecords uses /servers/abc/zones/example.com. + if (request.getURI().getPath().endsWith("abc")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getURI().getPath().endsWith("example.com.")) { + return createResponse(200, "{\"rrsets\":[{\"name\":\"www.example.com.\",\"type\":\"A\"}]}"); + } + return createResponse(500, null); + } + }); + + Iterable records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com"); + assertNotNull(records); + assertTrue(records.iterator().hasNext()); + assertEquals("www.example.com.", records.iterator().next().path("name").asText()); + } + + @Test + public void testListRecordsEmpty() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getURI().getPath().endsWith("abc")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getURI().getPath().endsWith("example.com.")) { + return createResponse(200, "{}"); + } + return createResponse(500, null); + } + }); + + Iterable records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com"); + assertNotNull(records); + assertTrue(!records.iterator().hasNext()); + } + + @Test(expected = DnsNotFoundException.class) + public void testExecuteThrowsNotFound() throws Exception { + mockHttpResponse(404, "Not Found"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsAuthenticationException.class) + public void testExecuteThrowsAuthError() throws Exception { + mockHttpResponse(401, "Unauthorized"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsConflictException.class) + public void testExecuteThrowsConflictError() throws Exception { + mockHttpResponse(409, "Conflict"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsOperationException.class) + public void testExecuteThrowsUnexpectedStatus() throws Exception { + mockHttpResponse(500, "Server Error"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + // Route helper: GET /servers/abc → validate; GET /zones/... → zone response + private void mockRecordExists(String zoneJson) throws IOException { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + String path = request.getURI().getPath(); + if (path.endsWith("/abc")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + // zone query (contains /zones/) + if (zoneJson == null) { + return createResponse(200, null); // empty body → execute() returns null + } + return createResponse(200, zoneJson); + } + }); + } + + @Test + public void testRecordExistsZoneNodeNull() throws Exception { + // execute() returns null → zoneNode == null → false + mockRecordExists(null); + boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A"); + assertEquals(false, result); + } + + @Test + public void testRecordExistsMissingRrSetsField() throws Exception { + // response has no "rrsets" key → !zoneNode.has(RR_SETS) → false + mockRecordExists("{}"); + boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A"); + assertEquals(false, result); + } + + @Test + public void testRecordExistsRrSetsNotArray() throws Exception { + // rrsets is a scalar string, not an ArrayNode → isArray() == false → false + mockRecordExists("{\"rrsets\":\"not-an-array\"}"); + boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A"); + assertEquals(false, result); + } + + @Test + public void testRecordExistsEmptyRrSetsArray() throws Exception { + // rrsets is an empty array → isArray() == true && isEmpty() == true → false + mockRecordExists("{\"rrsets\":[]}"); + boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A"); + assertEquals(false, result); + } + + @Test + public void testRecordExistsNonEmptyRrSetsArray() throws Exception { + // rrsets is a non-empty array → isArray() == true && !isEmpty() → true + mockRecordExists("{\"rrsets\":[{\"name\":\"www.example.com.\",\"type\":\"A\"}]}"); + boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A"); + assertEquals(true, result); + } + + @Test + public void testCloseSucceeds() throws Exception { + // httpClient.close() completes normally → no exception propagated + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + ReflectionTestUtils.setField(client, "httpClient", mockClient); + client.close(); + org.mockito.Mockito.verify(mockClient).close(); + } + + @Test + public void testCloseSwallowsIOException() throws Exception { + // httpClient.close() throws IOException → caught and logged (warn), no rethrow + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + org.mockito.Mockito.doThrow(new IOException("connection reset")).when(mockClient).close(); + ReflectionTestUtils.setField(client, "httpClient", mockClient); + client.close(); // must NOT throw + } +} diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java new file mode 100644 index 000000000000..eae1c9867140 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java @@ -0,0 +1,391 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package org.apache.cloudstack.dns.powerdns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; + + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsRecord.RecordType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +@RunWith(MockitoJUnitRunner.class) +public class PowerDnsProviderTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PowerDnsProvider provider; + private PowerDnsClient clientMock; + private DnsServer serverMock; + private DnsZone zoneMock; + + @Before + public void setUp() { + provider = new PowerDnsProvider(); + clientMock = mock(PowerDnsClient.class); + serverMock = mock(DnsServer.class); + zoneMock = mock(DnsZone.class); + ReflectionTestUtils.setField(provider, "client", clientMock); + + when(serverMock.getUrl()).thenReturn("http://pdns:8081"); + when(serverMock.getDnsApiKey()).thenReturn("secret"); + when(serverMock.getPort()).thenReturn(8081); + when(serverMock.getDetail(PowerDnsProvider.PDNS_SERVER_ID)).thenReturn("localhost"); + when(serverMock.getNameServers()).thenReturn(Arrays.asList("ns1.example.com")); + + when(zoneMock.getName()).thenReturn("example.com"); + } + + @Test + public void testGetProviderType() { + assertEquals(DnsProviderType.PowerDNS, provider.getProviderType()); + } + + @Test + public void testConfigureCreatesClientWhenNull() { + PowerDnsProvider freshProvider = new PowerDnsProvider(); + boolean result = freshProvider.configure("test", new HashMap<>()); + assertTrue(result); + assertNotNull(ReflectionTestUtils.getField(freshProvider, "client")); + } + + @Test + public void testConfigureDoesNotReplaceExistingClient() { + PowerDnsClient existingClient = mock(PowerDnsClient.class); + ReflectionTestUtils.setField(provider, "client", existingClient); + + boolean result = provider.configure("test", new HashMap<>()); + + assertTrue(result); + assertEquals(existingClient, ReflectionTestUtils.getField(provider, "client")); + } + + @Test + public void testStopClosesClient() { + boolean result = provider.stop(); + assertTrue(result); + verify(clientMock, times(1)).close(); + } + + @Test + public void testStopWithNullClientSucceeds() { + ReflectionTestUtils.setField(provider, "client", null); + boolean result = provider.stop(); + assertTrue(result); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsNullUrl() { + when(serverMock.getUrl()).thenReturn(null); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsBlankUrl() { + when(serverMock.getUrl()).thenReturn(" "); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsNullApiKey() { + when(serverMock.getDnsApiKey()).thenReturn(null); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsBlankApiKey() { + when(serverMock.getDnsApiKey()).thenReturn(""); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerAndZoneFieldsBlankZoneName() { + when(zoneMock.getName()).thenReturn(" "); + provider.validateRequiredServerAndZoneFields(serverMock, zoneMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerAndZoneFieldsNullZoneName() { + when(zoneMock.getName()).thenReturn(null); + provider.validateRequiredServerAndZoneFields(serverMock, zoneMock); + } + + @Test + public void testValidateDelegatesToClient() throws DnsProviderException { + when(clientMock.validateServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost"); + provider.validate(serverMock); + verify(clientMock).validateServerId("http://pdns:8081", 8081, "secret", "localhost"); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateThrowsWhenServerUrlBlank() throws DnsProviderException { + when(serverMock.getUrl()).thenReturn(""); + provider.validate(serverMock); + } + + @Test + public void testValidateAndResolveServer() throws Exception { + when(clientMock.resolveServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost"); + String result = provider.validateAndResolveServer(serverMock); + assertEquals("localhost", result); + verify(clientMock).resolveServerId("http://pdns:8081", 8081, "secret", "localhost"); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateAndResolveServerThrowsWhenUrlBlank() throws Exception { + when(serverMock.getUrl()).thenReturn(null); + provider.validateAndResolveServer(serverMock); + } + + @Test + public void testProvisionZoneDelegatesToClient() throws DnsProviderException { + when(clientMock.createZone(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(), eq(false), anyList())).thenReturn("example.com."); + String zoneId = provider.provisionZone(serverMock, zoneMock); + assertEquals("example.com.", zoneId); + verify(clientMock).createZone("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "Native", false, Arrays.asList("ns1.example.com")); + } + + @Test(expected = IllegalArgumentException.class) + public void testProvisionZoneThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(null); + provider.provisionZone(serverMock, zoneMock); + } + + @Test + public void testDeleteZoneDelegatesToClient() throws DnsProviderException { + provider.deleteZone(serverMock, zoneMock); + verify(clientMock).deleteZone("http://pdns:8081", 8081, "secret", "localhost", "example.com"); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeleteZoneThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(""); + provider.deleteZone(serverMock, zoneMock); + } + + @Test + public void testUpdateZoneDelegatesToClient() throws DnsProviderException { + provider.updateZone(serverMock, zoneMock); + verify(clientMock).updateZone("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "Native", false, Arrays.asList("ns1.example.com")); + } + + @Test + public void testAddRecordDelegatesToClient() throws DnsProviderException { + DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("www.example.com"); + + String result = provider.addRecord(serverMock, zoneMock, record); + + assertEquals("www.example.com", result); + verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "www", "A", 300L, Arrays.asList("1.2.3.4"), "REPLACE"); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddRecordThrowsWhenServerUrlBlank() throws DnsProviderException { + when(serverMock.getUrl()).thenReturn(""); + DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300); + provider.addRecord(serverMock, zoneMock, record); + } + + @Test + public void testUpdateRecordDelegatesToAddRecord() throws DnsProviderException { + DnsRecord record = new DnsRecord("mail", RecordType.MX, Arrays.asList("10 mail.example.com"), 300); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("mail.example.com"); + + String result = provider.updateRecord(serverMock, zoneMock, record); + + assertEquals("mail.example.com", result); + verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + eq("mail"), eq("MX"), eq(300L), eq(Arrays.asList("10 mail.example.com")), eq("REPLACE")); + } + + @Test + public void testDeleteRecordDelegatesToClientWithDeleteChangeType() throws DnsProviderException { + DnsRecord record = new DnsRecord("old", RecordType.CNAME, Arrays.asList("target.com"), 600); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("old.example.com"); + + String result = provider.deleteRecord(serverMock, zoneMock, record); + + assertEquals("old.example.com", result); + verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + eq("old"), eq("CNAME"), eq(600L), eq(Arrays.asList("target.com")), eq("DELETE")); + } + + @Test + public void testApplyRecordPassesChangeTypeToClient() throws DnsProviderException { + DnsRecord record = new DnsRecord("txt", RecordType.TXT, Arrays.asList("v=spf1 include:example.com ~all"), 3600); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("txt.example.com"); + + provider.applyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + record, PowerDnsProvider.ChangeType.REPLACE); + + verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "txt", "TXT", 3600L, Arrays.asList("v=spf1 include:example.com ~all"), "REPLACE"); + } + + @Test + public void testListRecordsParsesRrsets() throws DnsProviderException { + ObjectNode aRecord = MAPPER.createObjectNode(); + aRecord.put("name", "www.example.com."); + aRecord.put("type", "A"); + aRecord.put("ttl", 300); + ArrayNode records = aRecord.putArray("records"); + records.addObject().put("content", "1.2.3.4"); + + ObjectNode mxRecord = MAPPER.createObjectNode(); + mxRecord.put("name", "example.com."); + mxRecord.put("type", "MX"); + mxRecord.put("ttl", 600); + ArrayNode mxRecords = mxRecord.putArray("records"); + mxRecords.addObject().put("content", "10 mail.example.com"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Arrays.asList(aRecord, mxRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + + assertEquals(2, result.size()); + + DnsRecord first = result.get(0); + assertEquals("www.example.com.", first.getName()); + assertEquals(RecordType.A, first.getType()); + assertEquals(300, first.getTtl()); + assertEquals(Arrays.asList("1.2.3.4"), first.getContents()); + + DnsRecord second = result.get(1); + assertEquals("example.com.", second.getName()); + assertEquals(RecordType.MX, second.getType()); + assertEquals(600, second.getTtl()); + assertEquals(Arrays.asList("10 mail.example.com"), second.getContents()); + } + + @Test + public void testListRecordsSkipsSoaRecords() throws DnsProviderException { + ObjectNode soaRecord = MAPPER.createObjectNode(); + soaRecord.put("name", "example.com."); + soaRecord.put("type", "SOA"); + soaRecord.put("ttl", 3600); + soaRecord.putArray("records").addObject().put("content", "ns1.example.com. admin.example.com. ..."); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(soaRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertTrue(result.isEmpty()); + } + + @Test + public void testListRecordsSkipsUnknownRecordTypes() throws DnsProviderException { + ObjectNode unknownRecord = MAPPER.createObjectNode(); + unknownRecord.put("name", "test.example.com."); + unknownRecord.put("type", "UNKNOWNTYPE"); + unknownRecord.put("ttl", 300); + unknownRecord.putArray("records").addObject().put("content", "some-data"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(unknownRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertTrue(result.isEmpty()); + } + + @Test + public void testListRecordsIgnoresEmptyContentEntries() throws DnsProviderException { + ObjectNode aRecord = MAPPER.createObjectNode(); + aRecord.put("name", "host.example.com."); + aRecord.put("type", "A"); + aRecord.put("ttl", 300); + ArrayNode records = aRecord.putArray("records"); + records.addObject().put("content", ""); + records.addObject().put("content", "5.6.7.8"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(aRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertEquals(1, result.size()); + assertEquals(Collections.singletonList("5.6.7.8"), result.get(0).getContents()); + } + + @Test + public void testListRecordsReturnsEmptyListWhenClientReturnsEmpty() throws DnsProviderException { + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.emptyList()); + + List result = provider.listRecords(serverMock, zoneMock); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test(expected = DnsProviderException.class) + public void testListRecordsPropagatesClientException() throws DnsProviderException { + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenThrow(mock(DnsProviderException.class)); + + provider.listRecords(serverMock, zoneMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testListRecordsThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(""); + provider.listRecords(serverMock, zoneMock); + } + + @Test + public void testChangeTypeValues() { + assertEquals("REPLACE", PowerDnsProvider.ChangeType.REPLACE.name()); + assertEquals("DELETE", PowerDnsProvider.ChangeType.DELETE.name()); + } +} diff --git a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java index 224e49f91a32..b98147b5f031 100644 --- a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java +++ b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java @@ -493,7 +493,7 @@ public boolean start() { @Override public synchronized boolean stop() { - if (s_connection.isOpen()) { + if (s_connection != null && s_connection.isOpen()) { for (String subscriberId : s_subscribers.keySet()) { Ternary subscriberDetails = s_subscribers.get(subscriberId); Channel channel = subscriberDetails.second(); diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java index ac840c00be32..3f2d85458d30 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java @@ -48,6 +48,8 @@ import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import javax.net.ssl.SSLContext; + import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustAllStrategy; import org.apache.http.entity.ContentType; @@ -97,7 +99,9 @@ protected boolean isValidJson(String json) { protected void setHttpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { if (webhook.isSslVerification()) { - httpClient = HttpClients.createDefault(); + httpClient = HttpClients.custom() + .setSSLContext(SSLContext.getDefault()) + .build(); return; } httpClient = HttpClients diff --git a/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java b/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java deleted file mode 100644 index 42129944a194..000000000000 --- a/plugins/host-allocators/random/src/main/java/com/cloud/agent/manager/allocator/impl/RandomAllocator.java +++ /dev/null @@ -1,196 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. -package com.cloud.agent.manager.allocator.impl; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; - -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.ListUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - -import com.cloud.agent.manager.allocator.HostAllocator; -import com.cloud.capacity.CapacityManager; -import com.cloud.dc.ClusterDetailsDao; -import com.cloud.dc.dao.ClusterDao; -import com.cloud.deploy.DeploymentPlan; -import com.cloud.deploy.DeploymentPlanner.ExcludeList; -import com.cloud.host.Host; -import com.cloud.host.Host.Type; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; -import com.cloud.offering.ServiceOffering; -import com.cloud.resource.ResourceManager; -import com.cloud.storage.VMTemplateVO; -import com.cloud.utils.Pair; -import com.cloud.utils.component.AdapterBase; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachineProfile; - -@Component -public class RandomAllocator extends AdapterBase implements HostAllocator { - @Inject - private HostDao _hostDao; - @Inject - private ResourceManager _resourceMgr; - @Inject - private ClusterDao clusterDao; - @Inject - private ClusterDetailsDao clusterDetailsDao; - @Inject - private CapacityManager capacityManager; - - protected List listHostsByTags(Host.Type type, long dcId, Long podId, Long clusterId, String offeringHostTag, String templateTag) { - List taggedHosts = new ArrayList<>(); - if (offeringHostTag != null) { - taggedHosts.addAll(_hostDao.listByHostTag(type, clusterId, podId, dcId, offeringHostTag)); - } - if (templateTag != null) { - List templateTaggedHosts = _hostDao.listByHostTag(type, clusterId, podId, dcId, templateTag); - if (taggedHosts.isEmpty()) { - taggedHosts = templateTaggedHosts; - } else { - taggedHosts.retainAll(templateTaggedHosts); - } - } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Found %d hosts %s with type: %s, zone ID: %d, pod ID: %d, cluster ID: %s, offering host tag(s): %s, template tag: %s", - taggedHosts.size(), - (taggedHosts.isEmpty() ? "" : String.format("(%s)", StringUtils.join(taggedHosts.stream().map(HostVO::toString).toArray(), ","))), - type.name(), dcId, podId, clusterId, offeringHostTag, templateTag)); - } - return taggedHosts; - } - private List findSuitableHosts(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, - ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - long dcId = plan.getDataCenterId(); - Long podId = plan.getPodId(); - Long clusterId = plan.getClusterId(); - ServiceOffering offering = vmProfile.getServiceOffering(); - List hostsCopy = null; - List suitableHosts = new ArrayList<>(); - - if (type == Host.Type.Storage) { - return suitableHosts; - } - String offeringHostTag = offering.getHostTag(); - - VMTemplateVO template = (VMTemplateVO)vmProfile.getTemplate(); - String templateTag = template.getTemplateTag(); - String hostTag = null; - if (ObjectUtils.anyNotNull(offeringHostTag, templateTag)) { - hostTag = ObjectUtils.allNotNull(offeringHostTag, templateTag) ? - String.format("%s, %s", offeringHostTag, templateTag) : - ObjectUtils.firstNonNull(offeringHostTag, templateTag); - logger.debug("Looking for hosts in dc [{}], pod [{}], cluster [{}] and complying with host tag(s): [{}]", dcId, podId, clusterId, hostTag); - } else { - logger.debug("Looking for hosts in dc: {} pod: {} cluster: {}", dcId , podId, clusterId); - } - if (hosts != null) { - // retain all computing hosts, regardless of whether they support routing...it's random after all - hostsCopy = new ArrayList<>(hosts); - if (ObjectUtils.anyNotNull(offeringHostTag, templateTag)) { - hostsCopy.retainAll(listHostsByTags(type, dcId, podId, clusterId, offeringHostTag, templateTag)); - } else { - hostsCopy.retainAll(_hostDao.listAllHostsThatHaveNoRuleTag(type, clusterId, podId, dcId)); - } - } else { - // list all computing hosts, regardless of whether they support routing...it's random after all - if (offeringHostTag != null) { - hostsCopy = listHostsByTags(type, dcId, podId, clusterId, offeringHostTag, templateTag); - } else { - hostsCopy = _hostDao.listAllHostsThatHaveNoRuleTag(type, clusterId, podId, dcId); - } - } - hostsCopy = ListUtils.union(hostsCopy, _hostDao.findHostsWithTagRuleThatMatchComputeOferringTags(offeringHostTag)); - - if (hostsCopy.isEmpty()) { - logger.info("No suitable host found for VM [{}] in {}.", vmProfile, hostTag); - return null; - } - - logger.debug("Random Allocator found {} hosts", hostsCopy.size()); - if (hostsCopy.isEmpty()) { - return suitableHosts; - } - - Collections.shuffle(hostsCopy); - for (Host host : hostsCopy) { - if (suitableHosts.size() == returnUpTo) { - break; - } - if (avoid.shouldAvoid(host)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Host %s is in avoid set, skipping this and trying other available hosts", host)); - } - continue; - } - Pair cpuCapabilityAndCapacity = capacityManager.checkIfHostHasCpuCapabilityAndCapacity(host, offering, considerReservedCapacity); - if (!cpuCapabilityAndCapacity.first() || !cpuCapabilityAndCapacity.second()) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Not using host %s; host has cpu capability? %s, host has capacity? %s", host, cpuCapabilityAndCapacity.first(), cpuCapabilityAndCapacity.second())); - } - continue; - } - if (logger.isDebugEnabled()) { - logger.debug(String.format("Found a suitable host, adding to list: %s", host)); - } - suitableHosts.add(host); - } - if (logger.isDebugEnabled()) { - logger.debug("Random Host Allocator returning " + suitableHosts.size() + " suitable hosts"); - } - return suitableHosts; - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, ExcludeList avoid, int returnUpTo) { - return allocateTo(vmProfile, plan, type, avoid, returnUpTo, true); - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, Type type, - ExcludeList avoid, List hosts, int returnUpTo, - boolean considerReservedCapacity) { - if (CollectionUtils.isEmpty(hosts)) { - if (logger.isDebugEnabled()) { - logger.debug("Random Allocator found 0 hosts as given host list is empty"); - } - return new ArrayList<>(); - } - return findSuitableHosts(vmProfile, plan, type, avoid, hosts, returnUpTo, considerReservedCapacity); - } - - @Override - public List allocateTo(VirtualMachineProfile vmProfile, DeploymentPlan plan, - Type type, ExcludeList avoid, int returnUpTo, boolean considerReservedCapacity) { - return findSuitableHosts(vmProfile, plan, type, avoid, null, returnUpTo, considerReservedCapacity); - } - - @Override - public boolean isVirtualMachineUpgradable(VirtualMachine vm, ServiceOffering offering) { - // currently we do no special checks to rule out a VM being upgradable to an offering, so - // return true - return true; - } -} diff --git a/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java b/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java deleted file mode 100644 index 538d7157184a..000000000000 --- a/plugins/host-allocators/random/src/test/java/com/cloud/agent/manager/allocator/impl/RandomAllocatorTest.java +++ /dev/null @@ -1,80 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. -package com.cloud.agent.manager.allocator.impl; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.collections.CollectionUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import com.cloud.host.Host; -import com.cloud.host.HostVO; -import com.cloud.host.dao.HostDao; - -@RunWith(MockitoJUnitRunner.class) -public class RandomAllocatorTest { - - @Mock - HostDao hostDao; - @InjectMocks - RandomAllocator randomAllocator; - - @Test - public void testListHostsByTags() { - Host.Type type = Host.Type.Routing; - Long id = 1L; - String templateTag = "tag1"; - String offeringTag = "tag2"; - HostVO host1 = Mockito.mock(HostVO.class); - HostVO host2 = Mockito.mock(HostVO.class); - Mockito.when(hostDao.listByHostTag(type, id, id, id, offeringTag)).thenReturn(List.of(host1, host2)); - - // No template tagged host - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(new ArrayList<>()); - List result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertTrue(CollectionUtils.isEmpty(result)); - - // Different template tagged host - HostVO host3 = Mockito.mock(HostVO.class); - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(List.of(host3)); - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertTrue(CollectionUtils.isEmpty(result)); - - // Matching template tagged host - Mockito.when(hostDao.listByHostTag(type, id, id, id, templateTag)).thenReturn(List.of(host1)); - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, templateTag); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(1, result.size()); - - // No template tag - result = randomAllocator.listHostsByTags(type, id, id, id, offeringTag, null); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(2, result.size()); - - // No offering tag - result = randomAllocator.listHostsByTags(type, id, id, id, null, templateTag); - Assert.assertFalse(CollectionUtils.isEmpty(result)); - Assert.assertEquals(1, result.size()); - } -} diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java index 940897de3c95..c6c38a398098 100644 --- a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java +++ b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java @@ -106,7 +106,6 @@ public VMTemplateVO create(TemplateProfile profile) { } } - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); return template; } diff --git a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java index 5695325fb137..c05d52326cea 100644 --- a/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java +++ b/plugins/hypervisors/baremetal/src/main/java/com/cloud/baremetal/manager/BaremetalVlanManagerImpl.java @@ -263,9 +263,8 @@ public boolean start() { user.setSource(User.Source.UNKNOWN); user = userDao.persist(user); - String[] keys = acntMgr.createApiKeyAndSecretKey(user.getId()); - user.setApiKey(keys[0]); - user.setSecretKey(keys[1]); + acntMgr.createApiKeyAndSecretKey(user.getId()); + userDao.update(user.getId(), user); return true; } diff --git a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalDhcpCmd.java b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalDhcpCmd.java index 89467358349d..192b646b150f 100644 --- a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalDhcpCmd.java +++ b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalDhcpCmd.java @@ -70,7 +70,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Adding an external DHCP server"; + return "Adding an external DHCP server to physical network with ID: " + getResourceUuid(ApiConstants.PHYSICAL_NETWORK_ID); } @Override diff --git a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalPxeCmd.java b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalPxeCmd.java index bf97947ecccc..a2c6060a92ff 100644 --- a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalPxeCmd.java +++ b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/AddBaremetalPxeCmd.java @@ -72,7 +72,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "Adding an external pxe server"; + return "Adding an external PXE server to physical network with ID: " + getResourceUuid(ApiConstants.PHYSICAL_NETWORK_ID); } @Override diff --git a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/BaremetalProvisionDoneNotificationCmd.java b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/BaremetalProvisionDoneNotificationCmd.java index 73752134abc1..a9b166528302 100644 --- a/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/BaremetalProvisionDoneNotificationCmd.java +++ b/plugins/hypervisors/baremetal/src/main/java/org/apache/cloudstack/api/BaremetalProvisionDoneNotificationCmd.java @@ -50,7 +50,7 @@ public String getEventType() { @Override public String getEventDescription() { - return "notify management server that baremetal provision has been done on a host"; + return "Notifying management server that baremetal provision has been done on a host"; } @Override diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java index aefe9d0d1809..e56b9ce7b633 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java @@ -163,7 +163,7 @@ public List createTemplateForPostUpload(Templ } // Set Event Details for Template/ISO Upload String eventResourceId = template.getUuid(); - CallContext.current().setEventDetails(String.format("Template Id: %s", eventResourceId)); + CallContext.current().setEventDetails(String.format("Template ID: %s", eventResourceId)); CallContext.current().putContextParameter(VirtualMachineTemplate.class, eventResourceId); Long zoneId = zoneIdList.get(0); DataStore imageStore = verifyHeuristicRulesForZone(template, zoneId); diff --git a/plugins/hypervisors/hyperv/DotNet/ServerResource/AgentShell/App.config b/plugins/hypervisors/hyperv/DotNet/ServerResource/AgentShell/App.config index b66258ad9f6f..e96b6d8b8c5e 100644 --- a/plugins/hypervisors/hyperv/DotNet/ServerResource/AgentShell/App.config +++ b/plugins/hypervisors/hyperv/DotNet/ServerResource/AgentShell/App.config @@ -35,7 +35,7 @@ - + diff --git a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/App.config b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/App.config index 3c4f70660827..c5025fb267c5 100644 --- a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/App.config +++ b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/App.config @@ -1,5 +1,5 @@ - diff --git a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs index 7e31ced3e389..84c67d7a9650 100644 --- a/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs +++ b/plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs @@ -1159,7 +1159,11 @@ public JContainer StartCommand([FromBody]dynamic cmd) try { string systemVmIsoPath = null; - String uriStr = (String)cmd.secondaryStorage; + String uriStr; + foreach (var item in cmd.secondaryStorages) + { + uriStr = item; + } if (!String.IsNullOrEmpty(uriStr)) { NFSTO share = new NFSTO(); diff --git a/plugins/hypervisors/hyperv/DotNet/ServerResource/ServerResource.Tests/App.config b/plugins/hypervisors/hyperv/DotNet/ServerResource/ServerResource.Tests/App.config index ddf1da0bcf67..184c44a5f279 100644 --- a/plugins/hypervisors/hyperv/DotNet/ServerResource/ServerResource.Tests/App.config +++ b/plugins/hypervisors/hyperv/DotNet/ServerResource/ServerResource.Tests/App.config @@ -1,5 +1,5 @@ - diff --git a/plugins/hypervisors/hyperv/src/main/java/com/cloud/ha/HypervInvestigator.java b/plugins/hypervisors/hyperv/src/main/java/com/cloud/ha/HypervInvestigator.java index 3d79b9efdd13..4e44d8cb7359 100644 --- a/plugins/hypervisors/hyperv/src/main/java/com/cloud/ha/HypervInvestigator.java +++ b/plugins/hypervisors/hyperv/src/main/java/com/cloud/ha/HypervInvestigator.java @@ -41,15 +41,15 @@ public class HypervInvestigator extends AdapterBase implements Investigator { @Override public boolean isVmAlive(com.cloud.vm.VirtualMachine vm, Host host) throws UnknownVM { - Status status = isAgentAlive(host); + Status status = getHostAgentStatus(host); if (status == null) { throw new UnknownVM(); } - return status == Status.Up ? true : null; + return status == Status.Up; } @Override - public Status isAgentAlive(Host agent) { + public Status getHostAgentStatus(Host agent) { if (agent.getHypervisorType() != Hypervisor.HypervisorType.Hyperv) { return null; } diff --git a/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java b/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java index 6ad06f426a79..12d6c42fd7e6 100644 --- a/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java +++ b/plugins/hypervisors/hyperv/src/main/java/com/cloud/hypervisor/hyperv/resource/HypervDirectConnectResource.java @@ -455,7 +455,7 @@ public final Answer executeRequest(final Command cmd) { if (s_hypervMgr != null) { final String secondary = s_hypervMgr.prepareSecondaryStorageStore(Long.parseLong(zoneId)); if (secondary != null) { - ((StartCommand)cmd).setSecondaryStorage(secondary); + ((StartCommand)cmd).setSecondaryStorages(List.of(secondary)); } } else { logger.error("Hyperv manager isn't available. Couldn't check and copy the System VM ISO."); diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index 255ada09ef4f..a00530268e83 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -62,6 +62,21 @@ rados ${cs.rados-java.version} + + com.github.jai-imageio + jai-imageio-core + 1.4.0 + + + com.dynatrace.hash4j + hash4j + 0.29.0 + + + com.mikesamuel + json-sanitizer + 1.2.3 + com.linbit.linstor.api java-linstor diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/ha/KVMInvestigator.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/ha/KVMInvestigator.java index ce9fbe6c232c..da9a0d6e2919 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/ha/KVMInvestigator.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/ha/KVMInvestigator.java @@ -19,10 +19,7 @@ package com.cloud.ha; import com.cloud.agent.AgentManager; -import com.cloud.agent.api.Answer; -import com.cloud.agent.api.CheckOnHostCommand; import com.cloud.host.Host; -import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; @@ -34,11 +31,12 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.ha.HAManager; +import org.apache.cloudstack.kvm.ha.KVMHostActivityChecker; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import javax.inject.Inject; -import java.util.Arrays; +import java.util.Collections; import java.util.List; public class KVMInvestigator extends AdapterBase implements Investigator { @@ -54,13 +52,15 @@ public class KVMInvestigator extends AdapterBase implements Investigator { private HAManager haManager; @Inject private DataStoreProviderManager dataStoreProviderMgr; + @Inject + private KVMHostActivityChecker hostActivityChecker; @Override public boolean isVmAlive(com.cloud.vm.VirtualMachine vm, Host host) throws UnknownVM { if (haManager.isHAEligible(host)) { return haManager.isVMAliveOnHost(host); } - Status status = isAgentAlive(host); + Status status = getHostAgentStatus(host); logger.debug("HA: HOST is ineligible legacy state {} for host {}", status, host); if (status == null) { throw new UnknownVM(); @@ -73,86 +73,41 @@ public boolean isVmAlive(com.cloud.vm.VirtualMachine vm, Host host) throws Unkno } @Override - public Status isAgentAlive(Host agent) { - if (agent.getHypervisorType() != Hypervisor.HypervisorType.KVM && agent.getHypervisorType() != Hypervisor.HypervisorType.LXC) { + public Status getHostAgentStatus(Host host) { + if (host.getHypervisorType() != Hypervisor.HypervisorType.KVM && host.getHypervisorType() != Hypervisor.HypervisorType.LXC) { return null; } - if (haManager.isHAEligible(agent)) { - return haManager.getHostStatus(agent); + if (haManager.isHAEligible(host)) { + return haManager.getHostStatusFromHAConfig(host); } - List clusterPools = _storagePoolDao.findPoolsInClusters(Arrays.asList(agent.getClusterId()), null); - boolean storageSupportHA = storageSupportHa(clusterPools); - if (!storageSupportHA) { - List zonePools = _storagePoolDao.findZoneWideStoragePoolsByHypervisor(agent.getDataCenterId(), agent.getHypervisorType()); - storageSupportHA = storageSupportHa(zonePools); + List clusterPools = _storagePoolDao.findPoolsInClusters(Collections.singletonList(host.getClusterId()), null); + boolean storageSupportsHA = storageSupportsHA(clusterPools); + if (!storageSupportsHA) { + List zonePools = _storagePoolDao.findZoneWideStoragePoolsByHypervisor(host.getDataCenterId(), host.getHypervisorType()); + storageSupportsHA = storageSupportsHA(zonePools); } - if (!storageSupportHA) { - logger.warn("Agent investigation was requested on host {}, but host does not support investigation because it has no NFS storage. Skipping investigation.", agent); + if (!storageSupportsHA) { + logger.warn("Agent investigation was requested on host {}, but host does not support investigation" + + " because it has no HA supported storage. Skipping investigation.", host); return null; } - Status hostStatus = null; - Status neighbourStatus = null; - boolean reportFailureIfOneStorageIsDown = HighAvailabilityManager.KvmHAFenceHostIfHeartbeatFailsOnStorage.value(); - CheckOnHostCommand cmd = new CheckOnHostCommand(agent, reportFailureIfOneStorageIsDown); - - try { - Answer answer = _agentMgr.easySend(agent.getId(), cmd); - if (answer != null) { - hostStatus = answer.getResult() ? Status.Down : Status.Up; - } - } catch (Exception e) { - logger.debug("Failed to send command to host: {}", agent); - } - if (hostStatus == null) { - hostStatus = Status.Disconnected; - } - - List neighbors = _resourceMgr.listHostsInClusterByStatus(agent.getClusterId(), Status.Up); - for (HostVO neighbor : neighbors) { - if (neighbor.getId() == agent.getId() - || (neighbor.getHypervisorType() != Hypervisor.HypervisorType.KVM && neighbor.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { - continue; - } - logger.debug("Investigating host:{} via neighbouring host:{}", agent, neighbor); - try { - Answer answer = _agentMgr.easySend(neighbor.getId(), cmd); - if (answer != null) { - neighbourStatus = answer.getResult() ? Status.Down : Status.Up; - logger.debug("Neighbouring host:{} returned status:{} for the investigated host:{}", neighbor, neighbourStatus, agent); - if (neighbourStatus == Status.Up) { - break; - } - } - } catch (Exception e) { - logger.debug("Failed to send command to host: {}", neighbor); - } - } - if (neighbourStatus == Status.Up && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { - hostStatus = Status.Disconnected; - } - if (neighbourStatus == Status.Down && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { - hostStatus = Status.Down; - } - logger.debug("HA: HOST is ineligible legacy state {} for host {}", hostStatus, agent); - return hostStatus; + return hostActivityChecker.getHostAgentStatus(host); } - private boolean storageSupportHa(List pools) { - boolean storageSupportHA = false; + private boolean storageSupportsHA(List pools) { for (StoragePoolVO pool : pools) { DataStoreProvider storeProvider = dataStoreProviderMgr.getDataStoreProvider(pool.getStorageProviderName()); DataStoreDriver storeDriver = storeProvider.getDataStoreDriver(); if (storeDriver instanceof PrimaryDataStoreDriver) { PrimaryDataStoreDriver primaryStoreDriver = (PrimaryDataStoreDriver)storeDriver; if (primaryStoreDriver.isStorageSupportHA(pool.getPoolType())) { - storageSupportHA = true; - break; + return true; } } } - return storageSupportHA; + return false; } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java index d360aa481372..ab4513642efa 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BlockCommitListener.java @@ -27,18 +27,14 @@ import org.libvirt.event.BlockJobStatus; import org.libvirt.event.BlockJobType; -import java.util.concurrent.Semaphore; - public class BlockCommitListener implements BlockJobListener { - private Semaphore semaphore; private String result; private String vmName; private Logger logger; private String logid; - protected BlockCommitListener(Semaphore semaphore, String vmName, String logid) { - this.semaphore = semaphore; + protected BlockCommitListener(String vmName, String logid) { this.vmName = vmName; this.logid = logid; logger = LogManager.getLogger(getClass()); @@ -54,24 +50,22 @@ public void onEvent(Domain domain, String diskPath, BlockJobType type, BlockJobS return; } + ThreadContext.put("logcontextid", logid); + logger.debug("Received status [{}] on disk [{}] while listening for block commit of VM [{}].", status, diskPath, vmName); switch (status) { case COMPLETED: result = null; - semaphore.release(); return; case READY: try { - ThreadContext.put("logcontextid", logid); logger.debug("Pivoting disk [{}] of VM [{}].", diskPath, vmName); domain.blockJobAbort(diskPath, Domain.BlockJobAbortFlags.PIVOT); } catch (LibvirtException ex) { result = String.format("Failed to pivot disk due to [%s].", ex.getMessage()); - semaphore.release(); } return; default: result = String.format("Failed to block commit disk with status [%s].", status); - semaphore.release(); } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java index da60f6fd7177..327ec46e0ecf 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/BridgeVifDriver.java @@ -48,6 +48,7 @@ public class BridgeVifDriver extends VifDriverBase { private final Object _vnetBridgeMonitor = new Object(); private String _modifyVlanPath; private String _modifyVxlanPath; + private String _macIpScriptPath; private String _controlCidr = NetUtils.getLinkLocalCIDR(); private Long libvirtVersion; @@ -76,9 +77,19 @@ public void configure(Map params) throws ConfigurationException if (_modifyVlanPath == null) { throw new ConfigurationException("Unable to find modifyvlan.sh"); } - _modifyVxlanPath = Script.findScript(networkScriptsDir, "modifyvxlan.sh"); + String vxlanMode = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.NETWORK_VXLAN_MODE); + String vxlanScript = "evpn".equalsIgnoreCase(vxlanMode) ? "modifyvxlan-evpn.sh" : "modifyvxlan.sh"; + _modifyVxlanPath = Script.findScript(networkScriptsDir, vxlanScript); if (_modifyVxlanPath == null) { - throw new ConfigurationException("Unable to find modifyvxlan.sh"); + throw new ConfigurationException("Unable to find " + vxlanScript); + } + + if (Boolean.TRUE.equals(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_NETWORK_MACIP_STATIC))) { + _macIpScriptPath = Script.findScript(networkScriptsDir, "modifymacip.sh"); + if (_macIpScriptPath == null) { + throw new ConfigurationException("Unable to find modifymacip.sh"); + } + logger.info("VM network MAC/IP static script configured: {}", _macIpScriptPath); } libvirtVersion = (Long) params.get("libvirtVersion"); @@ -275,12 +286,16 @@ public LibvirtVMDef.InterfaceDef plug(NicTO nic, String guestOsType, String nicA if (nic.getPxeDisable()) { intf.setPxeDisable(true); } + intf.setLinkStateUp(nic.isEnabled()); + + executeMacIpScript(intf.getBrName(), nic.getMac(), nic.getIp(), nic.getIp6Address(), nic.getNicSecIps()); return intf; } @Override public void unplug(LibvirtVMDef.InterfaceDef iface, boolean deleteBr) { + executeMacIpScript(iface.getBrName(), iface.getMacAddress()); deleteVnetBr(iface.getBrName(), deleteBr); } @@ -400,6 +415,60 @@ private void deleteVnetBr(String brName, boolean deleteBr) { } } + private void executeMacIpScript(String brName, String mac) { + if (_macIpScriptPath == null || mac == null || brName == null) { + return; + } + try { + final Script command = new Script(_macIpScriptPath, _timeout, logger); + command.add("-o", "delete"); + command.add("-b", brName); + command.add("-m", mac); + final String result = command.execute(); + if (result != null) { + logger.warn("MAC/IP script returned error for delete on {}: {}", mac, result); + } + } catch (Exception e) { + // Managing host neighbour/route entries is best-effort and must never break VM lifecycle operations + logger.warn("Failed to run MAC/IP script for delete on {} ({})", mac, brName, e); + } + } + + private void executeMacIpScript(String brName, String mac, String ipv4, String ipv6, List secondaryIps) { + if (_macIpScriptPath == null || mac == null || brName == null) { + return; + } + try { + final Script command = new Script(_macIpScriptPath, _timeout, logger); + command.add("-o", "add"); + command.add("-b", brName); + command.add("-m", mac); + if (ipv4 != null && !ipv4.isEmpty()) { + command.add("-4", ipv4); + } + command.add("-6", NetUtils.ipv6LinkLocal(mac).toString()); + if (ipv6 != null && !ipv6.isEmpty()) { + command.add("-6", ipv6); + } + if (secondaryIps != null) { + for (String secIp : secondaryIps) { + if (NetUtils.isValidIp6(secIp)) { + command.add("-6", secIp); + } else { + command.add("-4", secIp); + } + } + } + final String result = command.execute(); + if (result != null) { + logger.warn("MAC/IP script returned error for add on {}: {}", mac, result); + } + } catch (Exception e) { + // Managing host neighbour/route entries is best-effort and must never break VM lifecycle operations + logger.warn("Failed to run MAC/IP script for add on {} ({})", mac, brName, e); + } + } + private void deleteExistingLinkLocalRouteTable(String linkLocalBr) { Script command = new Script("/bin/bash", _timeout); command.add("-c"); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java new file mode 100644 index 000000000000..2e9852f7bc1e --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java @@ -0,0 +1,123 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * Communicates with the cloudstack-image-server control socket via socat. + * + * Protocol: newline-delimited JSON over a Unix domain socket. + * Actions: register, unregister, status. + */ +public class ImageServerControlSocket { + private static final Logger LOGGER = LogManager.getLogger(ImageServerControlSocket.class); + static final String CONTROL_SOCKET_PATH = "/var/run/cloudstack/image-server.sock"; + private static final Gson GSON = new GsonBuilder().create(); + + private ImageServerControlSocket() { + } + + /** + * Send a JSON message to the image server control socket and return the + * parsed response, or null on communication failure. + */ + static JsonObject sendMessage(Map message) { + String json = GSON.toJson(message); + Script script = new Script("/bin/bash", LOGGER); + script.add("-c"); + script.add(String.format("echo '%s' | socat -t5 - UNIX-CONNECT:%s", + json.replace("'", "'\\''"), CONTROL_SOCKET_PATH)); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = script.execute(parser); + if (result != null) { + LOGGER.error("Control socket communication failed: {}", result); + return null; + } + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + LOGGER.error("Empty response from control socket"); + return null; + } + try { + return JsonParser.parseString(output.trim()).getAsJsonObject(); + } catch (Exception e) { + LOGGER.error("Failed to parse control socket response: {}", output, e); + return null; + } + } + + /** + * Register a transfer config with the image server. + * @return true if the server accepted the registration. + */ + public static boolean registerTransfer(String transferId, Map config) { + Map msg = new HashMap<>(); + msg.put("action", "register"); + msg.put("transfer_id", transferId); + msg.put("config", config); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return false; + } + return "ok".equals(resp.has("status") ? resp.get("status").getAsString() : null); + } + + /** + * Unregister a transfer from the image server. + * @return the number of remaining active transfers, or -1 on error. + */ + public static int unregisterTransfer(String transferId) { + Map msg = new HashMap<>(); + msg.put("action", "unregister"); + msg.put("transfer_id", transferId); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return -1; + } + if (!"ok".equals(resp.has("status") ? resp.get("status").getAsString() : null)) { + return -1; + } + return resp.has("active_transfers") ? resp.get("active_transfers").getAsInt() : -1; + } + + /** + * Check whether the image server control socket is responsive. + * @return true if the server responded with status "ok". + */ + public static boolean isReady() { + Map msg = new HashMap<>(); + msg.put("action", "status"); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return false; + } + return "ok".equals(resp.has("status") ? resp.get("status").getAsString() : null); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHABase.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHABase.java index 896426addca1..f30e2c779195 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHABase.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHABase.java @@ -35,11 +35,9 @@ public class KVMHABase { protected Logger logger = LogManager.getLogger(getClass()); private long _timeout = 60000; /* 1 minutes */ - protected static String s_heartBeatPath; - protected long _heartBeatUpdateTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEARTBEAT_UPDATE_TIMEOUT); - protected long _heartBeatUpdateFreq = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); + protected long _heartBeatUpdateFreqInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); protected long _heartBeatUpdateMaxTries = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_MAX_TRIES); - protected long _heartBeatUpdateRetrySleep = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_RETRY_SLEEP); + protected long _heartBeatUpdateRetrySleepInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_RETRY_SLEEP); public static enum PoolType { PrimaryStorage, SecondaryStorage @@ -139,7 +137,7 @@ protected String checkingMountPoint(HAStoragePool pool, String poolName) { /* Can't find the mount point? */ /* we need to mount it under poolName */ if (poolName != null) { - Script mount = new Script("/bin/bash", 60000); + Script mount = new Script("/bin/bash", _timeout); mount.add("-c"); mount.add("mount " + mountSource + " " + destPath); String result = mount.execute(); @@ -155,7 +153,6 @@ protected String checkingMountPoint(HAStoragePool pool, String poolName) { } protected String getMountPoint(HAStoragePool storagePool) { - StoragePool pool = null; String poolName = null; try { @@ -172,7 +169,6 @@ protected String getMountPoint(HAStoragePool storagePool) { } poolName = pool.getName(); } - } catch (LibvirtException e) { logger.debug("Ignoring libvirt error.", e); } finally { @@ -235,7 +231,7 @@ protected String runScriptRetry(String cmdString, OutputInterpreter interpreter) return result; } - public Boolean checkingHeartBeat() { + public Boolean hasHeartBeat() { // TODO Auto-generated method stub return null; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAChecker.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAChecker.java index db6190fa8f28..0ee59c95da39 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAChecker.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAChecker.java @@ -26,44 +26,43 @@ public class KVMHAChecker extends KVMHABase implements Callable { private List storagePools; private HostTO host; - private boolean reportFailureIfOneStorageIsDown; + private boolean reportIfHeartBeatFailedForOneStoragePool; - public KVMHAChecker(List pools, HostTO host, boolean reportFailureIfOneStorageIsDown) { + public KVMHAChecker(List pools, HostTO host, boolean reportIfHeartBeatFailedForOneStoragePool) { this.storagePools = pools; this.host = host; - this.reportFailureIfOneStorageIsDown = reportFailureIfOneStorageIsDown; + this.reportIfHeartBeatFailedForOneStoragePool = reportIfHeartBeatFailedForOneStoragePool; } /* - * True means heartbeaing is on going, or we can't get it's status. False - * means heartbeating is stopped definitely + * True means heart beating is on going, or we can't get it's status. + * False means heart beating is stopped definitely. */ @Override - public Boolean checkingHeartBeat() { - boolean validResult = false; - - String hostAndPools = String.format("host IP [%s] in pools [%s]", host.getPrivateNetwork().getIp(), storagePools.stream().map(pool -> pool.getPoolUUID()).collect(Collectors.joining(", "))); - - logger.debug(String.format("Checking heart beat with KVMHAChecker for %s", hostAndPools)); + public Boolean hasHeartBeat() { + String hostAndPools = String.format("host IP [%s] in pools [%s]", host.getPrivateNetwork().getIp(), + storagePools.stream().map(pool -> pool.getPoolUUID()).collect(Collectors.joining(", "))); + logger.debug("Checking heart beat with KVMHAChecker for {}", hostAndPools); + boolean heartBeatCheckResult = false; for (HAStoragePool pool : storagePools) { - validResult = pool.getPool().checkingHeartBeat(pool, host); - if (reportFailureIfOneStorageIsDown && !validResult) { + heartBeatCheckResult = pool.getPool().hasHeartBeat(pool, host); + if (reportIfHeartBeatFailedForOneStoragePool && !heartBeatCheckResult) { break; } } - if (!validResult) { - logger.warn(String.format("All checks with KVMHAChecker for %s considered it as dead. It may cause a shutdown of the host.", hostAndPools)); + if (!heartBeatCheckResult) { + logger.warn("All checks with KVMHAChecker for {} considered it as dead. It may cause a shutdown of the host.", hostAndPools); } - return validResult; + return heartBeatCheckResult; } @Override public Boolean call() throws Exception { // logger.addAppender(new org.apache.log4j.ConsoleAppender(new // org.apache.log4j.PatternLayout(), "System.out")); - return checkingHeartBeat(); + return hasHeartBeat(); } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAMonitor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAMonitor.java index cf407bfc08a8..9f1b849e9727 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAMonitor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAMonitor.java @@ -18,7 +18,7 @@ import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; -import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.ha.HighAvailabilityManager; import com.cloud.utils.script.Script; import org.libvirt.Connect; import org.libvirt.LibvirtException; @@ -34,60 +34,51 @@ public class KVMHAMonitor extends KVMHABase implements Runnable { - private final Map storagePool = new ConcurrentHashMap<>(); + private final Map haStoragePools = new ConcurrentHashMap<>(); private final boolean rebootHostAndAlertManagementOnHeartbeatTimeout; private final String hostPrivateIp; - public KVMHAMonitor(HAStoragePool pool, String host, String scriptPath) { - if (pool != null) { - storagePool.put(pool.getPoolUUID(), pool); - } + public KVMHAMonitor(String host) { hostPrivateIp = host; - configureHeartBeatPath(scriptPath); - rebootHostAndAlertManagementOnHeartbeatTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.REBOOT_HOST_AND_ALERT_MANAGEMENT_ON_HEARTBEAT_TIMEOUT); } - private static synchronized void configureHeartBeatPath(String scriptPath) { - KVMHABase.s_heartBeatPath = scriptPath; - } - public void addStoragePool(HAStoragePool pool) { - synchronized (storagePool) { - storagePool.put(pool.getPoolUUID(), pool); + synchronized (haStoragePools) { + haStoragePools.put(pool.getPoolUUID(), pool); } } public void removeStoragePool(String uuid) { - synchronized (storagePool) { - HAStoragePool pool = storagePool.get(uuid); + synchronized (haStoragePools) { + HAStoragePool pool = haStoragePools.get(uuid); if (pool != null) { Script.runSimpleBashScript("umount " + pool.getMountDestPath()); - storagePool.remove(uuid); + haStoragePools.remove(uuid); } } } public List getStoragePools() { - synchronized (storagePool) { - return new ArrayList<>(storagePool.values()); + synchronized (haStoragePools) { + return new ArrayList<>(haStoragePools.values()); } } public HAStoragePool getStoragePool(String uuid) { - synchronized (storagePool) { - return storagePool.get(uuid); + synchronized (haStoragePools) { + return haStoragePools.get(uuid); } } protected void runHeartBeat() { - synchronized (storagePool) { + synchronized (haStoragePools) { Set removedPools = new HashSet<>(); - for (String uuid : storagePool.keySet()) { - HAStoragePool primaryStoragePool = storagePool.get(uuid); - if (primaryStoragePool.getPool().getType() == StoragePoolType.NetworkFilesystem) { - checkForNotExistingPools(removedPools, uuid); + for (String uuid : haStoragePools.keySet()) { + HAStoragePool primaryStoragePool = haStoragePools.get(uuid); + if (HighAvailabilityManager.LIBVIRT_STORAGE_POOL_TYPES_WITH_HA_SUPPORT.contains(primaryStoragePool.getPool().getType())) { + checkForNotExistingLibvirtStoragePools(removedPools, uuid); if (removedPools.contains(uuid)) { continue; } @@ -96,7 +87,7 @@ protected void runHeartBeat() { result = executePoolHeartBeatCommand(uuid, primaryStoragePool, result); if (result != null && rebootHostAndAlertManagementOnHeartbeatTimeout) { - logger.warn(String.format("Write heartbeat for pool [%s] failed: %s; stopping cloudstack-agent.", uuid, result)); + logger.warn("Write heartbeat for pool [{}] failed: {}; stopping cloudstack-agent.", uuid, result); primaryStoragePool.getPool().createHeartBeatCommand(primaryStoragePool, null, false);; } } @@ -109,45 +100,43 @@ protected void runHeartBeat() { } private String executePoolHeartBeatCommand(String uuid, HAStoragePool primaryStoragePool, String result) { - for (int i = 1; i <= _heartBeatUpdateMaxTries; i++) { + for (int attempt = 1; attempt <= _heartBeatUpdateMaxTries; attempt++) { result = primaryStoragePool.getPool().createHeartBeatCommand(primaryStoragePool, hostPrivateIp, true); - - if (result != null) { - logger.warn(String.format("Write heartbeat for pool [%s] failed: %s; try: %s of %s.", uuid, result, i, _heartBeatUpdateMaxTries)); - try { - Thread.sleep(_heartBeatUpdateRetrySleep); - } catch (InterruptedException e) { - logger.debug("[IGNORED] Interrupted between heartbeat retries.", e); - } - } else { + if (result == null) { break; } + logger.warn("Write heartbeat for pool [{}] failed: {}; try: {} of {}.", uuid, result, attempt, _heartBeatUpdateMaxTries); + try { + Thread.sleep(_heartBeatUpdateRetrySleepInMs); + } catch (InterruptedException e) { + logger.debug("[IGNORED] Interrupted between heartbeat retries.", e); + } } return result; } - private void checkForNotExistingPools(Set removedPools, String uuid) { + private void checkForNotExistingLibvirtStoragePools(Set removedPools, String uuid) { try { Connect conn = LibvirtConnection.getConnection(); StoragePool storage = conn.storagePoolLookupByUUIDString(uuid); if (storage == null || storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { if (storage == null) { - logger.debug(String.format("Libvirt storage pool [%s] not found, removing from HA list.", uuid)); + logger.debug("Libvirt storage pool [{}] not found, removing from HA list.", uuid); } else { - logger.debug(String.format("Libvirt storage pool [%s] found, but not running, removing from HA list.", uuid)); + logger.debug("Libvirt storage pool [{}] found, but not running, removing from HA list.", uuid); } removedPools.add(uuid); } - logger.debug(String.format("Found NFS storage pool [%s] in libvirt, continuing.", uuid)); + logger.debug("Found NFS storage pool [{}] in libvirt, continuing.", uuid); } catch (LibvirtException e) { - logger.debug(String.format("Failed to lookup libvirt storage pool [%s].", uuid), e); + logger.debug("Failed to lookup libvirt storage pool [{}].", uuid, e); if (e.toString().contains("pool not found")) { - logger.debug(String.format("Removing pool [%s] from HA monitor since it was deleted.", uuid)); + logger.debug("Removing pool [{}] from HA monitor since it was deleted.", uuid); removedPools.add(uuid); } } @@ -160,11 +149,10 @@ public void run() { runHeartBeat(); try { - Thread.sleep(_heartBeatUpdateFreq); + Thread.sleep(_heartBeatUpdateFreqInMs); } catch (InterruptedException e) { logger.debug("[IGNORED] Interrupted between heartbeats.", e); } } } - } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAVMActivityChecker.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAVMActivityChecker.java index e6937b515e95..c13be64a3a3e 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAVMActivityChecker.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/KVMHAVMActivityChecker.java @@ -39,12 +39,12 @@ public KVMHAVMActivityChecker(final HAStoragePool pool, final HostTO host, final } @Override - public Boolean checkingHeartBeat() { - return this.storagePool.getPool().vmActivityCheck(storagePool, host, activityScriptTimeout, volumeUuidList, vmActivityCheckPath, suspectTimeInSeconds); + public Boolean hasHeartBeat() { + return this.storagePool.getPool().hasVmActivity(storagePool, host, activityScriptTimeout, volumeUuidList, vmActivityCheckPath, suspectTimeInSeconds); } @Override public Boolean call() throws Exception { - return checkingHeartBeat(); + return hasHeartBeat(); } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 0a9e0d2d98e6..4281036d9456 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -16,8 +16,12 @@ // under the License. package com.cloud.hypervisor.kvm.resource; +import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT; import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION; import static com.cloud.host.Host.HOST_OVFTOOL_VERSION; +import static com.cloud.host.Host.HOST_VDDK_LIB_DIR; +import static com.cloud.host.Host.HOST_VDDK_SUPPORT; +import static com.cloud.host.Host.HOST_VDDK_VERSION; import static com.cloud.host.Host.HOST_VIRTV2V_VERSION; import static com.cloud.host.Host.HOST_VOLUME_ENCRYPTION; import static org.apache.cloudstack.utils.linux.KVMHostInfo.isHostS390x; @@ -32,6 +36,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; @@ -47,8 +52,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -70,12 +73,16 @@ import javax.xml.xpath.XPathFactory; import com.cloud.agent.api.to.VirtualMachineMetadataTO; +import com.cloud.utils.exception.BackupException; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import com.cloud.agent.api.to.DataObjectType; import org.apache.cloudstack.api.ApiConstants.IoDriverPolicy; import org.apache.cloudstack.command.CommandInfo; import org.apache.cloudstack.command.ReconcileCommandService; import org.apache.cloudstack.command.ReconcileCommandUtils; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsAnswer; import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand; import org.apache.cloudstack.storage.configdrive.ConfigDrive; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; @@ -121,6 +128,7 @@ import org.libvirt.LibvirtException; import org.libvirt.MemoryStatistic; import org.libvirt.Network; +import org.libvirt.SchedLongParameter; import org.libvirt.SchedParameter; import org.libvirt.SchedUlongParameter; import org.libvirt.Secret; @@ -221,9 +229,11 @@ import com.cloud.resource.ServerResource; import com.cloud.resource.ServerResourceBase; import com.cloud.storage.JavaStorageLayer; +import com.cloud.template.TemplateManager; import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageLayer; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Volume; import com.cloud.storage.resource.StorageSubsystemCommandHandler; import com.cloud.storage.resource.StorageSubsystemCommandHandlerBase; @@ -365,6 +375,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String WINDOWS_GUEST_CONVERSION_SUPPORTED_CHECK_CMD = "rpm -qa | grep -i virtio-win"; public static final String UBUNTU_WINDOWS_GUEST_CONVERSION_SUPPORTED_CHECK_CMD = "dpkg -l virtio-win"; public static final String UBUNTU_NBDKIT_PKG_CHECK_CMD = "dpkg -l nbdkit"; + public static final String VDDK_AUTODETECT_PATH_CMD = "find / -type d -name 'vmware-vix-disklib-distrib' 2>/dev/null | head -n 1"; public static final int LIBVIRT_CGROUP_CPU_SHARES_MIN = 2; public static final int LIBVIRT_CGROUP_CPU_SHARES_MAX = 262144; @@ -382,6 +393,23 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String CHECKPOINT_DELETE_COMMAND = "virsh checkpoint-delete --domain %s --checkpointname %s --metadata"; + public static final int IMAGE_SERVER_DEFAULT_PORT = 54322; + public static final String IMAGE_SERVER_SYSTEMD_UNIT_NAME = "cloudstack-image-server"; + + private static final String BLOCK_PULL_COMMAND = "virsh blockpull --domain %s --path %s"; + + private static final String SNAPSHOT_XML = "\n" + + "%s\n" + + "\n" + + " \n" + + "%s" + + " \n" + + ""; + + private static final String TAG_DISK_SNAPSHOT = "\n" + + "\n" + + "\n"; + protected int qcow2DeltaMergeTimeout; private String modifyVlanPath; @@ -395,6 +423,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private String heartBeatPath; private String vmActivityCheckPath; private String nasBackupPath; + private String imageServerPath; + private boolean imageServerTlsEnabled = false; + private String imageServerListenAddress; private String securityGroupPath; private String ovsPvlanDhcpHostPath; private String ovsPvlanVmPath; @@ -579,6 +610,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String CGROUP_V2 = "cgroup2fs"; + public static final String AGENT_IS_NOT_CONNECTED = "QEMU guest agent is not connected"; + /** * Virsh command to merge (blockcommit) snapshot into the base file.

    * 1st parameter: VM's name;
    @@ -597,7 +630,7 @@ public long getHypervisorQemuVersion() { @Override public synchronized void registerStatusUpdater(AgentStatusUpdater updater) { - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED)) { + if (isLibvirtEventsEnabled()) { try { Connect conn = LibvirtConnection.getConnection(); if (libvirtDomainListener != null) { @@ -809,6 +842,18 @@ public String getNasBackupPath() { return nasBackupPath; } + public String getImageServerPath() { + return imageServerPath; + } + + public boolean isImageServerTlsEnabled() { + return imageServerTlsEnabled; + } + + public String getImageServerListenAddress() { + return imageServerListenAddress; + } + public String getOvsPvlanDhcpHostPath() { return ovsPvlanDhcpHostPath; } @@ -879,16 +924,41 @@ protected enum HealthCheckResult { SUCCESS, FAILURE, IGNORE } + public enum CpuSchedulerParameter { + CPU_SHARES("cpu_shares"), PERIOD("vcpu_period"), QUOTA("vcpu_quota"); + + private String name; + + CpuSchedulerParameter(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return getName(); + } + } + protected BridgeType bridgeType; protected StorageSubsystemCommandHandler storageHandler; private boolean convertInstanceVerboseMode = false; - private String[] convertInstanceEnv = null; + private Map convertInstanceEnv = null; + private String vddkLibDir = null; + private static final String libguestfsBackend = "direct"; protected boolean dpdkSupport = false; protected String dpdkOvsPath; protected String directDownloadTemporaryDownloadPath; protected String cachePath; + private String vddkTransports = null; + private String vddkThumbprint = null; + private String vddkVersion = null; + private String detectedPasswordFileOption = null; protected String javaTempDir = System.getProperty("java.io.tmpdir"); private String getEndIpFromStartIp(final String startIp, final int numIps) { @@ -949,10 +1019,30 @@ public boolean isConvertInstanceVerboseModeEnabled() { return convertInstanceVerboseMode; } - public String[] getConvertInstanceEnv() { + public Map getConvertInstanceEnv() { return convertInstanceEnv; } + public String getVddkLibDir() { + return vddkLibDir; + } + + public String getLibguestfsBackend() { + return libguestfsBackend; + } + + public String getVddkTransports() { + return vddkTransports; + } + + public String getVddkThumbprint() { + return vddkThumbprint; + } + + public String getVddkVersion() { + return vddkVersion; + } + /** * Defines resource's public and private network interface according to what is configured in agent.properties. */ @@ -1027,6 +1117,9 @@ public boolean configure(final String name, final Map params) th cachePath = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_CACHE_LOCATION); + imageServerTlsEnabled = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_TLS_ENABLED); + imageServerListenAddress = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_LISTEN_ADDRESS); + params.put("domr.scripts.dir", domrScriptsDir); virtRouterResource = new VirtualRoutingResource(this); @@ -1065,11 +1158,6 @@ public boolean configure(final String name, final Map params) th throw new ConfigurationException("Unable to find patch.sh"); } - heartBeatPath = Script.findScript(kvmScriptsDir, "kvmheartbeat.sh"); - if (heartBeatPath == null) { - throw new ConfigurationException("Unable to find kvmheartbeat.sh"); - } - createVmPath = Script.findScript(storageScriptsDir, "createvm.sh"); if (createVmPath == null) { throw new ConfigurationException("Unable to find the createvm.sh"); @@ -1095,6 +1183,12 @@ public boolean configure(final String name, final Map params) th throw new ConfigurationException("Unable to find nasbackup.sh"); } + String imageServerMain = Script.findScript(kvmScriptsDir, "imageserver/__main__.py"); + if (imageServerMain == null) { + throw new ConfigurationException("Unable to find imageserver package"); + } + imageServerPath = new File(imageServerMain).getParent(); + createTmplPath = Script.findScript(storageScriptsDir, "createtmplt.sh"); if (createTmplPath == null) { throw new ConfigurationException("Unable to find the createtmplt.sh"); @@ -1158,6 +1252,37 @@ public boolean configure(final String name, final Map params) th setConvertInstanceEnv(convertEnvTmpDir, convertEnvVirtv2vTmpDir); + vddkLibDir = StringUtils.trimToNull(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VDDK_LIB_DIR)); + if (StringUtils.isNotBlank(vddkLibDir) && !isVddkLibDirValid(vddkLibDir)) { + LOGGER.warn("Configured VDDK library dir [{}] is invalid (missing lib64/libvixDiskLib.so), attempting auto-detection", vddkLibDir); + vddkLibDir = null; + } + if (StringUtils.isBlank(vddkLibDir)) { + vddkLibDir = detectVddkLibDir(); + } + if (StringUtils.isNotBlank(vddkLibDir)) { + LOGGER.info("Detected VDDK library dir: {}", vddkLibDir); + } else { + LOGGER.warn("Could not detect a valid VDDK library dir; VDDK conversion will be unavailable"); + } + + vddkVersion = detectVddkVersion(); + if (StringUtils.isNotBlank(vddkVersion)) { + LOGGER.info("Detected nbdkit VDDK plugin version: {}", vddkVersion); + } + + vddkTransports = StringUtils.trimToNull( + AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VDDK_TRANSPORTS)); + vddkThumbprint = StringUtils.trimToNull( + AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VDDK_THUMBPRINT)); + + detectedPasswordFileOption = detectPasswordFileOption(); + if (StringUtils.isNotBlank(detectedPasswordFileOption)) { + LOGGER.info("Detected virt-v2v password option: {}", detectedPasswordFileOption); + } else { + LOGGER.warn("Could not detect virt-v2v password option, VDDK conversions may fail"); + } + pool = (String)params.get("pool"); if (pool == null) { pool = "/root"; @@ -1332,9 +1457,9 @@ public boolean configure(final String name, final Map params) th final String[] info = NetUtils.getNetworkParams(privateNic); - kvmhaMonitor = new KVMHAMonitor(null, info[0], heartBeatPath); - final Thread ha = new Thread(kvmhaMonitor); - ha.start(); + kvmhaMonitor = new KVMHAMonitor(info[0]); + final Thread haMonitorThread = new Thread(kvmhaMonitor); + haMonitorThread.start(); storagePoolManager = new KVMStoragePoolManager(storageLayer, kvmhaMonitor); @@ -1439,14 +1564,14 @@ private void setConvertInstanceEnv(String convertEnvTmpDir, String convertEnvVir return; } if (StringUtils.isNotBlank(convertEnvTmpDir) && StringUtils.isNotBlank(convertEnvVirtv2vTmpDir)) { - convertInstanceEnv = new String[2]; - convertInstanceEnv[0] = String.format("%s=%s", "TMPDIR", convertEnvTmpDir); - convertInstanceEnv[1] = String.format("%s=%s", "VIRT_V2V_TMPDIR", convertEnvVirtv2vTmpDir); + convertInstanceEnv = new HashMap<>(2); + convertInstanceEnv.put("TMPDIR", convertEnvTmpDir); + convertInstanceEnv.put("VIRT_V2V_TMPDIR", convertEnvVirtv2vTmpDir); } else { - convertInstanceEnv = new String[1]; + convertInstanceEnv = new HashMap<>(1); String key = StringUtils.isNotBlank(convertEnvTmpDir) ? "TMPDIR" : "VIRT_V2V_TMPDIR"; String value = StringUtils.isNotBlank(convertEnvTmpDir) ? convertEnvTmpDir : convertEnvVirtv2vTmpDir; - convertInstanceEnv[0] = String.format("%s=%s", key, value); + convertInstanceEnv.put(key, value); } } @@ -2251,7 +2376,7 @@ public String startVM(final Connect conn, final String vmName, final String doma public boolean stop() { try { final Connect conn = LibvirtConnection.getConnection(); - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED) && libvirtDomainListener != null) { + if (isLibvirtEventsEnabled() && libvirtDomainListener != null) { LOGGER.debug("Clearing old domain listener"); conn.removeLifecycleListener(libvirtDomainListener); } @@ -2481,6 +2606,8 @@ public String getResizeScriptType(final KVMStoragePool pool, final KVMPhysicalDi if (pool.getType() == StoragePoolType.CLVM && volFormat == PhysicalDiskFormat.RAW) { return "CLVM"; + } else if (poolType == StoragePoolType.CLVM_NG) { + return "CLVM_NG"; } else if ((poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SharedMountPoint || poolType == StoragePoolType.Filesystem @@ -2909,23 +3036,61 @@ protected String getUuid(String uuid) { protected void setQuotaAndPeriod(VirtualMachineTO vmTO, CpuTuneDef ctd) { if (vmTO.isLimitCpuUse() && vmTO.getCpuQuotaPercentage() != null) { Double cpuQuotaPercentage = vmTO.getCpuQuotaPercentage(); - int period = CpuTuneDef.DEFAULT_PERIOD; - int quota = (int) (period * cpuQuotaPercentage); - if (quota < CpuTuneDef.MIN_QUOTA) { - LOGGER.info("Calculated quota (" + quota + ") below the minimum (" + CpuTuneDef.MIN_QUOTA + ") for VM domain " + vmTO.getUuid() + ", setting it to minimum " + - "and calculating period instead of using the default"); - quota = CpuTuneDef.MIN_QUOTA; - period = (int) ((double) quota / cpuQuotaPercentage); - if (period > CpuTuneDef.MAX_PERIOD) { - LOGGER.info("Calculated period (" + period + ") exceeds the maximum (" + CpuTuneDef.MAX_PERIOD + - "), setting it to the maximum"); - period = CpuTuneDef.MAX_PERIOD; - } + Pair periodAndQuota = getPeriodAndQuota(cpuQuotaPercentage); + ctd.setPeriod(periodAndQuota.first()); + ctd.setQuota(periodAndQuota.second()); + LOGGER.info("Setting quota = [{}] and period = [{}] to VM domain [{}].", periodAndQuota.second(), periodAndQuota.first(), vmTO.getUuid()); + } + } + + /** + * Calculates the CPU period and quota based on the quota percentage defined by the Management Server + * @param cpuQuotaPercentage CPU quota percentage defined by the Management Server + * @return The period and quota to be defined for the VM's domain + */ + protected Pair getPeriodAndQuota(double cpuQuotaPercentage) { + int period = CpuTuneDef.DEFAULT_PERIOD; + long quota = (long) (period * cpuQuotaPercentage); + if (quota < CpuTuneDef.MIN_QUOTA) { + LOGGER.info("Calculated quota ({}) below the minimum ({}), setting it to minimum and calculating period instead of using the default", quota, CpuTuneDef.MIN_QUOTA); + quota = CpuTuneDef.MIN_QUOTA; + period = (int) ((double) quota / cpuQuotaPercentage); + if (period > CpuTuneDef.MAX_PERIOD) { + LOGGER.info("Calculated period ({}) exceeds the maximum ({}), setting it to the maximum", period, CpuTuneDef.MAX_PERIOD); + period = CpuTuneDef.MAX_PERIOD; } - ctd.setQuota(quota); - ctd.setPeriod(period); - LOGGER.info("Setting quota=" + quota + ", period=" + period + " to VM domain " + vmTO.getUuid()); } + + LOGGER.info("Calculated period = [{}] and quota = [{}] given the [{}] quota percentage.", period, quota, cpuQuotaPercentage); + return new Pair<>(period, quota); + } + + /** + * Dynamically updates the domain's "vcpu_quota" and "period" fields of the CPU tune definition. + * This is required because the values of the fields must change according to the new CPU speed of the VM. + * When the CPU limitation is removed from the domain, the "vcpu_quota" field is set to 17,592,186,044,415. + * @param domain VM's domain. + * @param vmTO VM's transfer object, which contains the required fields to update the "vcpu_quota" and "period" fields. + * @param limitCpuUseChange Indicates whether the CPU limitation for the VM has changed. + * @throws org.libvirt.LibvirtException + **/ + public void updateCpuQuotaAndPeriod(Domain domain, VirtualMachineTO vmTO, boolean limitCpuUseChange) throws LibvirtException { + if (hypervisorLibvirtVersion < MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE || (!limitCpuUseChange && !vmTO.isLimitCpuUse())) { + logger.info("Not updating the [{}] and [{}] for the [{}] domain, because [{}].", + CpuSchedulerParameter.QUOTA, CpuSchedulerParameter.PERIOD, domain.getName(), hypervisorLibvirtVersion < MIN_LIBVIRT_VERSION_FOR_GUEST_CPU_TUNE ? + "the current Libvirt version does not support CPU tune" : "it was not requested to remove, change or apply CPU limitation for the instance."); + return; + } + + if (limitCpuUseChange && !vmTO.isLimitCpuUse()) { + logger.info("Updating the [{}] of the [{}] domain to [{}], because CPU limitation has been removed.", CpuSchedulerParameter.QUOTA, domain.getName(), CpuTuneDef.MAX_CPU_QUOTA); + LibvirtComputingResource.setQuota(domain, CpuTuneDef.MAX_CPU_QUOTA); + return; + } + + Pair periodAndQuota = getPeriodAndQuota(vmTO.getCpuQuotaPercentage()); + LibvirtComputingResource.setPeriod(domain, periodAndQuota.first()); + LibvirtComputingResource.setQuota(domain, periodAndQuota.second()); } protected void enlightenWindowsVm(VirtualMachineTO vmTO, FeaturesDef features) { @@ -3389,10 +3554,10 @@ protected GuestResourceDef createGuestResourceDef(VirtualMachineTO vmTO){ grd.setMemBalloning(!noMemBalloon); - Long maxRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMaxRam()); - - grd.setMemorySize(maxRam); - grd.setCurrentMem(getCurrentMemAccordingToMemBallooning(vmTO, maxRam)); + long requestedRam = ByteScaleUtils.bytesToKibibytes(vmTO.getRequestedRam()); + long minRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMinRam()); + grd.setCurrentMem(getCurrentMemAccordingToMemBallooning(vmTO, requestedRam, minRam)); + grd.setMaxMemory(ByteScaleUtils.bytesToKibibytes(vmTO.getMaxRam())); int vcpus = vmTO.getCpus(); Integer maxVcpus = vmTO.getVcpuMaxLimit(); @@ -3403,18 +3568,19 @@ protected GuestResourceDef createGuestResourceDef(VirtualMachineTO vmTO){ return grd; } - protected long getCurrentMemAccordingToMemBallooning(VirtualMachineTO vmTO, long maxRam) { - long retVal = maxRam; + protected long getCurrentMemAccordingToMemBallooning(VirtualMachineTO vmTO, long requestedRam, long minRam) { if (noMemBalloon) { - LOGGER.warn(String.format("Setting VM's [%s] current memory as max memory [%s] due to memory ballooning is disabled. If you are using a custom service offering, verify if memory ballooning really should be disabled.", vmTO.toString(), maxRam)); - } else if (vmTO != null && vmTO.getType() != VirtualMachine.Type.User) { - LOGGER.warn(String.format("Setting System VM's [%s] current memory as max memory [%s].", vmTO.toString(), maxRam)); - } else { - long minRam = ByteScaleUtils.bytesToKibibytes(vmTO.getMinRam()); - LOGGER.debug(String.format("Setting VM's [%s] current memory as min memory [%s] due to memory ballooning is enabled.", vmTO.toString(), minRam)); - retVal = minRam; + LOGGER.warn("Setting VM's [{}] current memory as requested memory [{}] due to memory ballooning is disabled.", vmTO.toString(), requestedRam); + return requestedRam; + } + + if (vmTO != null && vmTO.getType() != VirtualMachine.Type.User) { + LOGGER.warn("Setting System VM's [{}] current memory as requested memory [{}].", vmTO.toString(), requestedRam); + return requestedRam; } - return retVal; + + LOGGER.debug("Setting VM's [{}] current memory as min memory [{}] due to memory ballooning is enabled.", vmTO.toString(), minRam); + return minRam; } /** @@ -3550,6 +3716,7 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { if (vmSpec.getOs().toLowerCase().contains("window")) { isWindowsTemplate = true; } + final Set definedCdromSlots = new HashSet<>(); for (final DiskTO volume : disks) { KVMPhysicalDisk physicalDisk = null; KVMStoragePool pool = null; @@ -3628,6 +3795,7 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { if (volume.getType() == Volume.Type.ISO) { final DiskDef.DiskType diskType = getDiskType(physicalDisk); disk.defISODisk(volPath, devId, isUefiEnabled, diskType); + definedCdromSlots.add(devId); if (guestCpuArch != null && (guestCpuArch.equals("aarch64") || guestCpuArch.equals("s390x"))) { disk.setBusType(DiskDef.DiskBus.SCSI); @@ -3680,13 +3848,18 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { final String glusterVolume = pool.getSourceDir().replace("/", ""); disk.defNetworkBasedDisk(glusterVolume + path.replace(mountpoint, ""), pool.getSourceHost(), pool.getSourcePort(), null, null, devId, diskBusType, DiskProtocol.GLUSTER, DiskDef.DiskFmtType.QCOW2); - } else if (pool.getType() == StoragePoolType.CLVM || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { + } else if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData); - } - else { + } else { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusType); } + + // CLVM_NG uses QCOW2 format on block devices, override the default RAW format + if (pool.getType() == StoragePoolType.CLVM_NG) { + disk.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); + } + if (pool.getType() == StoragePoolType.Linstor && isQemuDiscardBugFree(diskBusType)) { disk.setDiscard(DiscardType.UNMAP); } @@ -3720,6 +3893,17 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { vm.getDevices().addDevice(disk); } + if (vmSpec.getType() == VirtualMachine.Type.User) { + for (int slot = TemplateManager.CDROM_PRIMARY_DEVICE_SEQ; + slot < TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + LibvirtVMDef.MAX_CDROMS_PER_VM; slot++) { + if (!definedCdromSlots.contains(slot)) { + final DiskDef emptyCdrom = new DiskDef(); + emptyCdrom.defISODisk(null, slot, isUefiEnabled, DiskDef.DiskType.FILE); + vm.getDevices().addDevice(emptyCdrom); + } + } + } + if (vmSpec.getType() != VirtualMachine.Type.User) { final DiskDef iso = new DiskDef(); iso.defISODisk(sysvmISOPath, DiskDef.DiskType.FILE); @@ -4229,12 +4413,21 @@ public StartupCommand[] initialize() { cmd.setHostTags(getHostTags()); boolean instanceConversionSupported = hostSupportsInstanceConversion(); cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported)); + cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk())); + cmd.getHostDetails().put(HOST_CDROM_MAX_COUNT, String.valueOf(LibvirtVMDef.MAX_CDROMS_PER_VM)); + if (StringUtils.isNotBlank(vddkLibDir)) { + cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir); + } + if (StringUtils.isNotBlank(vddkVersion)) { + cmd.getHostDetails().put(HOST_VDDK_VERSION, vddkVersion); + } if (instanceConversionSupported) { cmd.getHostDetails().put(HOST_VIRTV2V_VERSION, getHostVirtV2vVersion()); } if (hostSupportsOvfExport()) { cmd.getHostDetails().put(HOST_OVFTOOL_VERSION, getHostOvfToolVersion()); } + addBackupJobDetails(cmd.getHostDetails()); HealthCheckResult healthCheckResult = getHostHealthCheckResult(); if (healthCheckResult != HealthCheckResult.IGNORE) { cmd.setHostHealthCheckResult(healthCheckResult == HealthCheckResult.SUCCESS); @@ -4268,6 +4461,18 @@ public StartupCommand[] initialize() { return startupCommandsArray; } + private void addBackupJobDetails(Map details) { + Integer maxCompressionOperations = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST); + if (maxCompressionOperations != null) { + details.put(AgentProperties.BACKUP_COMPRESSION_MAX_CONCURRENT_OPERATIONS_PER_HOST.getName(), maxCompressionOperations.toString()); + } + + Integer maxValidationOperations = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST); + if (maxValidationOperations != null) { + details.put(AgentProperties.BACKUP_VALIDATION_MAX_CONCURRENT_OPERATIONS_PER_HOST.getName(), maxValidationOperations.toString()); + } + } + protected List getHostTags() { List hostTagsList = new ArrayList<>(); String hostTags = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_TAGS); @@ -4851,6 +5056,12 @@ public List getInterfaces(final Connect conn, final String vmName) } } + public InterfaceDef getInterface(final Connect conn, final String vmName, final String macAddress) { + List interfaces = getInterfaces(conn, vmName); + return interfaces.stream().filter(interfaceDef -> interfaceDef.getMacAddress().equals(macAddress)) + .findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find NIC with MAC address %s.", macAddress))); + } + public List getDisks(final Connect conn, final String vmName) { final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); Domain dm = null; @@ -4967,7 +5178,7 @@ public DiskDef getDiskWithPathOfVolumeObjectTO(List disks, VolumeObject return disks.stream() .filter(diskDef -> diskDef.getDiskPath() != null && diskDef.getDiskPath().contains(vol.getPath())) .findFirst() - .orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find volume [%s].", vol.getUuid()))); + .orElseThrow(() -> new CloudRuntimeException(String.format("Unable to find volume [%s] with path [%s].", vol.getUuid(), vol.getPath()))); } protected String getDiskPathFromDiskDef(DiskDef disk) { @@ -5221,6 +5432,24 @@ public void removeCheckpointsOnVm(String vmName, String volumeUuid, List logger.debug("Removed all checkpoints of volume [{}] on VM [{}].", volumeUuid, vmName); } + public Map getDiskPathLabelMap(String vmName) { + try { + Connect conn = LibvirtConnection.getConnectionByVmName(vmName); + List disks = getDisks(conn, vmName); + Map diskPathLabelMap = new HashMap<>(); + for (DiskDef disk : disks) { + if (disk.getDeviceType() != DeviceType.DISK) { + continue; + } + diskPathLabelMap.put(disk.getDiskPath(), disk.getDiskLabel()); + } + return diskPathLabelMap; + } catch (LibvirtException e) { + logger.error("Failed to get disk path label map for VM [{}] due to: [{}].", vmName, e.getMessage(), e); + throw new CloudRuntimeException(e); + } + } + public boolean recreateCheckpointsOnVm(List volumes, String vmName, Connect conn) { logger.debug("Trying to recreate checkpoints on VM [{}] with volumes [{}].", vmName, volumes); try { @@ -5555,10 +5784,73 @@ public boolean configureDefaultNetworkRulesForSystemVm(final Connect conn, final public Answer listFilesAtPath(ListDataStoreObjectsCommand command) { DataStoreTO store = command.getStore(); - KVMStoragePool storagePool = storagePoolManager.getStoragePool(StoragePoolType.NetworkFilesystem, store.getUuid()); + StoragePoolType poolType = StoragePoolType.NetworkFilesystem; + if (store instanceof PrimaryDataStoreTO) { + poolType = ((PrimaryDataStoreTO) store).getPoolType(); + } + KVMStoragePool storagePool = storagePoolManager.getStoragePool(poolType, store.getUuid()); + if (ClvmPoolManager.isClvmPoolType(poolType)) { + return listLvmVolumes(storagePool.getLocalPath(), command.getStartIndex(), command.getPageSize()); + } return listFilesAtPath(storagePool.getLocalPath(), command.getPath(), command.getStartIndex(), command.getPageSize()); } + private Answer listLvmVolumes(String localPath, int startIndex, int pageSize) { + String vgName = localPath; + if (vgName.startsWith("/")) { + String[] parts = vgName.split("/"); + for (int i = parts.length - 1; i >= 0; i--) { + if (!parts[i].isEmpty()) { + vgName = parts[i]; + break; + } + } + } + + Script lvs = new Script("lvs", 30000, logger); + lvs.add("--noheadings"); + lvs.add("--nosuffix"); + lvs.add("-o", "lv_name,lv_size"); + lvs.add("--units", "b"); + lvs.add(vgName); + AllLinesParser parser = new AllLinesParser(); + String result = lvs.execute(parser); + + List names = new ArrayList<>(); + List paths = new ArrayList<>(); + List absPaths = new ArrayList<>(); + List isDirs = new ArrayList<>(); + List sizes = new ArrayList<>(); + List lastModified = new ArrayList<>(); + + if (result != null) { + logger.warn("lvs listing failed for VG {}: {}", vgName, result); + return new ListDataStoreObjectsAnswer(false, 0, names, paths, absPaths, isDirs, sizes, lastModified); + } + + List entries = new ArrayList<>(); + for (String line : parser.getLines().split("\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + String[] cols = trimmed.split("\\s+"); + if (cols.length >= 2) entries.add(cols); + } + + int count = entries.size(); + for (int i = startIndex; i < startIndex + pageSize && i < count; i++) { + String lvName = entries.get(i)[0]; + long size = 0; + try { size = Long.parseLong(entries.get(i)[1]); } catch (NumberFormatException ignored) {} + names.add(lvName); + paths.add("/" + lvName); + absPaths.add("/dev/" + vgName + "/" + lvName); + isDirs.add(false); + sizes.add(size); + lastModified.add(0L); + } + return new ListDataStoreObjectsAnswer(true, count, names, paths, absPaths, isDirs, sizes, lastModified); + } + public boolean addNetworkRules(final String vmName, final String vmId, final String guestIP, final String guestIP6, final String sig, final String seq, final String mac, final String rules, final String vif, final String brname, final String secIps) { if (!canBridgeFirewall) { @@ -5944,6 +6236,66 @@ public boolean hostSupportsInstanceConversion() { return exitValue == 0; } + public boolean hostSupportsVddk() { + return hostSupportsVddk(null); + } + + public boolean hostSupportsVddk(String overriddenVddkLibDir) { + String effectiveVddkLibDir = StringUtils.trimToNull(overriddenVddkLibDir); + if (StringUtils.isBlank(effectiveVddkLibDir)) { + effectiveVddkLibDir = StringUtils.trimToNull(vddkLibDir); + } + if (StringUtils.isBlank(effectiveVddkLibDir) || !isVddkLibDirValid(effectiveVddkLibDir)) { + effectiveVddkLibDir = detectVddkLibDir(); + } + return hostSupportsInstanceConversion() && isVddkLibDirValid(effectiveVddkLibDir) && StringUtils.isNotBlank(detectVddkVersion()); + } + + protected boolean isVddkLibDirValid(String path) { + if (StringUtils.isBlank(path)) { + return false; + } + File libDir = new File(path, "lib64"); + if (!libDir.isDirectory()) { + return false; + } + File[] libs = libDir.listFiles((dir, name) -> name.startsWith("libvixDiskLib.so")); + return libs != null && libs.length > 0; + } + + protected String detectVddkLibDir() { + String detectedPath = StringUtils.trimToNull(Script.runSimpleBashScript(VDDK_AUTODETECT_PATH_CMD)); + if (StringUtils.isNotBlank(detectedPath) && isVddkLibDirValid(detectedPath)) { + return detectedPath; + } + return null; + } + + protected String detectVddkVersion() { + try { + ProcessBuilder pb = new ProcessBuilder("nbdkit", "vddk", "--version"); + Process process = pb.start(); + + String output = new String(process.getInputStream().readAllBytes()); + process.waitFor(); + + if (StringUtils.isBlank(output)) { + return null; + } + + for (String line : output.split("\\R")) { + String trimmed = StringUtils.trimToEmpty(line); + if (trimmed.startsWith("vddk ")) { + return StringUtils.trimToNull(trimmed.substring("vddk ".length())); + } + } + return null; + } catch (Exception e) { + LOGGER.error("Failed to detect vddk version: {}", e.getMessage()); + return null; + } + } + public boolean hostSupportsWindowsGuestConversion() { if (isUbuntuOrDebianHost()) { int exitValue = Script.runSimpleBashScriptForExitValue(UBUNTU_WINDOWS_GUEST_CONVERSION_SUPPORTED_CHECK_CMD); @@ -5958,6 +6310,40 @@ public boolean hostSupportsOvfExport() { return exitValue == 0; } + /** + * Detect which password option virt-v2v supports by examining its --help output + * @return "-ip" if supported (virt-v2v >= 2.8.1), "--password-file" if older version, or null if detection fails + */ + protected String detectPasswordFileOption() { + try { + ProcessBuilder pb = new ProcessBuilder("virt-v2v", "--help"); + Process process = pb.start(); + + String output = new String(process.getInputStream().readAllBytes()); + process.waitFor(); + + if (output.contains("-ip ")) { + return "-ip"; + } else if (output.contains("--password-file")) { + return "--password-file"; + } else { + LOGGER.error("virt-v2v does not support -ip or --password-file"); + return null; + } + } catch (Exception e) { + LOGGER.error("Failed to detect virt-v2v password option: {}", e.getMessage()); + return null; + } + } + + /** + * Get the detected password file option for virt-v2v + * @return the password option ("-ip" or "--password-file") or null if not detected + */ + public String getDetectedPasswordFileOption() { + return detectedPasswordFileOption; + } + public String getHostVirtV2vVersion() { if (!hostSupportsInstanceConversion()) { return ""; @@ -6092,29 +6478,59 @@ public static long countDomainRunningVcpus(Domain dm) throws LibvirtException { **/ public static Integer getCpuShares(Domain dm) throws LibvirtException { for (SchedParameter c : dm.getSchedulerParameters()) { - if (c.field.equals("cpu_shares")) { + if (c.field.equals(CpuSchedulerParameter.CPU_SHARES.getName())) { return Integer.parseInt(c.getValueAsString()); } } - LOGGER.warn(String.format("Could not get cpu_shares of domain: [%s]. Returning default value of 0. ", dm.getName())); + LOGGER.warn("Could not get [{}] of domain: [{}]. Returning default value of 0. ", CpuSchedulerParameter.CPU_SHARES.getName(), dm.getName()); return 0; } /** - * Sets the cpu_shares (priority) of the running VM
    + * Updates the cpu_shares (priority) of the running VM. * @param dm domain of the VM. * @param cpuShares new priority of the running VM. - * @throws org.libvirt.LibvirtException **/ public static void setCpuShares(Domain dm, Integer cpuShares) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.CPU_SHARES.getName(), dm.getName(), cpuShares); SchedUlongParameter[] params = new SchedUlongParameter[1]; params[0] = new SchedUlongParameter(); - params[0].field = "cpu_shares"; + params[0].field = CpuSchedulerParameter.CPU_SHARES.getName(); params[0].value = cpuShares; dm.setSchedulerParameters(params); } + /** + * Updates the period of the running VM. + * @param domain domain of the VM. + * @param period new period of the running VM. + **/ + public static void setPeriod(Domain domain, int period) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.PERIOD.getName(), domain.getName(), period); + SchedUlongParameter[] params = new SchedUlongParameter[1]; + params[0] = new SchedUlongParameter(); + params[0].field = CpuSchedulerParameter.PERIOD.getName(); + params[0].value = period; + + domain.setSchedulerParameters(params); + } + + /** + * Updates the quota of the running VM. + * @param domain domain of the VM. + * @param quota new quota of the running VM. + **/ + public static void setQuota(Domain domain, long quota) throws LibvirtException { + LOGGER.info("Dynamically updating the [{}] of the [{}] VM to [{}].", CpuSchedulerParameter.QUOTA.getName(), domain.getName(), quota); + SchedLongParameter[] params = new SchedLongParameter[1]; + params[0] = new SchedLongParameter(); + params[0].field = CpuSchedulerParameter.QUOTA.getName(); + params[0].value = quota; + + domain.setSchedulerParameters(params); + } + /** * Set up a libvirt secret for a volume. If Libvirt says that a secret already exists for this volume path, we use its uuid. * The UUID of the secret needs to be prescriptive such that we can register the same UUID on target host during live migration @@ -6194,7 +6610,7 @@ public static String generateSecretUUIDFromString(String seed) { } /** - * Merges the snapshot into base file. + * Merges the delta into a base file. * * @param vm Domain of the VM; * @param diskLabel Disk label to manage snapshot and base file; @@ -6206,15 +6622,19 @@ public static String generateSecretUUIDFromString(String seed) { * @param conn Libvirt connection; * @throws LibvirtException */ - public void mergeSnapshotIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume, + public void mergeDeltaIntoBaseFile(Domain vm, String diskLabel, String baseFilePath, String topFilePath, boolean active, String snapshotName, VolumeObjectTO volume, Connect conn) throws LibvirtException { - if (AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED)) { + if (isLibvirtEventsEnabled()) { mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn); } else { mergeSnapshotIntoBaseFileWithoutEvents(vm, diskLabel, baseFilePath, topFilePath, active, snapshotName, volume, conn); } } + protected Boolean isLibvirtEventsEnabled() { + return AgentPropertiesFileHandler.getPropertyValue(AgentProperties.LIBVIRT_EVENTS_ENABLED); + } + /** * This method only works if LIBVIRT_EVENTS_ENABLED is true. * */ @@ -6231,40 +6651,26 @@ protected void mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(Domain commitFlags |= Domain.BlockCommitFlags.ACTIVE; } - Semaphore semaphore = getSemaphoreToWaitForMerge(); - BlockCommitListener blockCommitListener = getBlockCommitListener(semaphore, vmName); - vm.addBlockJobListener(blockCommitListener); - - logger.info("Starting block commit of snapshot [{}] of VM [{}]. Using parameters: diskLabel [{}]; baseFilePath [{}]; topFilePath [{}]; commitFlags [{}]", snapshotName, - vmName, diskLabel, baseFilePath, topFilePath, commitFlags); + BlockCommitListener blockCommitListener = getBlockCommitListener(vmName); + try { + vm.addBlockJobListener(blockCommitListener); - vm.blockCommit(diskLabel, baseFilePath, topFilePath, 0, commitFlags); + logger.info("Starting block commit of QCOW2 delta [{}] of VM [{}]. Using parameters: diskLabel [{}]; baseFilePath [{}]; topFilePath [{}]; commitFlags [{}]", + snapshotName, + vmName, diskLabel, baseFilePath, topFilePath, commitFlags); - Thread checkProgressThread = new Thread(() -> checkBlockCommitProgress(vm, diskLabel, vmName, snapshotName, topFilePath, baseFilePath)); - checkProgressThread.start(); + vm.blockCommit(diskLabel, baseFilePath, topFilePath, 0, commitFlags); - String errorMessage = String.format("the block commit of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]." + - " The job will be left running to avoid data corruption, but ACS will return an error and volume [%s] will need to be normalized manually. If the commit" + - " involved the active image, the pivot will need to be manually done.", topFilePath, baseFilePath, snapshotName, vmName, volume); - try { - if (!semaphore.tryAcquire(qcow2DeltaMergeTimeout, TimeUnit.SECONDS)) { - throw new CloudRuntimeException("Timed out while waiting for " + errorMessage); - } - } catch (InterruptedException e) { - throw new CloudRuntimeException("Interrupted while waiting for " + errorMessage); + checkBlockCommitProgress(vm, diskLabel, vmName, snapshotName, topFilePath, baseFilePath); } finally { vm.removeBlockJobListener(blockCommitListener); } String mergeResult = blockCommitListener.getResult(); - try { - checkProgressThread.join(); - } catch (InterruptedException ex) { - throw new CloudRuntimeException(String.format("Exception while running wait block commit task of snapshot [%s] and VM [%s].", snapshotName, vmName)); - } - if (mergeResult != null) { - String commitError = String.format("Failed %s The failure occurred due to [%s].", errorMessage, mergeResult); + String commitError = String.format("Failed the block commit of top file [%s] into base file [%s] for snapshot [%s] of VM [%s]. The job will be left running to avoid" + + " data corruption, but ACS will return an error and volume [%s] will need to be normalized manually. If the commit involved the active image, the pivot will" + + " need to be manually done. The failure occurred due to [%s].", topFilePath, baseFilePath, snapshotName, vmName, volume, mergeResult); logger.error(commitError); throw new CloudRuntimeException(commitError); } @@ -6321,15 +6727,8 @@ protected String buildMergeCommand(String vmName, String diskLabel, String baseF /** * This was created to facilitate testing. * */ - protected BlockCommitListener getBlockCommitListener(Semaphore semaphore, String vmName) { - return new BlockCommitListener(semaphore, vmName, ThreadContext.get("logcontextid")); - } - - /** - * This was created to facilitate testing. - * */ - protected Semaphore getSemaphoreToWaitForMerge() { - return new Semaphore(0); + protected BlockCommitListener getBlockCommitListener(String vmName) { + return new BlockCommitListener(vmName, ThreadContext.get("logcontextid")); } protected void checkBlockCommitProgress(Domain vm, String diskLabel, String vmName, String snapshotName, String topFilePath, String baseFilePath) { @@ -6345,8 +6744,8 @@ protected void checkBlockCommitProgress(Domain vm, String diskLabel, String vmNa try { Thread.sleep(1000); } catch (InterruptedException ex) { - logger.debug("Thread that was tracking the progress {} was interrupted.", partialLog, ex); - return; + logger.trace("Thread that was tracking the progress for the block commit job {} was interrupted. Ignoring.", partialLog, ex); + continue; } try { @@ -6523,4 +6922,494 @@ public String getHypervisorPath() { public String getGuestCpuArch() { return guestCpuArch; } + + /** + * CLVM volume state for migration operations on source host + */ + public enum ClvmVolumeState { + /** Shared mode (-asy) - used before migration to allow both hosts to access volume */ + SHARED("-asy", "shared", "Before migration: activating in shared mode"), + + /** Deactivate (-an) - used after successful migration to release volume on source */ + DEACTIVATE("-an", "deactivated", "After successful migration: deactivating volume"), + + /** Exclusive mode (-aey) - used after failed migration to revert to original exclusive state */ + EXCLUSIVE("-aey", "exclusive", "After failed migration: reverting to exclusive mode"); + + private final String lvchangeFlag; + private final String description; + private final String logMessage; + + ClvmVolumeState(String lvchangeFlag, String description, String logMessage) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + this.logMessage = logMessage; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + + public String getLogMessage() { + return logMessage; + } + } + + public static void modifyClvmVolumesStateForMigration(List disks, VirtualMachineTO vmSpec, ClvmVolumeState state) { + for (DiskDef disk : disks) { + if (isClvmVolume(disk, vmSpec)) { + String volumePath = disk.getDiskPath(); + try { + modifyClvmVolumeState(volumePath, state.getLvchangeFlag(), state.getDescription(), state.getLogMessage()); + } catch (Exception e) { + LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}", + volumePath, state.getDescription(), e.getMessage(), e); + } + } + } + } + + private static void modifyClvmVolumeState(String volumePath, String lvchangeFlag, + String stateDescription, String logMessage) { + try { + LOGGER.info("{} for volume [{}]", logMessage, volumePath); + + Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); + cmd.add(lvchangeFlag); + cmd.add(volumePath); + + String result = cmd.execute(); + if (result != null) { + String errorMsg = String.format( + "Failed to set volume [%s] to %s state. Command result: %s", + volumePath, stateDescription, result); + LOGGER.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } else { + LOGGER.info("Successfully set volume [{}] to {} state.", + volumePath, stateDescription); + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = String.format( + "Exception while setting volume [%s] to %s state: %s", + volumePath, stateDescription, e.getMessage()); + LOGGER.error(errorMsg, e); + throw new CloudRuntimeException(errorMsg, e); + } + } + + public static void activateClvmVolumeExclusive(String volumePath) { + modifyClvmVolumeState(volumePath, ClvmVolumeState.EXCLUSIVE.getLvchangeFlag(), + ClvmVolumeState.EXCLUSIVE.getDescription(), + "Activating CLVM volume in exclusive mode"); + } + + public static void deactivateClvmVolume(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.DEACTIVATE.getLvchangeFlag(), + ClvmVolumeState.DEACTIVATE.getDescription(), + "Deactivating CLVM volume"); + } catch (Exception e) { + LOGGER.warn("Failed to deactivate CLVM volume {}: {}", volumePath, e.getMessage()); + } + } + + public static void setClvmVolumeToSharedMode(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.SHARED.getLvchangeFlag(), + ClvmVolumeState.SHARED.getDescription(), + "Setting CLVM volume to shared mode"); + } catch (Exception e) { + LOGGER.warn("Failed to set CLVM volume {} to shared mode: {}", volumePath, e.getMessage()); + } + } + + /** + * Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO. + * This is the most reliable method as it uses CloudStack's own storage pool information. + * + * @param disk The disk definition to check + * @param resource The LibvirtComputingResource instance (unused but kept for compatibility) + * @param vmSpec The VirtualMachineTO specification containing disk and pool information + * @return true if the disk is on a CLVM storage pool, false otherwise + */ + private static boolean isClvmVolume(DiskDef disk, VirtualMachineTO vmSpec) { + String diskPath = disk.getDiskPath(); + if (diskPath == null || vmSpec == null) { + return false; + } + + try { + if (vmSpec.getDisks() != null) { + for (DiskTO diskTO : vmSpec.getDisks()) { + if (!(diskTO.getData() instanceof VolumeObjectTO)) { + continue; + } + VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData(); + if (!diskPath.substring(diskPath.lastIndexOf(File.separator) + 1).equals(volumeTO.getPath())) { + continue; + } + DataStoreTO dataStore = volumeTO.getDataStore(); + if (!(dataStore instanceof PrimaryDataStoreTO)) { + continue; + } + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore; + boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType() || + StoragePoolType.CLVM_NG == primaryStore.getPoolType(); + LOGGER.debug("Disk {} identified as CLVM/CLVM_NG={} via VirtualMachineTO pool type: {}", + diskPath, isClvm, primaryStore.getPoolType()); + return isClvm; + } + } + + if (diskPath.startsWith("/dev/") && !diskPath.contains("/dev/mapper/")) { + String vgName = extractVolumeGroupFromPath(diskPath); + if (vgName != null) { + boolean isClustered = checkIfVolumeGroupIsClustered(vgName); + LOGGER.debug("Disk {} VG {} identified as clustered={} via vgs attribute check", + diskPath, vgName, isClustered); + return isClustered; + } + } + + } catch (Exception e) { + LOGGER.error("Error determining if volume {} is CLVM: {}", diskPath, e.getMessage(), e); + } + + return false; + } + + /** + * Extracts the volume group name from a device path. + * + * @param devicePath The device path (e.g., /dev/vgname/lvname) + * @return The volume group name, or null if cannot be determined + */ + static String extractVolumeGroupFromPath(String devicePath) { + if (devicePath == null || !devicePath.startsWith("/dev/")) { + return null; + } + + // Format: /dev// + String[] parts = devicePath.split("/"); + if (parts.length >= 3) { + return parts[2]; // ["", "dev", "vgname", ...] + } + + return null; + } + + /** + * Checks if a volume group is clustered (CLVM) by examining its attributes. + * Uses 'vgs' command to check for the clustered/shared flag in VG attributes. + * + * VG Attr format (6 characters): wz--nc or wz--ns + * Position 6: Clustered flag - 'c' = CLVM (clustered), 's' = shared (lvmlockd), '-' = not clustered + * + * @param vgName The volume group name + * @return true if the VG is clustered or shared, false otherwise + */ + static boolean checkIfVolumeGroupIsClustered(String vgName) { + if (vgName == null) { + return false; + } + + try { + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + Script vgsCmd = new Script("vgs", 30000, LOGGER); + vgsCmd.add("--noheadings"); + vgsCmd.add("--unbuffered"); + vgsCmd.add("-o"); + vgsCmd.add("vg_attr"); + vgsCmd.add(vgName); + + String result = vgsCmd.execute(parser); + + if (result == null && parser.getLines() != null) { + String output = parser.getLines(); + if (output != null && !output.isEmpty()) { + String vgAttr = output.trim(); + if (vgAttr.length() >= 6) { + char clusterFlag = vgAttr.charAt(5); // Position 6 (0-indexed 5) + boolean isClustered = (clusterFlag == 'c' || clusterFlag == 's'); + LOGGER.debug("VG {} has attributes '{}', cluster/shared flag '{}' = {}", + vgName, vgAttr, clusterFlag, isClustered); + return isClustered; + } else { + LOGGER.warn("VG {} attributes '{}' have unexpected format (expected 6+ chars)", vgName, vgAttr); + } + } + } else { + LOGGER.warn("Failed to get VG attributes for {}: {}", vgName, result); + } + + } catch (Exception e) { + LOGGER.debug("Error checking if VG {} is clustered: {}", vgName, e.getMessage()); + } + + return false; + } + + public Map createDiskOnlyVmSnapshotForRunningVm(List> volumeTosAndNewPaths, String vmName, String snapshotName, + boolean quiesceVm) throws BackupException { + logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName); + + Domain dm = null; + try { + LibvirtUtilitiesHelper libvirtUtilitiesHelper = getLibvirtUtilitiesHelper(); + Connect conn = libvirtUtilitiesHelper.getConnection(); + List disks = getDisks(conn, vmName); + + dm = getDomain(conn, vmName); + + if (dm == null) { + throw new BackupException(String.format("Creation of disk-only VM snapshot failed as we could not find the VM [%s].", vmName), true); + } + + Pair> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeTosAndNewPaths, disks, snapshotName); + + int flagsToUseForRunningVmSnapshotCreation = getFlagsToUseForRunningVmSnapshotCreation(quiesceVm); + String snapshotXml = snapshotXmlAndVolumeToNewPathMap.first(); + + logger.info("Creating disk-only VM snapshot for VM [{}] using parameters: snapshotXml [{}]; flags [{}].", vmName, snapshotXml, flagsToUseForRunningVmSnapshotCreation); + + dm.snapshotCreateXML(snapshotXml, flagsToUseForRunningVmSnapshotCreation); + + return snapshotXmlAndVolumeToNewPathMap.second(); + } catch (LibvirtException e) { + String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()); + boolean isVmConsistent = false; + if (e.getMessage().contains(AGENT_IS_NOT_CONNECTED)) { + errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" + + " guest agent installed; thus the QuiesceVM parameter is not supported."; + isVmConsistent = true; + } + logger.error(errorMsg, e); + throw new BackupException(errorMsg, isVmConsistent); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException l) { + logger.trace("Ignoring Libvirt error.", l); + } + } + } + } + + public Map createDiskOnlyVMSnapshotOfStoppedVm(List> volumeTosAndNewPaths, String vmName) { + logger.info("Creating volume deltas for stopped VM [{}].", vmName); + + Map mapVolumeToSnapshotSize = new HashMap<>(); + try { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + + String snapshotPath = volumeObjectTOAndNewPath.second(); + String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath); + QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(0); + + logger.debug("Creating new delta [{}] for volume [{}] as part of the delta creation process for VM [{}].", newDelta, volumeObjectTO.getUuid(), vmName); + qemuImg.create(newDelta, currentDelta); + + mapVolumeToSnapshotSize.put(volumeObjectTO.getUuid(), getFileSize(currentDeltaFullPath)); + } + } catch (Exception e) { + logger.error("Exception while creating volume delta for VM [{}]. Deleting leftover deltas.", vmName, e); + cleanupLeftoverDeltas(volumeTosAndNewPaths, mapVolumeToSnapshotSize); + throw new BackupException(String.format("An exception was caught during the delta creation for VM [%s]. The leftover deltas have been deleted.", vmName), true); + } + + return mapVolumeToSnapshotSize; + } + + protected void cleanupLeftoverDeltas(List> volumeTosAndNewPaths, Map mapVolumeToSnapshotSize) { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + Long volSize = mapVolumeToSnapshotSize.get(volumeObjectTO.getUuid()); + if (volSize == null) { + continue; + } + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + try { + Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTOAndNewPath.second()))); + } catch (IOException ex) { + logger.warn("Tried to delete leftover delta at [{}]. Failed.", volumeObjectTOAndNewPath.second(), ex); + } + } + } + + public void mergeDeltaForStoppedVm(DeltaMergeTreeTO deltaMergeTreeTO) throws QemuImgException, IOException, LibvirtException { + logger.debug("Merging delta [{}] for stopped VM.", deltaMergeTreeTO); + + QemuImg qemuImg = new QemuImg(qcow2DeltaMergeTimeout * 1000); + DataTO parentTo = deltaMergeTreeTO.getParent(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) parentTo.getDataStore(); + KVMStoragePool storagePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String childLocalPath = storagePool.getLocalPathFor(deltaMergeTreeTO.getChild().getPath()); + + QemuImgFile parent = new QemuImgFile(storagePool.getLocalPathFor(parentTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile child = new QemuImgFile(childLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Committing child delta [{}] into parent delta [{}].", parentTo, deltaMergeTreeTO.getChild()); + qemuImg.commit(child, parent, true); + + List grandChildren = deltaMergeTreeTO.getGrandChildren().stream() + .map(deltaTo -> new QemuImgFile(storagePool.getLocalPathFor(deltaTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) + .collect(Collectors.toList()); + + logger.debug("Rebasing grand-children [{}] into parent at [{}].", grandChildren, parent.getFileName()); + for (QemuImgFile grandChild : grandChildren) { + qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); + } + + logger.debug("Deleting child at [{}] as it is useless.", childLocalPath); + + Files.deleteIfExists(Path.of(childLocalPath)); + } + + public void mergeDeltaForRunningVm(DeltaMergeTreeTO mergeTreeTO, String vmName, VolumeObjectTO volumeObjectTO) throws LibvirtException, QemuImgException { + logger.debug("Merging delta [{}] for running VM [{}].", mergeTreeTO, vmName); + + QemuImg qemuImg = new QemuImg(qcow2DeltaMergeTimeout * 1000); + Connect conn = libvirtUtilitiesHelper.getConnection(); + Domain domain = getDomain(conn, vmName); + List disks = getDisks(conn, vmName); + + DataTO childTO = mergeTreeTO.getChild(); + DataTO parentSnapshotTO = mergeTreeTO.getParent(); + KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(volumeObjectTO, storagePoolManager); + + boolean active = DataObjectType.VOLUME.equals(childTO.getObjectType()); + String label = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO).getDiskLabel(); + String parentSnapshotLocalPath = storagePool.getLocalPathFor(parentSnapshotTO.getPath()); + String childDeltaPath = storagePool.getLocalPathFor(childTO.getPath()); + + logger.debug("Found label [{}] for [{}]. Will merge delta at [{}] into delta at [{}].", label, volumeObjectTO, parentSnapshotLocalPath, childDeltaPath); + + mergeDeltaIntoBaseFile(domain, label, parentSnapshotLocalPath, childDeltaPath, active, childTO.getPath(), volumeObjectTO, conn); + + QemuImgFile parent = new QemuImgFile(parentSnapshotLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Rebasing grand-children [{}] into parent at [{}].", mergeTreeTO.getGrandChildren(), parentSnapshotLocalPath); + for (DataTO grandChildTo : mergeTreeTO.getGrandChildren()) { + if (checkIfFileIsInActiveChainForVm(domain, grandChildTo)) { + logger.debug("Grand-child [{}] is on the active chain of VM [{}], thus Libvirt has already rebased it, will ignore it.", grandChildTo, vmName); + continue; + } + QemuImgFile grandChild = new QemuImgFile(storagePool.getLocalPathFor(grandChildTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); + } + } + + private boolean checkIfFileIsInActiveChainForVm(Domain vm, DataTO dataTO) throws LibvirtException { + String xml = vm.getXMLDesc(0); + KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(dataTO, storagePoolManager); + return xml.contains(storagePool.getLocalPathFor(dataTO.getPath())); + } + + public int getFlagsToUseForRunningVmSnapshotCreation(boolean quiesceVm) { + int flags = quiesceVm ? Domain.SnapshotCreateFlags.QUIESCE : 0; + flags += Domain.SnapshotCreateFlags.DISK_ONLY + + Domain.SnapshotCreateFlags.ATOMIC + + Domain.SnapshotCreateFlags.NO_METADATA; + return flags; + } + + public Pair> createSnapshotXmlAndNewVolumePathMap(List> volumeTosAndNewPaths, List disks, String snapshotName) { + StringBuilder stringBuilder = new StringBuilder(); + Map volumeObjectToNewPathMap = new HashMap<>(); + + for (Pair volumeObjectTOAndPath : volumeTosAndNewPaths) { + LibvirtVMDef.DiskDef diskdef = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTOAndPath.first()); + String newPath = volumeObjectTOAndPath.second(); + stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath))); + + long snapSize = getFileSize(diskdef.getDiskPath()); + + volumeObjectToNewPathMap.put(volumeObjectTOAndPath.first().getUuid(), snapSize); + } + + String snapshotXml = String.format(SNAPSHOT_XML, snapshotName, stringBuilder); + return new Pair<>(snapshotXml, volumeObjectToNewPathMap); + } + + public long getFileSize(String path) { + return new File(path).length(); + } + + public boolean pullVolumeBackingFile(VolumeObjectTO volumeObjectTO, String vmName) throws LibvirtException { + Connect conn = libvirtUtilitiesHelper.getConnection(); + + Domain vm = getDomain(conn, vmName); + List disks = getDisks(conn, vmName); + DiskDef diskDef = getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO); + + String diskLabel = diskDef.getDiskLabel(); + Script.runSimpleBashScript(String.format(BLOCK_PULL_COMMAND, vmName, diskLabel)); + + boolean result = checkBlockPullProgress(vm, diskLabel, vmName, volumeObjectTO.getUuid()); + + if (!result) { + logger.warn("Failed to block pull volume [{}] of VM [{}], aborting.", volumeObjectTO, vmName); + vm.blockJobAbort(diskLabel, Domain.BlockJobAbortFlags.ASYNC); + } + return result; + } + + protected Boolean checkBlockPullProgress(Domain vm, String diskLabel, String vmName, String volumeUuid) { + int timeout = qcow2DeltaMergeTimeout; + DomainBlockJobInfo result; + long lastCommittedBytes = 0; + long endBytes = 0; + String partialLog = String.format("for volume [%s] of VM [%s]", volumeUuid, vmName); + while (timeout > 0) { + timeout -= 1; + + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + logger.trace("Thread that was tracking the block pull progress {} was interrupted. Ignoring.", partialLog, ex); + continue; + } + + try { + result = vm.getBlockJobInfo(diskLabel, 0); + } catch (LibvirtException ex) { + logger.warn("Exception while getting block job info {}: [{}].", partialLog, ex.getMessage(), ex); + return false; + } + + if (result == null || result.type == 0 && result.end == 0 && result.cur == 0) { + logger.debug("Block pull job {} has finished.", partialLog); + return true; + } + + long currentCommittedBytes = result.cur; + if (currentCommittedBytes > lastCommittedBytes) { + logger.debug("The block pull {} is at [{}] of [{}].", partialLog, currentCommittedBytes, result.end); + } + lastCommittedBytes = currentCommittedBytes; + endBytes = result.end; + } + logger.warn(String.format("Block pull %s has timed out after waiting at least %s seconds. The progress of the operation was [%s] of [%s].", partialLog, + qcow2DeltaMergeTimeout, lastCommittedBytes, endBytes)); + return false; + } + + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java index 4d823783a99a..1a4d23af1c90 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java @@ -131,6 +131,7 @@ public boolean parseDomainXML(String domXML) { fmt = DiskDef.DiskFmtType.valueOf(diskFmtType.toUpperCase()); } def.defFileBasedDisk(diskFile, diskLabel, DiskDef.DiskBus.valueOf(bus.toUpperCase()), fmt); + parseBackingFiles(disk, def); } else if (device.equalsIgnoreCase("cdrom")) { def.defISODisk(diskFile, i+1, diskLabel, DiskDef.DiskType.FILE); } @@ -405,6 +406,21 @@ public boolean parseDomainXML(String domXML) { return false; } + private void parseBackingFiles(Element disk, DiskDef def) { + NodeList backingStoreNodeList = disk.getElementsByTagName("backingStore"); + List backingStoreList = new ArrayList<>(); + while (backingStoreNodeList.getLength() > 0) { + Element backingStore = (Element)backingStoreNodeList.item(0); + String path = getAttrValue("source", "file", backingStore); + if (StringUtils.isEmpty(path)) { + break; + } + backingStoreList.add(path.substring(path.lastIndexOf(File.separator))+1); + backingStoreNodeList = backingStore.getElementsByTagName("backingStore"); + } + def.setBackingStoreList(backingStoreList); + } + /** * Parse the memballoon tag. * @param devices the devices tag. @@ -542,7 +558,7 @@ private void extractCpuTuneDef(final Element rootElement) { final String quota = getTagValue("quota", cpuTuneDefElement); if (StringUtils.isNotBlank(quota)) { - cpuTuneDef.setQuota((Integer.parseInt(quota))); + cpuTuneDef.setQuota((Long.parseLong(quota))); } final String period = getTagValue("period", cpuTuneDefElement); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDef.java index 08086859fb70..d3765c01ccbc 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDef.java @@ -79,28 +79,47 @@ private void generatePciXml(StringBuilder gpuBuilder) { gpuBuilder.append(" \n"); gpuBuilder.append(" \n"); - // Parse the bus address (e.g., 00:02.0) into domain, bus, slot, function - String domain = "0x0000"; - String bus = "0x00"; - String slot = "0x00"; - String function = "0x0"; + // Parse the bus address into domain, bus, slot, function. Two input formats are accepted: + // - "dddd:bb:ss.f" full PCI address with domain (e.g. 0000:00:02.0) + // - "bb:ss.f" legacy short BDF; domain defaults to 0000 + // Each segment is parsed as a hex integer and formatted with fixed widths + // (domain: 4 hex digits, bus/slot: 2 hex digits, function: 1 hex digit) to + // produce canonical libvirt XML values regardless of input casing or padding. + int domainVal = 0, busVal = 0, slotVal = 0, funcVal = 0; if (busAddress != null && !busAddress.isEmpty()) { String[] parts = busAddress.split(":"); - if (parts.length > 1) { - bus = "0x" + parts[0]; - String[] slotFunctionParts = parts[1].split("\\."); - if (slotFunctionParts.length > 0) { - slot = "0x" + slotFunctionParts[0]; - if (slotFunctionParts.length > 1) { - function = "0x" + slotFunctionParts[1].trim(); - } + try { + String slotFunction; + if (parts.length == 3) { + domainVal = Integer.parseInt(parts[0], 16); + busVal = Integer.parseInt(parts[1], 16); + slotFunction = parts[2]; + } else if (parts.length == 2) { + busVal = Integer.parseInt(parts[0], 16); + slotFunction = parts[1]; + } else { + throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'"); } + String[] sf = slotFunction.split("\\."); + if (sf.length == 2) { + slotVal = Integer.parseInt(sf[0], 16); + funcVal = Integer.parseInt(sf[1].trim(), 16); + } else { + throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid PCI bus address format: '" + busAddress + "'", e); } } + String domain = String.format("0x%04x", domainVal); + String bus = String.format("0x%02x", busVal); + String slot = String.format("0x%02x", slotVal); + String function = String.format("0x%x", funcVal); + gpuBuilder.append("

    \n"); + .append(slot).append("' function='").append(function).append("'/>\n"); gpuBuilder.append(" \n"); gpuBuilder.append("\n"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java new file mode 100644 index 000000000000..a3dd2040cfc8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtMigrateResourceBetweenSecondaryStorages.java @@ -0,0 +1,123 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.resource.CommandWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +public abstract class LibvirtMigrateResourceBetweenSecondaryStorages extends CommandWrapper { + + protected static final String BACKUP = "backup"; + protected static final String SNAPSHOT = "snapshot"; + + protected Set filesToRemove; + protected List> resourcesToUpdate; + protected String resourceType; + protected int wait; + + public String copyResourceToDestDataStore(DataTO resource, String resourceCurrentPath, KVMStoragePool destImagePool, String resourceParentPath) throws QemuImgException, LibvirtException { + String resourceDestDataStoreFullPath = destImagePool.getLocalPathFor(resource.getPath()); + String resourceDestCheckpointPath = resourceDestDataStoreFullPath.replace("snapshots", "checkpoints"); + + QemuImgFile resourceOrigin = new QemuImgFile(resourceCurrentPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile resourceDestination = new QemuImgFile(resourceDestDataStoreFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentResource = null; + + if (resourceParentPath != null) { + parentResource = new QemuImgFile(resourceParentPath, QemuImg.PhysicalDiskFormat.QCOW2); + } + + logger.debug("Migrating {} [{}] to [{}] with {}", resourceType, resourceOrigin, resourceDestination, parentResource == null ? "no parent." : String.format("parent [%s].", parentResource)); + + long resourceId = resource.getId(); + + createDirsIfNeeded(resourceDestDataStoreFullPath, resourceId); + + QemuImg qemuImg = new QemuImg(wait); + qemuImg.convert(resourceOrigin, resourceDestination, parentResource, null, null, new QemuImageOptions(resourceOrigin.getFormat(), resourceOrigin.getFileName(), null), + null, true, false, false, false, null, null); + + filesToRemove.add(resourceCurrentPath); + + if (SNAPSHOT.equals(resourceType)) { + String resourceCurrentCheckpointPath = resourceCurrentPath.replace("snapshots", "checkpoints"); + createDirsIfNeeded(resourceDestCheckpointPath, resourceId); + migrateCheckpointFile(resourceCurrentPath, resourceDestDataStoreFullPath); + filesToRemove.add(resourceCurrentCheckpointPath); + resourcesToUpdate.add(new Pair<>(resourceId, resourceDestCheckpointPath)); + } + + return resourceDestDataStoreFullPath; + } + + private void migrateCheckpointFile(String resourceCurrentPath, String resourceDestDataStoreFullPath) { + resourceCurrentPath = resourceCurrentPath.replace("snapshots", "checkpoints"); + resourceDestDataStoreFullPath = resourceDestDataStoreFullPath.replace("snapshots", "checkpoints"); + + String copyCommand = String.format("cp %s %s", resourceCurrentPath, resourceDestDataStoreFullPath); + Script.runSimpleBashScript(copyCommand); + } + + public void removeResourceFromSourceDataStore(String resourcePath) { + logger.debug("Removing file [{}].", resourcePath); + try { + Files.deleteIfExists(Path.of(resourcePath)); + } catch (IOException ex) { + logger.error("Failed to remove {} [{}].", resourceType, resourcePath, ex); + } + } + + public String rebaseResourceToNewParentPath(String resourcePath, String parentResourcePath) throws LibvirtException, QemuImgException { + QemuImgFile resource = new QemuImgFile(resourcePath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentResource = new QemuImgFile(parentResourcePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(wait); + qemuImg.rebase(resource, parentResource, parentResource.getFormat().toString(), false); + + return resourcePath; + } + + private void createDirsIfNeeded(String resourceFullPath, Long resourceId) { + String dirs = resourceFullPath.substring(0, resourceFullPath.lastIndexOf(File.separator)); + try { + Files.createDirectories(Path.of(dirs)); + } catch (IOException e) { + throw new BackupException(String.format("Error while creating directories for migration of %s [%s].", resourceType, resourceId), e, true); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java index 1b6f73039ca5..00126a07cd3a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStorageVolumeXMLParser.java @@ -61,6 +61,26 @@ public LibvirtStorageVolumeDef parseStorageVolumeXML(String volXML) { return null; } + public String getBackingFileNameIfExists(String volXML) { + try { + DocumentBuilder builder = ParserUtils.getSaferDocumentBuilderFactory().newDocumentBuilder(); + + InputSource is = new InputSource(); + is.setCharacterStream(new StringReader(volXML)); + Document doc = builder.parse(is); + + Element rootElement = doc.getDocumentElement(); + Element backingStore = (Element)rootElement.getElementsByTagName("backingStore").item(0); + if (backingStore != null) { + String[] paths = getTagValue("path", backingStore).split("/"); + return paths[paths.length-1]; + } + } catch (ParserConfigurationException | SAXException | IOException e) { + logger.error(e.toString(), e); + } + return null; + } + private static String getTagValue(String tag, Element eElement) { NodeList nlList = eElement.getElementsByTagName(tag).item(0).getChildNodes(); Node nValue = nlList.item(0); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 097a9b8dd322..74529d9d5fa2 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -57,6 +57,10 @@ public class LibvirtVMDef { protected static Logger LOGGER = LogManager.getLogger(LibvirtVMDef.class); + // CD-ROM slot allocation: getDevLabel() maps deviceSeq=3,4 to hdc and hdd on the IDE bus. + // Bumping this requires extending getDevLabel() (e.g. to spill onto SATA or a second IDE controller). + public static final int MAX_CDROMS_PER_VM = 2; + private String _hvsType; private static long s_libvirtVersion; private static long s_qemuVersion; @@ -443,15 +447,15 @@ public String toString() { } public static class GuestResourceDef { - private long memory; + private long maxMemory; private long currentMemory = -1; private int vcpu = -1; private int maxVcpu = -1; private boolean memoryBalloning = false; private int memoryBalloonStatsPeriod = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VM_MEMBALLOON_STATS_PERIOD); - public void setMemorySize(long mem) { - this.memory = mem; + public void setMaxMemory(long mem) { + this.maxMemory = mem; } public void setCurrentMem(long currMem) { @@ -484,8 +488,8 @@ public String toString() { response.append(String.format("%s\n", this.currentMemory)); response.append(String.format("%s\n", this.currentMemory)); - if (this.memory > this.currentMemory) { - response.append(String.format("%s\n", this.memory)); + if (this.maxMemory > this.currentMemory) { + response.append(String.format("%s\n", this.maxMemory)); response.append(String.format(" \n", this.maxVcpu - 1, this.currentMemory)); } @@ -971,6 +975,7 @@ public String toString() { private BlockIOSize logicalBlockIOSize = null; private BlockIOSize physicalBlockIOSize = null; private DiskGeometry geometry = null; + private List backingStoreList = null; // Ordered list of backing stores, the first in the list is the immediate backing store, and the last in the list is the base public DiscardType getDiscard() { return _discard; @@ -1342,6 +1347,14 @@ public String getSourcePath() { return _sourcePath; } + public List getBackingStoreList() { + return backingStoreList; + } + + public void setBackingStoreList(List backingStoreList) { + this.backingStoreList = backingStoreList; + } + @Override public String toString() { StringBuilder diskBuilder = new StringBuilder(); @@ -1920,11 +1933,12 @@ public String toString() { public static class CpuTuneDef { private int _shares = 0; - private int quota = 0; + private long quota = 0; private int period = 0; static final int DEFAULT_PERIOD = 10000; static final int MIN_QUOTA = 1000; static final int MAX_PERIOD = 1000000; + public static final long MAX_CPU_QUOTA = 17592186044415L; public void setShares(int shares) { _shares = shares; @@ -1934,11 +1948,11 @@ public int getShares() { return _shares; } - public int getQuota() { + public long getQuota() { return quota; } - public void setQuota(int quota) { + public void setQuota(long quota) { this.quota = quota; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java new file mode 100644 index 000000000000..dc34a4cb62d8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtBaseConvertCommandWrapper.java @@ -0,0 +1,283 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ServerResource; +import com.cloud.storage.Storage; +import com.cloud.utils.FileUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public abstract class LibvirtBaseConvertCommandWrapper extends CommandWrapper { + + protected KVMStoragePool getTemporaryStoragePool(DataStoreTO conversionTemporaryLocation, KVMStoragePoolManager storagePoolMgr) { + if (conversionTemporaryLocation instanceof NfsTO) { + NfsTO nfsTO = (NfsTO) conversionTemporaryLocation; + return storagePoolMgr.getStoragePoolByURI(nfsTO.getUrl()); + } else { + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) conversionTemporaryLocation; + return storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + } + } + + protected List getTemporaryDisksFromParsedXml(KVMStoragePool pool, LibvirtDomainXMLParser xmlParser, + String convertedBasePath, + String conversionPoolPath, String vmVolumesPrefix) { + List disksDefs = xmlParser.getDisks(); + disksDefs = disksDefs.stream().filter(x -> x.getDiskType() == LibvirtVMDef.DiskDef.DiskType.FILE && + x.getDeviceType() == LibvirtVMDef.DiskDef.DeviceType.DISK).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(disksDefs)) { + String err = String.format("Cannot find any disk defined on the converted XML domain %s.xml, " + + "checking disks at: %s with prefix: %s", convertedBasePath, conversionPoolPath, vmVolumesPrefix); + logger.warn(err); + return getTemporaryDisksWithPrefixFromTemporaryPool(pool, conversionPoolPath, vmVolumesPrefix); + } + sanitizeDisksPath(disksDefs); + return getPhysicalDisksFromDefPaths(disksDefs, pool); + } + + private List getPhysicalDisksFromDefPaths(List disksDefs, KVMStoragePool pool) { + List disks = new ArrayList<>(); + for (LibvirtVMDef.DiskDef diskDef : disksDefs) { + KVMPhysicalDisk physicalDisk = pool.getPhysicalDisk(diskDef.getDiskPath()); + disks.add(physicalDisk); + } + return disks; + } + + protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVMStoragePool pool, String path, String prefix) { + String msg = String.format("Could not parse correctly the converted XML domain, checking for disks on %s with prefix %s", path, prefix); + logger.info(msg); + pool.refresh(); + List disksWithPrefix = pool.listPhysicalDisks() + .stream() + .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(disksWithPrefix)) { + msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); + logger.error(msg); + throw new CloudRuntimeException(msg); + } + return disksWithPrefix; + } + + protected void cleanupDisksAndDomainFromTemporaryLocation(List disks, + KVMStoragePool temporaryStoragePool, + String temporaryConvertUuid, boolean xmlExists) { + for (KVMPhysicalDisk disk : disks) { + logger.info(String.format("Cleaning up temporary disk %s after conversion from temporary location", disk.getName())); + temporaryStoragePool.deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); + } + if (xmlExists) { + logger.info(String.format("Cleaning up temporary domain %s after conversion from temporary location", temporaryConvertUuid)); + FileUtil.deleteFiles(temporaryStoragePool.getLocalPath(), temporaryConvertUuid, ".xml"); + } + } + + protected void sanitizeDisksPath(List disks) { + for (LibvirtVMDef.DiskDef disk : disks) { + String[] diskPathParts = disk.getDiskPath().split("/"); + String relativePath = diskPathParts[diskPathParts.length - 1]; + disk.setDiskPath(relativePath); + } + } + + protected List moveTemporaryDisksToDestination(List temporaryDisks, + List destinationStoragePools, + KVMStoragePoolManager storagePoolMgr) { + List targetDisks = new ArrayList<>(); + if (temporaryDisks.size() != destinationStoragePools.size()) { + String warn = String.format("Discrepancy between the converted instance disks (%s) " + + "and the expected number of disks (%s)", temporaryDisks.size(), destinationStoragePools.size()); + logger.warn(warn); + } + for (int i = 0; i < temporaryDisks.size(); i++) { + String poolPath = destinationStoragePools.get(i); + KVMStoragePool destinationPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, poolPath); + if (destinationPool == null) { + String err = String.format("Could not find a storage pool by URI: %s", poolPath); + logger.error(err); + continue; + } + if (destinationPool.getType() != Storage.StoragePoolType.NetworkFilesystem) { + String err = String.format("Storage pool by URI: %s is not an NFS storage", poolPath); + logger.error(err); + continue; + } + KVMPhysicalDisk sourceDisk = temporaryDisks.get(i); + if (logger.isDebugEnabled()) { + String msg = String.format("Trying to copy converted instance disk number %s from the temporary location %s" + + " to destination storage pool %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid()); + logger.debug(msg); + } + + String destinationName = UUID.randomUUID().toString(); + + try { + if (destinationPool.getAvailable() < sourceDisk.getSize()) { + String msg = String.format("Not enough space on destination pool %s (%s bytes) to copy disk %s (size %s)", + destinationPool.getUuid(), destinationPool.getAvailable(), sourceDisk.getName(), sourceDisk.getSize()); + logger.error(msg); + throw new CloudRuntimeException(msg); + } + + KVMPhysicalDisk destinationDisk = storagePoolMgr.copyPhysicalDisk(sourceDisk, destinationName, destinationPool, 7200 * 1000); + targetDisks.add(destinationDisk); + } catch (Exception e) { + String err = String.format("Error copying converted instance disk number %s from the temporary location %s" + + " to destination storage pool %s: %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid(), e.getMessage()); + logger.error(err, e); + cleanupMovedDisksOnDestinationPool(targetDisks); + return null; + } + } + return targetDisks; + } + + private void cleanupMovedDisksOnDestinationPool(List targetDisks) { + if (CollectionUtils.isEmpty(targetDisks)) { + return; + } + for (KVMPhysicalDisk disk : targetDisks) { + logger.info(String.format("Cleaning up disk %s from pool %s after conversion", disk.getName(), disk.getPool().getUuid())); + disk.getPool().deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); + } + } + + protected UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, + List vmDisks, + LibvirtDomainXMLParser xmlParser) { + UnmanagedInstanceTO instanceTO = new UnmanagedInstanceTO(); + instanceTO.setName(baseName); + instanceTO.setDisks(getUnmanagedInstanceDisks(vmDisks, xmlParser)); + instanceTO.setNics(getUnmanagedInstanceNics(xmlParser)); + return instanceTO; + } + + private List getUnmanagedInstanceNics(LibvirtDomainXMLParser xmlParser) { + List nics = new ArrayList<>(); + if (xmlParser != null) { + List interfaces = xmlParser.getInterfaces(); + for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { + UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); + nic.setMacAddress(interfaceDef.getMacAddress()); + nic.setNicId(interfaceDef.getBrName()); + nic.setAdapterType(interfaceDef.getModel().toString()); + nics.add(nic); + } + } + return nics; + } + + protected List getUnmanagedInstanceDisks(List vmDisks, LibvirtDomainXMLParser xmlParser) { + List instanceDisks = new ArrayList<>(); + List diskDefs = xmlParser != null ? xmlParser.getDisks() : null; + for (int i = 0; i< vmDisks.size(); i++) { + KVMPhysicalDisk physicalDisk = vmDisks.get(i); + KVMStoragePool storagePool = physicalDisk.getPool(); + UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); + disk.setPosition(i); + Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); + disk.setDatastoreHost(storagePoolHostAndPath.first()); + disk.setDatastorePath(storagePoolHostAndPath.second()); + disk.setDatastoreName(storagePool.getUuid()); + disk.setDatastoreType(storagePool.getType().name()); + disk.setCapacity(physicalDisk.getVirtualSize()); + disk.setFileBaseName(physicalDisk.getName()); + if (CollectionUtils.isNotEmpty(diskDefs)) { + LibvirtVMDef.DiskDef diskDef = diskDefs.get(i); + disk.setController(diskDef.getBusType() != null ? diskDef.getBusType().toString() : LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); + } else { + // If the job is finished but we cannot parse the XML, the guest VM can use the virtio driver + disk.setController(LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); + } + instanceDisks.add(disk); + } + return instanceDisks; + } + + protected Pair getNfsStoragePoolHostAndPath(KVMStoragePool storagePool) { + String sourceHostIp = null; + String sourcePath = null; + List commands = new ArrayList<>(); + commands.add(new String[]{Script.getExecutableAbsolutePath("mount")}); + commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), storagePool.getLocalPath()}); + String storagePoolMountPoint = Script.executePipedCommands(commands, 0).second(); + logger.debug(String.format("NFS Storage pool: %s - local path: %s, mount point: %s", storagePool.getUuid(), storagePool.getLocalPath(), storagePoolMountPoint)); + if (StringUtils.isNotEmpty(storagePoolMountPoint)) { + String[] res = storagePoolMountPoint.strip().split(" "); + res = res[0].split(":"); + if (res.length > 1) { + sourceHostIp = res[0].strip(); + sourcePath = res[1].strip(); + } + } + return new Pair<>(sourceHostIp, sourcePath); + } + + protected LibvirtDomainXMLParser parseMigratedVMXmlDomain(String installPath) throws IOException { + String xmlPath = String.format("%s.xml", installPath); + if (!new File(xmlPath).exists()) { + String err = String.format("Conversion failed. Unable to find the converted XML domain, expected %s", xmlPath); + logger.error(err); + throw new CloudRuntimeException(err); + } + String xml; + try (InputStream is = new BufferedInputStream(new FileInputStream(xmlPath))) { + xml = IOUtils.toString(is, Charset.defaultCharset()); + } + final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + try { + parser.parseDomainXML(xml); + return parser; + } catch (RuntimeException e) { + String err = String.format("Error parsing the converted instance XML domain at %s: %s", xmlPath, e.getMessage()); + logger.error(err, e); + logger.debug(xml); + return null; + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java index b94b48300036..de9341715f02 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapper.java @@ -30,7 +30,15 @@ public class LibvirtCheckConvertInstanceCommandWrapper extends CommandWrapper pools = monitor.getStoragePools(); final HostTO host = command.getHost(); - final KVMHAChecker ha = new KVMHAChecker(pools, host, command.isCheckFailedOnOneStorage()); + + final KVMHAChecker ha = new KVMHAChecker(pools, host, command.shouldReportIfHeartBeatFailedForOneStoragePool()); final Future future = executors.submit(ha); try { - final Boolean result = future.get(); - if (result) { - return new Answer(command, false, "Heart is beating..."); + final Boolean hasHeartBeat = future.get(); + if (hasHeartBeat) { + return new CheckOnHostAnswer(command, true, "Heart is beating"); } else { - return new Answer(command); + return new CheckOnHostAnswer(command, false, "Heart is not beating"); } } catch (final InterruptedException e) { - return new Answer(command, false, "CheckOnHostCommand: can't get status of host: InterruptedException"); + return new CheckOnHostAnswer(command, "CheckOnHostCommand: can't get status of host: InterruptedException"); } catch (final ExecutionException e) { - return new Answer(command, false, "CheckOnHostCommand: can't get status of host: ExecutionException"); + return new CheckOnHostAnswer(command, "CheckOnHostCommand: can't get status of host: ExecutionException"); } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVMActivityOnStoragePoolCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVMActivityOnStoragePoolCommandWrapper.java index a708d441be59..b132f6d98ce9 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVMActivityOnStoragePoolCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVMActivityOnStoragePoolCommandWrapper.java @@ -48,9 +48,9 @@ public Answer execute(final CheckVMActivityOnStoragePoolCommand command, final L KVMStoragePool primaryPool = storagePoolMgr.getStoragePool(pool.getType(), pool.getUuid()); - if (primaryPool.isPoolSupportHA()){ - final HAStoragePool nfspool = monitor.getStoragePool(pool.getUuid()); - final KVMHAVMActivityChecker ha = new KVMHAVMActivityChecker(nfspool, command.getHost(), command.getVolumeList(), libvirtComputingResource.getVmActivityCheckPath(), command.getSuspectTimeInSeconds()); + if (primaryPool.isPoolSupportHA()) { + final HAStoragePool haPool = monitor.getStoragePool(pool.getUuid()); + final KVMHAVMActivityChecker ha = new KVMHAVMActivityChecker(haPool, command.getHost(), command.getVolumeList(), libvirtComputingResource.getVmActivityCheckPath(), command.getSuspectTimeInSeconds()); final Future future = executors.submit(ha); try { final Boolean result = future.get(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapper.java index 99005755cbbe..aa6a37b6561d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapper.java @@ -45,11 +45,10 @@ public Answer execute(final CheckVirtualMachineCommand command, final LibvirtCom Integer vncPort = null; if (state == PowerState.PowerOn) { vncPort = libvirtComputingResource.getVncPort(conn, command.getVmName()); - } - - Domain vm = conn.domainLookupByName(command.getVmName()); - if (state == PowerState.PowerOn && DomainInfo.DomainState.VIR_DOMAIN_PAUSED.equals(vm.getInfo().state)) { - return new CheckVirtualMachineAnswer(command, PowerState.PowerUnknown, vncPort); + Domain vm = conn.domainLookupByName(command.getVmName()); + if (DomainInfo.DomainState.VIR_DOMAIN_PAUSED.equals(vm.getInfo().state)) { + return new CheckVirtualMachineAnswer(command, PowerState.PowerUnknown, vncPort); + } } return new CheckVirtualMachineAnswer(command, state, vncPort); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java index 55225ba086c3..6788516df741 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java @@ -47,7 +47,10 @@ @ResourceWrapper(handles = CheckVolumeCommand.class) public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper { - private static final List STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem); + private static final List STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList( + Storage.StoragePoolType.Filesystem, + Storage.StoragePoolType.NetworkFilesystem, + Storage.StoragePoolType.SharedMountPoint); @Override public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java new file mode 100644 index 000000000000..05afd8b2234d --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupConvertedInstanceDisksCommandWrapper.java @@ -0,0 +1,67 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CleanupConvertedInstanceDisksCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.ResourceWrapper; + +import java.io.File; +import java.util.List; + +@ResourceWrapper(handles = CleanupConvertedInstanceDisksCommand.class) +public class LibvirtCleanupConvertedInstanceDisksCommandWrapper extends LibvirtBaseConvertCommandWrapper { + + @Override + public Answer execute(CleanupConvertedInstanceDisksCommand command, LibvirtComputingResource serverResource) { + DataStoreTO vmVolumesStore = command.getVmVolumesStore(); + String vmVolumesPrefix = command.getVmVolumesPrefix(); + + final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + KVMStoragePool conversionPool = getTemporaryStoragePool(vmVolumesStore, storagePoolMgr); + final String conversionPoolPath = conversionPool.getLocalPath(); + + try { + String volumesBasePath = String.format("%s/%s", conversionPoolPath, vmVolumesPrefix); + String xmlPath = String.format("%s.xml", volumesBasePath); + boolean xmlExists = new File(xmlPath).exists(); + + LibvirtDomainXMLParser xmlParser = xmlExists ? parseMigratedVMXmlDomain(volumesBasePath) : null; + List temporaryDisks = xmlExists && xmlParser != null ? + getTemporaryDisksFromParsedXml(conversionPool, xmlParser, volumesBasePath, conversionPoolPath, vmVolumesPrefix) : + getTemporaryDisksWithPrefixFromTemporaryPool(conversionPool, conversionPoolPath, vmVolumesPrefix); + + cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, conversionPool, vmVolumesPrefix, xmlExists); + + } catch (Exception e) { + String error = String.format("Error cleaning up converted disks with prefix %s from %s, due to: %s", + vmVolumesPrefix, conversionPoolPath, e.getMessage()); + logger.error(error, e); + return new Answer(command, false, error); + } + + return new Answer(command); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java new file mode 100644 index 000000000000..c23a3ef0c979 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossValidationCommandWrapper.java @@ -0,0 +1,50 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.cloudstack.backup.CleanupKbossValidationCommand; + +@ResourceWrapper(handles = CleanupKbossValidationCommand.class) +public class LibvirtCleanupKbossValidationCommandWrapper extends CommandWrapper { + @Override + public Answer execute(CleanupKbossValidationCommand command, LibvirtComputingResource serverResource) { + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + cleanupSecondaryStorages(command, storagePoolMgr); + return new Answer(command); + } + + /** + * The objective of this command is to remove the secondary storage references after the validation VM was stopped. + * Since the getStoragePoolByURI and deleteStoragePool have a reference counter, where the first method increases the count and the second one + * decreases the count, we must call the deleteStoragePool twice so that the command is count negative. + * */ + private void cleanupSecondaryStorages(CleanupKbossValidationCommand command, KVMStoragePoolManager storagePoolMgr) { + logger.info("Cleaning up secondary storage references after backup validation process using VM [{}].", command.getVmName()); + for (String secondaryUrl : command.getSecondaryStorages()) { + logger.debug("Cleaning up secondary storage reference for secondary at [{}].", secondaryUrl); + KVMStoragePool secondary = storagePoolMgr.getStoragePoolByURI(secondaryUrl); + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java new file mode 100644 index 000000000000..430d1c6ea3c2 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCleanupKbossVmBackupCommandWrapper.java @@ -0,0 +1,231 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import org.apache.cloudstack.backup.CleanupKbossBackupErrorAnswer; +import org.apache.cloudstack.backup.CleanupKbossBackupErrorCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.commons.lang3.StringUtils; +import org.libvirt.Domain; +import org.libvirt.Error; +import org.libvirt.LibvirtException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +@ResourceWrapper(handles = CleanupKbossBackupErrorCommand.class) +public class LibvirtCleanupKbossVmBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + List kbossTOS = command.getKbossTOs(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + logger.info("Cleaning up backup error for VM [{}].", command.getVmName()); + cleanupBackupDeltasOnSecondary(command, storagePoolManager, kbossTOS); + + if (command.isRunningVM()) { + Pair, Boolean> volumeTosAndIsVmRunning = cleanupRunningVm(command, serverResource); + return new CleanupKbossBackupErrorAnswer(command, volumeTosAndIsVmRunning.first(), volumeTosAndIsVmRunning.second()); + } + + return new CleanupKbossBackupErrorAnswer(command, mergeDeltasForStoppedVmIfNeeded(command, serverResource), false); + } + + private Pair, Boolean> cleanupRunningVm(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + Domain dm = null; + try { + dm = serverResource.getDomain(serverResource.getLibvirtUtilitiesHelper().getConnection(), command.getVmName()); + return new Pair<>(mergeDeltasForRunningVmIfNeeded(command, serverResource, dm), true); + } catch (LibvirtException e) { + if (e.getError().getCode() == Error.ErrorNumber.VIR_ERR_NO_DOMAIN && isVmReallyStopped(command, serverResource)) { + return new Pair<>(mergeDeltasForStoppedVmIfNeeded(command, serverResource), false); + } + logger.error("Error while trying to get VM [{}]. Aborting the process.", command.getVmName(), e); + return new Pair<>(List.of(), false); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException e) { + logger.warn("Ignoring Libvirt exception.", e); + } + } + } + } + + private List mergeDeltasForStoppedVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + List volumeObjectTOList = new ArrayList<>(); + for (KbossTO kbossTO : command.getKbossTOs()) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO)volumeObjectTO.getDataStore(); + KVMStoragePool kvmStoragePool = serverResource.getStoragePoolMgr().getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + boolean backupErrorDeltaExists = Files.exists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()))); + boolean parentBackupDeltaExists = Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getDeltaPathOnPrimary()))); + boolean shouldBaseDeltaExist = kbossTO.getParentDeltaPathOnPrimary() != null; + boolean baseDeltaExists = shouldBaseDeltaExist && Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getParentDeltaPathOnPrimary()))); + + if(!mergeDeltaIfNeeded(serverResource, kbossTO, backupErrorDeltaExists, parentBackupDeltaExists, shouldBaseDeltaExist, baseDeltaExists, + false, volumeObjectTO, volumeObjectTOList)) { + return List.of(); + } + } + return volumeObjectTOList; + } + + private List mergeDeltasForRunningVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource, Domain dm) throws LibvirtException { + String xmlDesc = dm.getXMLDesc(0); + LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + parser.parseDomainXML(xmlDesc); + List volumeObjectTOList = new ArrayList<>(); + for (KbossTO kbossTO : command.getKbossTOs()) { + LibvirtVMDef.DiskDef diskDef = parser.getDisks().stream() + .filter(disk -> StringUtils.contains(disk.getDiskPath(), kbossTO.getVolumeObjectTO().getPath()) || + StringUtils.contains(disk.getDiskPath(), kbossTO.getDeltaPathOnPrimary()) || + StringUtils.contains(disk.getDiskPath(), kbossTO.getParentDeltaPathOnPrimary())).findFirst().orElse(null); + + if (diskDef == null) { + logger.warn("Volume [{}] does not match any record we have. This must be manually normalized.", kbossTO.getVolumeObjectTO().getUuid()); + return List.of(); + } + + boolean backupErrorDeltaExists = diskDef.getDiskPath().contains(kbossTO.getVolumeObjectTO().getPath()); + boolean parentBackupDeltaExists = diskDef.getDiskPath().contains(kbossTO.getDeltaPathOnPrimary()) || + diskDef.getBackingStoreList().stream().anyMatch(path -> path.contains(kbossTO.getDeltaPathOnPrimary())); + boolean shouldBaseDeltaExist = kbossTO.getParentDeltaPathOnPrimary() != null; + boolean baseDeltaExists = shouldBaseDeltaExist && StringUtils.contains(diskDef.getDiskPath(), kbossTO.getParentDeltaPathOnPrimary()) || + diskDef.getBackingStoreList().stream().anyMatch(path -> StringUtils.contains(path, kbossTO.getParentDeltaPathOnPrimary())); + + mergeDeltaIfNeeded(serverResource, kbossTO, backupErrorDeltaExists, parentBackupDeltaExists, shouldBaseDeltaExist, baseDeltaExists, true, + kbossTO.getVolumeObjectTO(), volumeObjectTOList); + } + + return volumeObjectTOList; + } + + private boolean mergeDeltaIfNeeded(LibvirtComputingResource serverResource, KbossTO kbossTO, boolean backupErrorDeltaExists, + boolean parentBackupDeltaExists, boolean shouldBaseDeltaExist, boolean baseDeltaExists, boolean runningVm, VolumeObjectTO volumeObjectTO, + List volumeObjectTOList) { + DeltaMergeTreeTO deltaMergeTreeTO; + if (!backupErrorDeltaExists) { + if (parentBackupDeltaExists && (!shouldBaseDeltaExist || baseDeltaExists)) { + volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); + logger.debug("Volume [{}] is already consistent. Its path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + volumeObjectTOList.add(volumeObjectTO); + return true; + } else if (baseDeltaExists) { + volumeObjectTO.setPath(kbossTO.getParentDeltaPathOnPrimary()); + logger.debug("Volume [{}] is already consistent. Its path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + volumeObjectTOList.add(volumeObjectTO); + return true; + } else { + logger.warn("Volume [{}] is inconsistent in an anomalous way. We cannot normalize it automatically.", volumeObjectTO.getUuid()); + return false; + } + } else if (parentBackupDeltaExists) { + logger.debug("Volume [{}] is inconsistent, but we can normalize it. We will merge the delta created by this backup with the delta created by the previous " + + "backup.", volumeObjectTO.getUuid()); + deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, + kbossTO.getDeltaPathOnPrimary()), volumeObjectTO, List.of()); + } else if (baseDeltaExists) { + logger.debug("Volume [{}] is inconsistent, but we can normalize it. We will merge the delta created by this backup with the base volume.", + volumeObjectTO.getUuid()); + deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, + kbossTO.getParentDeltaPathOnPrimary()), volumeObjectTO, List.of()); + } else { + logger.warn("Volume [{}] is inconsistent in an anomalous way. We cannot normalize it automatically.", volumeObjectTO.getUuid()); + return false; + } + + try { + if (runningVm) { + serverResource.mergeDeltaForRunningVm(deltaMergeTreeTO, volumeObjectTO.getVmName(), volumeObjectTO); + } else { + serverResource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } + volumeObjectTO.setPath(deltaMergeTreeTO.getParent().getPath()); + volumeObjectTOList.add(volumeObjectTO); + return true; + } catch (QemuImgException | IOException | LibvirtException ex) { + logger.error("Got an exception while trying to merge delta for volume [{}].", volumeObjectTO.getUuid(), ex); + return false; + } + } + + private boolean isVmReallyStopped(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { + VolumeObjectTO volume = command.getKbossTOs().stream() + .filter(kbossTO -> kbossTO.getVolumeObjectTO().getDeviceId() == 0) + .map(KbossTO::getVolumeObjectTO).findFirst().orElseThrow(); + PrimaryDataStoreTO primary = (PrimaryDataStoreTO)volume.getDataStore(); + KVMStoragePool storage = serverResource.getStoragePoolMgr().getStoragePool(primary.getPoolType(), primary.getUuid()); + KVMPhysicalDisk disk = storage.getPhysicalDisk(volume.getUuid()); + File diskFile = new File(disk.getPath()); + long time1 = diskFile.lastModified(); + try { + Thread.sleep(30 * 1000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + long time2 = diskFile.lastModified(); + if (time1 != time2) { + logger.info("VM root disk [{}] has been modified in the last 30 seconds. It seems the VM is running, even though we were unable to find it. If the VM is " + + "running on this host, you can try again later. If the VM was somehow migrated, you should update the database directly."); + return false; + } + logger.warn("VM [{}] was not found by Libvirt and the root disk has not had any writes on the last 30 seconds. Assuming that it is stopped.", command.getVmName()); + return true; + } + + private void cleanupBackupDeltasOnSecondary(CleanupKbossBackupErrorCommand command, KVMStoragePoolManager storagePoolManager, List kbossTOS) { + KVMStoragePool storagePool = null; + try { + storagePool = storagePoolManager.getStoragePoolByURI(command.getImageStoreUrl()); + for (KbossTO kbossTO : kbossTOS) { + String deltaPath = storagePool.getLocalPathFor(kbossTO.getDeltaPathOnSecondary()); + logger.debug("Cleaning up file at [{}] if it exists.", deltaPath); + try { + Files.deleteIfExists(Path.of(deltaPath)); + } catch (IOException e) { + logger.error("Unable to delete leftover backup delta at [{}].", deltaPath); + } + } + } finally { + if (storagePool != null) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java new file mode 100644 index 000000000000..18c4324addf5 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java @@ -0,0 +1,173 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferAnswer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; +import com.cloud.utils.script.OutputInterpreter; + +@ResourceWrapper(handles = ClvmLockTransferCommand.class) +public class LibvirtClvmLockTransferCommandWrapper + extends CommandWrapper { + + @Override + public Answer execute(ClvmLockTransferCommand cmd, LibvirtComputingResource serverResource) { + String lvPath = cmd.getLvPath(); + ClvmLockTransferCommand.Operation operation = cmd.getOperation(); + String volumeUuid = cmd.getVolumeUuid(); + + logger.info("Executing CLVM lock transfer: operation={}, lv={}, volume={}", + operation, lvPath, volumeUuid); + + try { + + if (operation == ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE) { + return handleQueryLockState(cmd, lvPath, volumeUuid); + } + + String lvchangeOpt; + String operationDesc; + switch (operation) { + case DEACTIVATE: + lvchangeOpt = "-an"; + operationDesc = "deactivated"; + break; + case ACTIVATE_EXCLUSIVE: + lvchangeOpt = "-aey"; + operationDesc = "activated exclusively"; + break; + case ACTIVATE_SHARED: + lvchangeOpt = "-asy"; + operationDesc = "activated in shared mode"; + break; + default: + return new ClvmLockTransferAnswer(cmd, false, "Unknown operation: " + operation); + } + + Script script = new Script("/usr/sbin/lvchange", 60000, logger); + script.add(lvchangeOpt); + script.add(lvPath); + + String result = script.execute(); + + if (result != null) { + logger.error("CLVM lock transfer failed for volume {}: {}", + volumeUuid, result); + return new ClvmLockTransferAnswer(cmd, false, + String.format("lvchange %s %s failed: %s", lvchangeOpt, lvPath, result)); + } + + logger.info("Successfully executed CLVM lock transfer: {} {} for volume {}", + lvchangeOpt, lvPath, volumeUuid); + + return new ClvmLockTransferAnswer(cmd, true, + String.format("Successfully %s CLVM volume %s", operationDesc, volumeUuid)); + + } catch (Exception e) { + logger.error("Exception during CLVM lock transfer for volume {}: {}", + volumeUuid, e.getMessage(), e); + return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage()); + } + } + + /** + * Query whether this host currently has the CLVM LV activated locally. + * Executes: lvs -o lv_attr,lv_host,lv_active --noheadings + * + * lv_attr[4]=='a' (isActive) is LOCAL and is the authoritative signal — true only on + * the host where the LV is currently activated. The management server fans out this + * query to all cluster hosts; the one returning isActive=true is the lock holder. + * lv_attr[5]=='o' (isOpen) means a VM has the device open on this host (doing I/O). + * lv_host is retained for diagnostic logging only — do NOT use it to identify the + * lock holder. + */ + private Answer handleQueryLockState(ClvmLockTransferCommand cmd, String lvPath, String volumeUuid) { + try { + Script script = new Script("/usr/sbin/lvs", 30000, logger); + script.add("-o"); + script.add("lv_attr,lv_host"); + script.add("--noheadings"); + script.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = script.execute(parser); + + if (result != null) { + logger.error("Failed to query lock state for volume {}: {}", volumeUuid, result); + return new ClvmLockTransferAnswer(cmd, false, + String.format("lvs command failed: %s", result)); + } + + String[] lines = parser.getLines().split("\n"); + String dataLine = null; + + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && + trimmed.length() >= 10 && + "-wrsvmpco".indexOf(trimmed.charAt(0)) >= 0) { + dataLine = trimmed; + break; + } + } + + if (dataLine == null) { + String allOutput = parser.getLines(); + logger.warn("Could not find lv_attr data line in lvs output for volume {}: {}", + volumeUuid, allOutput); + return new ClvmLockTransferAnswer(cmd, false, + String.format("Could not parse lvs output. Full output: %s", allOutput)); + } + + logger.debug("Parsed lv_attr data line for volume {}: {}", volumeUuid, dataLine); + + String[] parts = dataLine.split("\\s+"); + if (parts.length < 1) { + return new ClvmLockTransferAnswer(cmd, false, "Invalid lvs output format"); + } + + String lvAttr = parts[0]; + // lv_host: for diagnostics only, unreliable for lock-holder identification + String hostname = parts.length > 1 ? parts[1] : null; + + // lv_attr[4]=='a' → LV is active on THIS host (local activation state) + boolean isActive = lvAttr.length() > 4 && lvAttr.charAt(4) == 'a'; + // lv_attr[5]=='o' → a process has the device file open on this host (VM doing I/O) + boolean isOpen = lvAttr.length() > 5 && lvAttr.charAt(5) == 'o'; + + logger.info("Queried lock state for volume {}: attr={}, hostname={}, active={}, open={}", + volumeUuid, lvAttr, hostname, isActive, isOpen); + + return new ClvmLockTransferAnswer(cmd, true, + String.format("Lock state: active=%s, open=%s, host=%s", + isActive, isOpen, hostname != null ? hostname : "none"), + hostname, isActive, isOpen, lvAttr); + + } catch (Exception e) { + logger.error("Exception during lock state query for volume {}: {}", + volumeUuid, e.getMessage(), e); + return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage()); + } + } + +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java new file mode 100644 index 000000000000..2cc08311cbce --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCompressBackupCommandWrapper.java @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.CompressBackupCommand; +import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +@ResourceWrapper(handles = CompressBackupCommand.class) +public class LibvirtCompressBackupCommandWrapper extends CommandWrapper { + public static final String COMPRESSION_TYPE = "compression_type"; + private static final int MIN_QCOW_2_VERSION_FOR_ZSTD = 3; + + @Override + public Answer execute(CompressBackupCommand command, LibvirtComputingResource serverResource) { + List secondaryStorages = new ArrayList<>(); + List deltas = command.getBackupDeltasToCompress(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + logger.info("Starting compression for backup deltas [{}].", deltas); + try { + QemuImg qemuImg = new QemuImg(command.getWait() * 1000); + Integer rateLimit = validateAndGetRateLimit(command, qemuImg); + + KVMStoragePool mainSecStorage = storagePoolManager.getStoragePoolByURI(deltas.stream().findFirst().orElseThrow().getChild().getDataStore().getUrl()); + secondaryStorages.add(mainSecStorage); + secondaryStorages.addAll(command.getBackupChainImageStoreUrls().stream().map(storagePoolManager::getStoragePoolByURI).collect(Collectors.toList())); + + if (!checkAvailableStorage(command, mainSecStorage, storagePoolManager)) { + return new Answer(command, false, "Not enough available space on secondary."); + } + + for (DeltaMergeTreeTO delta : deltas) { + DataTO child = delta.getChild(); + + QemuImgFile backingFile = null; + DataTO parent = delta.getParent(); + if (parent != null) { + KVMStoragePool parentSecondaryStorage = storagePoolManager.getStoragePoolByURI(parent.getDataStore().getUrl()); + secondaryStorages.add(parentSecondaryStorage); + backingFile = new QemuImgFile(parentSecondaryStorage.getLocalPathFor(parent.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); + } + + String fullDeltaPath = mainSecStorage.getLocalPathFor(child.getPath()); + String compressedPath = fullDeltaPath + ".comp"; + QemuImgFile originalBackup = new QemuImgFile(fullDeltaPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile compressedBackup = new QemuImgFile(compressedPath, QemuImg.PhysicalDiskFormat.QCOW2); + + HashMap options = new HashMap<>(); + Backup.CompressionLibrary compressionLib = getCompressionLibrary(command, fullDeltaPath); + setCompressionTypeOptionIfAvailable(qemuImg, options, compressionLib); + int coroutines = command.getCoroutines(); + logger.info("Starting compression for backup delta [{}] with parent [{}] using [{}] coroutines.", child, parent, coroutines); + qemuImg.convert(originalBackup, compressedBackup, backingFile, options, null, new QemuImageOptions(originalBackup.getFormat(), originalBackup.getFileName(), + null), null, false, false, true, true, coroutines, rateLimit); + } + } catch (LibvirtException | QemuImgException e) { + return new Answer(command, e); + } finally { + for (KVMStoragePool secondaryStorage : secondaryStorages) { + storagePoolManager.deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + + return new Answer(command); + } + + private Integer validateAndGetRateLimit(CompressBackupCommand command, QemuImg qemuImg) { + if (command.getRateLimit() < 1) { + return null; + } + + if (qemuImg.getVersion() < QemuImg.QEMU_5_2) { + throw new CloudRuntimeException("Qemu version is lower than 5.2.0, unable to set the rate limit."); + } + + return command.getRateLimit(); + } + + /** + * Sets the compression type option if qemu-img is at least in version 5.1. Otherwise, will not set it and qemu will use zlib. + * */ + private void setCompressionTypeOptionIfAvailable(QemuImg qemuImg, HashMap options, Backup.CompressionLibrary compressionLib) { + if (qemuImg.getVersion() >= QemuImg.QEMU_5_1) { + options.put(COMPRESSION_TYPE, compressionLib.name()); + return; + } + logger.warn("Qemu is at a lower version than 5.1, we will not be able to use zstd to compress backups. Only zlib is supported for this version. Current version is [{}].", + qemuImg.getVersion()); + } + + private Backup.CompressionLibrary getCompressionLibrary(CompressBackupCommand command, String fullDeltaPath) { + Backup.CompressionLibrary compressionLib = command.getCompressionLib(); + if (compressionLib == Backup.CompressionLibrary.zlib || !Qcow2Inspector.validateQcow2Version(fullDeltaPath, MIN_QCOW_2_VERSION_FOR_ZSTD)) { + logger.debug("Compression for delta [{}] will use zlib as the compression library.", fullDeltaPath); + return Backup.CompressionLibrary.zlib; + } + + logger.debug("Compression for delta [{}] will try to use zstd as the compression library.", fullDeltaPath); + return Backup.CompressionLibrary.zstd; + } + + /** + * Validates available storage. Forces Libvirt to refresh storage info so that we have the most up to date data. + * */ + private boolean checkAvailableStorage(CompressBackupCommand command, KVMStoragePool mainSecStorage, KVMStoragePoolManager storagePoolManager) { + logger.debug("Checking available storage [{}].", mainSecStorage); + mainSecStorage = storagePoolManager.getStoragePool(mainSecStorage.getType(), mainSecStorage.getUuid(), true, false); + if (mainSecStorage.getAvailable() < command.getMinFreeStorage()) { + logger.warn("There is not enough available space for compression of backup! Available size is [{}], needed [{}]. Aborting compression.", + mainSecStorage.getAvailable(), command.getMinFreeStorage()); + return false; + } + return true; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java new file mode 100644 index 000000000000..30c8849df5fa --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConsolidateVolumesCommandWrapper.java @@ -0,0 +1,59 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import org.apache.cloudstack.backup.ConsolidateVolumesAnswer; +import org.apache.cloudstack.backup.ConsolidateVolumesCommand; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.List; + +@ResourceWrapper(handles = ConsolidateVolumesCommand.class) +public class LibvirtConsolidateVolumesCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(ConsolidateVolumesCommand command, LibvirtComputingResource serverResource) { + List volumeObjectTOs = command.getVolumesToConsolidate(); + String vmName = command.getVmName(); + + List successfulConsolidations = new ArrayList<>(); + try { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { + if (!serverResource.pullVolumeBackingFile(volumeObjectTO, vmName)) { + return new ConsolidateVolumesAnswer(command, false, "Failed to consolidate all volumes.", successfulConsolidations); + } + successfulConsolidations.add(volumeObjectTO); + } + } catch (LibvirtException ex) { + return new ConsolidateVolumesAnswer(command, false, ex.getMessage(), successfulConsolidations); + } + + KVMStoragePoolManager kvmStoragePoolManager = serverResource.getStoragePoolMgr(); + for (String secStorageUuid : command.getSecondaryStorageUuids()) { + kvmStoragePoolManager.deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, secStorageUuid); + } + return new ConsolidateVolumesAnswer(command, true, "Success", successfulConsolidations); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java index 93895349a6e8..f28b5eda4a63 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java @@ -20,11 +20,20 @@ import java.net.URLEncoder; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Locale; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.Answer; @@ -49,6 +58,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper supportedInstanceConvertSourceHypervisors = List.of(Hypervisor.HypervisorType.VMware); + private static final Pattern SHA1_FINGERPRINT_PATTERN = Pattern.compile("(?i)(?:SHA1\\s+)?Fingerprint\\s*=\\s*([0-9A-F:]+)"); @Override public Answer execute(ConvertInstanceCommand cmd, LibvirtComputingResource serverResource) { @@ -59,7 +69,8 @@ public Answer execute(ConvertInstanceCommand cmd, LibvirtComputingResource serve DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation(); long timeout = (long) cmd.getWait() * 1000; String extraParams = cmd.getExtraParams(); - String originalVMName = cmd.getOriginalVMName(); // For logging purposes, as the sourceInstance may have been cloned + boolean useVddk = cmd.isUseVddk(); + String originalVMName = cmd.getOriginalVMName(); if (cmd.getCheckConversionSupport() && !serverResource.hostSupportsInstanceConversion()) { String msg = String.format("Cannot convert the instance %s from VMware as the virt-v2v binary is not found. " + @@ -82,61 +93,75 @@ public Answer execute(ConvertInstanceCommand cmd, LibvirtComputingResource serve logger.info(String.format("(%s) Attempting to convert the instance %s from %s to KVM", originalVMName, sourceInstanceName, sourceHypervisorType)); final String temporaryConvertPath = temporaryStoragePool.getLocalPath(); + final String temporaryConvertUuid = UUID.randomUUID().toString(); + boolean verboseModeEnabled = serverResource.isConvertInstanceVerboseModeEnabled(); - String ovfTemplateDirOnConversionLocation; - String sourceOVFDirPath; + boolean cleanupSecondaryStorage = false; boolean ovfExported = false; - if (cmd.getExportOvfToConversionLocation()) { - String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance, originalVMName); - if (StringUtils.isBlank(exportInstanceOVAUrl)) { - String err = String.format("Couldn't export OVA for the VM %s, due to empty url", sourceInstanceName); - logger.error(String.format("(%s) %s", originalVMName, err)); - return new Answer(cmd, false, err); - } + String ovfTemplateDirOnConversionLocation = null; - int noOfThreads = cmd.getThreadsCountToExportOvf(); - if (noOfThreads > 1 && !serverResource.ovfExportToolSupportsParallelThreads()) { - noOfThreads = 0; - } - ovfTemplateDirOnConversionLocation = UUID.randomUUID().toString(); - temporaryStoragePool.createFolder(ovfTemplateDirOnConversionLocation); - sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); - ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, originalVMName, timeout); - if (!ovfExported) { - String err = String.format("Export OVA for the VM %s failed", sourceInstanceName); - logger.error(String.format("(%s) %s", originalVMName, err)); - return new Answer(cmd, false, err); - } - sourceOVFDirPath = String.format("%s%s/", sourceOVFDirPath, sourceInstanceName); - } else { - ovfTemplateDirOnConversionLocation = cmd.getTemplateDirOnConversionLocation(); - sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); - } + try { + boolean result; + if (useVddk) { + logger.info("({}) Using VDDK-based conversion (direct from VMware)", originalVMName); + String vddkLibDir = resolveVddkSetting(cmd.getVddkLibDir(), serverResource.getVddkLibDir()); + if (StringUtils.isBlank(vddkLibDir)) { + String err = String.format("VDDK lib dir is not configured on the host. " + + "Set '%s' in agent.properties or in details parameter of the import api call to use VDDK-based conversion.", "vddk.lib.dir"); + logger.error("({}) {}", originalVMName, err); + return new Answer(cmd, false, err); + } + String vddkTransports = resolveVddkSetting(cmd.getVddkTransports(), serverResource.getVddkTransports()); + String configuredVddkThumbprint = resolveVddkSetting(cmd.getVddkThumbprint(), serverResource.getVddkThumbprint()); + String passwordOption = serverResource.getDetectedPasswordFileOption(); + result = performInstanceConversionUsingVddk(sourceInstance, originalVMName, temporaryConvertPath, + vddkLibDir, serverResource.getLibguestfsBackend(), vddkTransports, configuredVddkThumbprint, + timeout, verboseModeEnabled, extraParams, temporaryConvertUuid, passwordOption); + } else { + logger.info("({}) Using OVF-based conversion (export + local convert)", originalVMName); + String sourceOVFDirPath; + if (cmd.getExportOvfToConversionLocation()) { + String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance, originalVMName); - logger.info(String.format("(%s) Attempting to convert the OVF %s of the instance %s from %s to KVM", - originalVMName, ovfTemplateDirOnConversionLocation, sourceInstanceName, sourceHypervisorType)); + if (StringUtils.isBlank(exportInstanceOVAUrl)) { + String err = String.format("Couldn't export OVA for the VM %s, due to empty url", sourceInstanceName); + logger.error("({}) {}", originalVMName, err); + return new Answer(cmd, false, err); + } - final String temporaryConvertUuid = UUID.randomUUID().toString(); - boolean verboseModeEnabled = serverResource.isConvertInstanceVerboseModeEnabled(); + int noOfThreads = cmd.getThreadsCountToExportOvf(); + if (noOfThreads > 1 && !serverResource.ovfExportToolSupportsParallelThreads()) { + noOfThreads = 0; + } + ovfTemplateDirOnConversionLocation = UUID.randomUUID().toString(); + temporaryStoragePool.createFolder(ovfTemplateDirOnConversionLocation); + sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); + ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, originalVMName, timeout); + + if (!ovfExported) { + String err = String.format("Export OVA for the VM %s failed", sourceInstanceName); + logger.error("({}) {}", originalVMName, err); + return new Answer(cmd, false, err); + } + sourceOVFDirPath = String.format("%s%s/", sourceOVFDirPath, sourceInstanceName); + } else { + ovfTemplateDirOnConversionLocation = cmd.getTemplateDirOnConversionLocation(); + sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); + } + + result = performInstanceConversion(originalVMName, sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid, + timeout, verboseModeEnabled, extraParams, serverResource); + } - boolean cleanupSecondaryStorage = false; - try { - boolean result = performInstanceConversion(originalVMName, sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid, - timeout, verboseModeEnabled, extraParams, serverResource); if (!result) { - String err = String.format( - "The virt-v2v conversion for the OVF %s failed. Please check the agent logs " + - "for the virt-v2v output. Please try on a different kvm host which " + - "has a different virt-v2v version.", - ovfTemplateDirOnConversionLocation); - logger.error(String.format("(%s) %s", originalVMName, err)); + String err = String.format("Instance conversion failed for VM %s. Please check virt-v2v logs.", sourceInstanceName); + logger.error("({}) {}", originalVMName, err); return new Answer(cmd, false, err); } return new ConvertInstanceAnswer(cmd, temporaryConvertUuid); } catch (Exception e) { - String error = String.format("Error converting instance %s from %s, due to: %s", - sourceInstanceName, sourceHypervisorType, e.getMessage()); - logger.error(String.format("(%s) %s", originalVMName, error), e); + String error = String.format("Error converting instance %s from %s, due to: %s", sourceInstanceName, sourceHypervisorType, e.getMessage()); + logger.error("({}) {}", originalVMName, error, e); cleanupSecondaryStorage = true; return new Answer(cmd, false, error); } finally { @@ -244,7 +269,12 @@ protected boolean performInstanceConversion(String originalVMName, String source String logPrefix = String.format("(%s) virt-v2v ovf source: %s progress", originalVMName, sourceOVFDirPath); OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); - script.execute(outputLogger); + Map convertInstanceEnv = serverResource.getConvertInstanceEnv(); + if (MapUtils.isEmpty(convertInstanceEnv)) { + script.execute(outputLogger); + } else { + script.execute(outputLogger, convertInstanceEnv); + } int exitValue = script.getExitValue(); return exitValue == 0; } @@ -268,4 +298,198 @@ protected void addExtraParamsToScript(String extraParams, Script script) { protected String encodeUsername(String username) { return URLEncoder.encode(username, Charset.defaultCharset()); } + + private String resolveVddkSetting(String commandValue, String agentValue) { + return StringUtils.defaultIfBlank(StringUtils.trimToNull(commandValue), StringUtils.trimToNull(agentValue)); + } + + protected boolean performInstanceConversionUsingVddk(RemoteInstanceTO vmwareInstance, String originalVMName, + String temporaryConvertFolder, String vddkLibDir, + String libguestfsBackend, String vddkTransports, + String configuredVddkThumbprint, + long timeout, boolean verboseModeEnabled, String extraParams, + String temporaryConvertUuid, String passwordOption) { + + String vcenterPassword = vmwareInstance.getVcenterPassword(); + if (StringUtils.isBlank(vcenterPassword)) { + logger.error("({}) Could not determine vCenter password for {}", originalVMName, vmwareInstance.getVcenterHost()); + return false; + } + + String passwordFilePath = String.format("/tmp/v2v.pass.cloud.%s.%s", + StringUtils.defaultIfBlank(vmwareInstance.getVcenterHost(), "unknown"), + UUID.randomUUID()); + try { + Files.writeString(Path.of(passwordFilePath), vcenterPassword); + Files.setPosixFilePermissions(Path.of(passwordFilePath), Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + logger.debug("({}) Written vCenter password to {}", originalVMName, passwordFilePath); + } catch (Exception e) { + logger.error("({}) Failed to write vCenter password file {}: {}", originalVMName, passwordFilePath, e.getMessage()); + return false; + } + + try { + String vpxUrl = buildVpxUrl(vmwareInstance); + + StringBuilder cmd = new StringBuilder(); + + cmd.append("export LIBGUESTFS_BACKEND=").append(libguestfsBackend).append(" && "); + + cmd.append("virt-v2v "); + cmd.append("--root first "); + cmd.append("-ic '").append(vpxUrl).append("' "); + if (StringUtils.isBlank(passwordOption)) { + logger.error("({}) Could not determine supported password file option for virt-v2v", originalVMName); + return false; + } + + cmd.append(passwordOption).append(" ").append(passwordFilePath).append(" "); + cmd.append("-it vddk "); + cmd.append("-io vddk-libdir=").append(vddkLibDir).append(" "); + String vddkThumbprint = StringUtils.trimToNull(configuredVddkThumbprint); + if (StringUtils.isBlank(vddkThumbprint)) { + vddkThumbprint = getVcenterThumbprint(vmwareInstance.getVcenterHost(), timeout, originalVMName); + } + if (StringUtils.isBlank(vddkThumbprint)) { + logger.error("({}) Could not determine vCenter thumbprint for {}", originalVMName, vmwareInstance.getVcenterHost()); + return false; + } + cmd.append("-io vddk-thumbprint=").append(vddkThumbprint).append(" "); + if (StringUtils.isNotBlank(vddkTransports)) { + cmd.append("-io vddk-transports=").append(vddkTransports).append(" "); + } + cmd.append(vmwareInstance.getInstanceName()).append(" "); + cmd.append("-o local "); + cmd.append("-os ").append(temporaryConvertFolder).append(" "); + cmd.append("-of qcow2 "); + cmd.append("-on ").append(temporaryConvertUuid).append(" "); + + if (verboseModeEnabled) { + cmd.append("-v "); + } + + if (StringUtils.isNotBlank(extraParams)) { + cmd.append(extraParams).append(" "); + } + + Script script = new Script("/bin/bash", timeout, logger); + script.add("-c"); + script.add(cmd.toString()); + + String logPrefix = String.format("(%s) virt-v2v vddk import", originalVMName); + OutputInterpreter.LineByLineOutputLogger outputLogger = + new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); + + logger.info("({}) Starting virt-v2v VDDK conversion", originalVMName); + script.execute(outputLogger); + + int exitValue = script.getExitValue(); + if (exitValue != 0) { + logger.error("({}) virt-v2v failed with exit code {}", originalVMName, exitValue); + } + + return exitValue == 0; + } finally { + try { + Files.deleteIfExists(Path.of(passwordFilePath)); + logger.debug("({}) Deleted password file {}", originalVMName, passwordFilePath); + } catch (Exception e) { + logger.warn("({}) Failed to delete password file {}: {}", originalVMName, passwordFilePath, e.getMessage()); + } + } + } + + protected String getVcenterThumbprint(String vcenterHost, long timeout, String originalVMName) { + if (StringUtils.isBlank(vcenterHost)) { + return null; + } + + String endpoint = String.format("%s:443", vcenterHost); + String command = String.format("openssl s_client -connect '%s' /dev/null | " + + "openssl x509 -fingerprint -sha1 -noout", endpoint); + + Script script = new Script("/bin/bash", timeout, logger); + script.add("-c"); + script.add(command); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + script.execute(parser); + + String output = parser.getLines(); + if (script.getExitValue() != 0) { + logger.error("({}) Failed to fetch vCenter thumbprint for {}", originalVMName, vcenterHost); + return null; + } + + String thumbprint = extractSha1Fingerprint(output); + if (StringUtils.isBlank(thumbprint)) { + logger.error("({}) Failed to parse vCenter thumbprint from output for {}", originalVMName, vcenterHost); + return null; + } + return thumbprint; + } + + private String extractSha1Fingerprint(String output) { + String parsedOutput = StringUtils.trimToEmpty(output); + if (StringUtils.isBlank(parsedOutput)) { + return null; + } + + for (String line : parsedOutput.split("\\R")) { + String trimmedLine = StringUtils.trimToEmpty(line); + if (StringUtils.isBlank(trimmedLine)) { + continue; + } + + Matcher matcher = SHA1_FINGERPRINT_PATTERN.matcher(trimmedLine); + if (matcher.find()) { + return matcher.group(1).toUpperCase(Locale.ROOT); + } + + // Fallback for raw fingerprint-only output. + if (trimmedLine.matches("(?i)[0-9a-f]{2}(:[0-9a-f]{2})+")) { + return trimmedLine.toUpperCase(Locale.ROOT); + } + } + return null; + } + + /** + * Build vpx:// URL for virt-v2v + * + * Format: + * vpx://user@vcenter/DC/cluster/host?no_verify=1 + */ + private String buildVpxUrl(RemoteInstanceTO vmwareInstance) { + + String vmName = vmwareInstance.getInstanceName(); + String vcenter = vmwareInstance.getVcenterHost(); + String username = vmwareInstance.getVcenterUsername(); + String datacenter = vmwareInstance.getDatacenterName(); + String cluster = vmwareInstance.getClusterName(); + String host = vmwareInstance.getHostName(); + + String encodedUsername = encodeUsername(username); + + StringBuilder url = new StringBuilder(); + url.append("vpx://") + .append(encodedUsername) + .append("@") + .append(vcenter) + .append("/") + .append(datacenter); + + if (StringUtils.isNotBlank(cluster)) { + url.append("/").append(cluster); + } + + if (StringUtils.isNotBlank(host)) { + url.append("/").append(host); + } + + url.append("?no_verify=1"); + + logger.info("({}) Using VPX URL: {}", vmName, url); + return url.toString(); + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java index 84d17a1a1161..26265f82467b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateDiskOnlyVMSnapshotCommandWrapper.java @@ -19,180 +19,29 @@ package com.cloud.hypervisor.kvm.resource.wrapper; import com.cloud.agent.api.Answer; -import com.cloud.agent.api.VMSnapshotTO; import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer; import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; -import com.cloud.hypervisor.kvm.storage.KVMStoragePool; -import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.cloudstack.utils.qemu.QemuImg; -import org.apache.cloudstack.utils.qemu.QemuImgException; -import org.apache.cloudstack.utils.qemu.QemuImgFile; -import org.libvirt.Connect; -import org.libvirt.Domain; -import org.libvirt.LibvirtException; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; @ResourceWrapper(handles = CreateDiskOnlyVmSnapshotCommand.class) public class LibvirtCreateDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper { - private static final String SNAPSHOT_XML = "\n" + - "%s\n" + - "\n" + - " \n" + - "%s" + - " \n" + - ""; - - private static final String TAG_DISK_SNAPSHOT = "\n" + - "\n" + - "\n"; - @Override public Answer execute(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { VirtualMachine.State state = cmd.getVmState(); - if (VirtualMachine.State.Running.equals(state)) { - return takeDiskOnlyVmSnapshotOfRunningVm(cmd, resource); - } - - return takeDiskOnlyVmSnapshotOfStoppedVm(cmd, resource); - } - - protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { - String vmName = cmd.getVmName(); - logger.info("Taking disk-only VM snapshot of running VM [{}].", vmName); - - Domain dm = null; try { - LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper(); - Connect conn = libvirtUtilitiesHelper.getConnection(); - List volumeObjectTOS = cmd.getVolumeTOs(); - List disks = resource.getDisks(conn, vmName); - - dm = resource.getDomain(conn, vmName); - - if (dm == null) { - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, String.format("Creation of disk-only VM Snapshot failed as we could not find the VM [%s].", vmName), null); - } - - VMSnapshotTO target = cmd.getTarget(); - Pair>> snapshotXmlAndVolumeToNewPathMap = createSnapshotXmlAndNewVolumePathMap(volumeObjectTOS, disks, target, resource); - - dm.snapshotCreateXML(snapshotXmlAndVolumeToNewPathMap.first(), getFlagsToUseForRunningVmSnapshotCreation(target)); - - return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, snapshotXmlAndVolumeToNewPathMap.second()); - } catch (LibvirtException e) { - String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()); - logger.error(errorMsg, e); - if (e.getMessage().contains("QEMU guest agent is not connected")) { - errorMsg = "QEMU guest agent is not connected. If the VM has been recently started, it might connect soon. Otherwise the VM does not have the" + - " guest agent installed; thus the QuiesceVM parameter is not supported."; - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null); - } - return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null); - } finally { - if (dm != null) { - try { - dm.free(); - } catch (LibvirtException l) { - logger.trace("Ignoring libvirt error.", l); - } + if (VirtualMachine.State.Running.equals(state)) { + return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVmSnapshotForRunningVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName(), + cmd.getTarget().getSnapshotName(), cmd.getTarget().getQuiescevm())); } + return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, resource.createDiskOnlyVMSnapshotOfStoppedVm(cmd.getVolumeTosAndNewPaths(), cmd.getVmName())); + } catch (BackupException ex) { + return new Answer(cmd, ex); } } - - protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) { - String vmName = cmd.getVmName(); - logger.info("Taking disk-only VM snapshot of stopped VM [{}].", vmName); - - Map> mapVolumeToSnapshotSizeAndNewVolumePath = new HashMap<>(); - - List volumeObjectTos = cmd.getVolumeTOs(); - KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr(); - try { - for (VolumeObjectTO volumeObjectTO : volumeObjectTos) { - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); - KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - - String snapshotPath = UUID.randomUUID().toString(); - String snapshotFullPath = kvmStoragePool.getLocalPathFor(snapshotPath); - QemuImgFile newDelta = new QemuImgFile(snapshotFullPath, QemuImg.PhysicalDiskFormat.QCOW2); - - String currentDeltaFullPath = kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()); - QemuImgFile currentDelta = new QemuImgFile(currentDeltaFullPath, QemuImg.PhysicalDiskFormat.QCOW2); - - QemuImg qemuImg = new QemuImg(0); - - logger.debug("Creating new delta for volume [{}] as part of the disk-only VM snapshot process for VM [{}].", volumeObjectTO.getUuid(), vmName); - qemuImg.create(newDelta, currentDelta); - - mapVolumeToSnapshotSizeAndNewVolumePath.put(volumeObjectTO.getUuid(), new Pair<>(getFileSize(currentDeltaFullPath), snapshotPath)); - } - } catch (LibvirtException | QemuImgException e) { - logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e); - for (VolumeObjectTO volumeObjectTO : volumeObjectTos) { - Pair volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid()); - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); - KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - - if (volSizeAndNewPath == null) { - continue; - } - try { - Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second()))); - } catch (IOException ex) { - logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex); - } - } - return new Answer(cmd, e); - } - - return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath); - } - - protected int getFlagsToUseForRunningVmSnapshotCreation(VMSnapshotTO target) { - int flags = target.getQuiescevm() ? Domain.SnapshotCreateFlags.QUIESCE : 0; - flags += Domain.SnapshotCreateFlags.DISK_ONLY + - Domain.SnapshotCreateFlags.ATOMIC + - Domain.SnapshotCreateFlags.NO_METADATA; - return flags; - } - - protected Pair>> createSnapshotXmlAndNewVolumePathMap(List volumeObjectTOS, List disks, VMSnapshotTO target, LibvirtComputingResource resource) { - StringBuilder stringBuilder = new StringBuilder(); - Map> volumeObjectToNewPathMap = new HashMap<>(); - - for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - LibvirtVMDef.DiskDef diskdef = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO); - String newPath = UUID.randomUUID().toString(); - stringBuilder.append(String.format(TAG_DISK_SNAPSHOT, diskdef.getDiskLabel(), resource.getSnapshotTemporaryPath(diskdef.getDiskPath(), newPath))); - - long snapSize = getFileSize(diskdef.getDiskPath()); - - volumeObjectToNewPathMap.put(volumeObjectTO.getUuid(), new Pair<>(snapSize, newPath)); - } - - String snapshotXml = String.format(SNAPSHOT_XML, target.getSnapshotName(), stringBuilder); - return new Pair<>(snapshotXml, volumeObjectToNewPathMap); - } - - protected long getFileSize(String path) { - return new File(path).length(); - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java new file mode 100644 index 000000000000..7cf05da9b211 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java @@ -0,0 +1,178 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.backup.CreateImageTransferAnswer; +import org.apache.cloudstack.backup.CreateImageTransferCommand; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.storage.resource.IpTablesHelper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.ImageServerControlSocket; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = CreateImageTransferCommand.class) +public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private static final String IMAGE_SERVER_TLS_CERT_FILE = "/etc/cloudstack/agent/cloud.crt"; + private static final String IMAGE_SERVER_TLS_KEY_FILE = "/etc/cloudstack/agent/cloud.key"; + + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + private static String shellQuote(String value) { + return "'" + value.replace("'", "'\\''") + "'"; + } + + private boolean startImageServerIfNotRunning(int imageServerPort, String listenAddress, LibvirtComputingResource resource) { + final String imageServerPackageDir = resource.getImageServerPath(); + final String imageServerParentDir = new File(imageServerPackageDir).getParent(); + final String imageServerModuleName = new File(imageServerPackageDir).getName(); + final boolean tlsEnabled = resource.isImageServerTlsEnabled(); + String unitName = resource.IMAGE_SERVER_SYSTEMD_UNIT_NAME; + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult == null && ImageServerControlSocket.isReady()) { + return true; + } + + resetService(unitName); + if (checkResult != null) { + StringBuilder systemdRunCmd = new StringBuilder(String.format( + "systemd-run --unit=%s --property=Restart=no --property=WorkingDirectory=%s /usr/bin/python3 -m %s --listen %s --port %d", + unitName, shellQuote(imageServerParentDir), imageServerModuleName, shellQuote(listenAddress), imageServerPort)); + + if (tlsEnabled) { + systemdRunCmd.append(" --tls-enabled"); + systemdRunCmd.append(" --tls-cert-file ").append(IMAGE_SERVER_TLS_CERT_FILE); + systemdRunCmd.append(" --tls-key-file ").append(IMAGE_SERVER_TLS_KEY_FILE); + } + + Script startScript = new Script("/bin/bash", logger); + startScript.add("-c"); + startScript.add(systemdRunCmd.toString()); + String startResult = startScript.execute(); + + if (startResult != null) { + logger.error(String.format("Failed to start the Image server: %s", startResult)); + return false; + } + } + + int maxWaitSeconds = 10; + int pollIntervalMs = 1000; + int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs; + boolean serverReady = false; + + for (int attempt = 0; attempt < maxAttempts; attempt++) { + if (ImageServerControlSocket.isReady()) { + serverReady = true; + logger.info(String.format("Image server control socket is ready (attempt %d)", attempt + 1)); + break; + } + try { + Thread.sleep(pollIntervalMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + if (!serverReady) { + logger.error(String.format("Image server control socket not ready within %d seconds", maxWaitSeconds)); + return false; + } + + String rule = String.format("-p tcp -m state --state NEW -m tcp --dport %d -j ACCEPT", imageServerPort); + IpTablesHelper.addConditionally(IpTablesHelper.INPUT_CHAIN, true, rule, + String.format("Error in opening up image server port %d", imageServerPort)); + + return true; + } + + public Answer execute(CreateImageTransferCommand cmd, LibvirtComputingResource resource) { + final String transferId = cmd.getTransferId(); + ImageTransfer.Backend backend = cmd.getBackend(); + + if (StringUtils.isBlank(transferId)) { + return new CreateImageTransferAnswer(cmd, false, "transferId is empty."); + } + + final Map payload = new HashMap<>(); + payload.put("backend", backend.toString()); + payload.put("idle_timeout_seconds", cmd.getIdleTimeoutSeconds()); + + if (backend == ImageTransfer.Backend.file) { + final String filePath = cmd.getFile(); + if (StringUtils.isBlank(filePath)) { + return new CreateImageTransferAnswer(cmd, false, "file path is empty for file backend."); + } + payload.put("file", filePath); + } else { + String socket = cmd.getSocket(); + final String exportName = cmd.getExportName(); + if (StringUtils.isBlank(socket)) { + return new CreateImageTransferAnswer(cmd, false, "Empty socket."); + } + if (StringUtils.isBlank(exportName)) { + return new CreateImageTransferAnswer(cmd, false, "exportName is empty."); + } + payload.put("socket", "/tmp/imagetransfer/" + socket + ".sock"); + payload.put("export", exportName); + String checkpointId = cmd.getCheckpointId(); + if (checkpointId != null) { + payload.put("export_bitmap", cmd.getCheckpointId()); + } + } + + final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT; + String listenAddress = resource.getImageServerListenAddress(); + if (StringUtils.isBlank(listenAddress)) { + listenAddress = resource.getPrivateIp(); + } + if (!startImageServerIfNotRunning(imageServerPort, listenAddress, resource)) { + return new CreateImageTransferAnswer(cmd, false, "Failed to start image server."); + } + + if (!ImageServerControlSocket.registerTransfer(transferId, payload)) { + return new CreateImageTransferAnswer(cmd, false, "Failed to register transfer with image server."); + } + + final String transferScheme = resource.isImageServerTlsEnabled() ? "https" : "http"; + final String transferUrl = String.format("%s://%s:%d/images/%s", transferScheme, listenAddress, imageServerPort, transferId); + return new CreateImageTransferAnswer(cmd, true, "Image transfer prepared on KVM host.", transferId, transferUrl); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java new file mode 100644 index 000000000000..ddb84ab29cb3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java @@ -0,0 +1,79 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.Map; + +import org.apache.cloudstack.backup.DeleteVmCheckpointCommand; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = DeleteVmCheckpointCommand.class) +public class LibvirtDeleteVmCheckpointCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(DeleteVmCheckpointCommand cmd, LibvirtComputingResource resource) { + if (cmd.isStoppedVM()) { + return deleteBitmapsOnDisks(cmd); + } + return deleteDomainCheckpoint(cmd); + } + + private Answer deleteDomainCheckpoint(DeleteVmCheckpointCommand cmd) { + String vmName = cmd.getVmName(); + String checkpointId = cmd.getCheckpointId(); + String virshCmd = String.format("virsh checkpoint-delete %s %s", vmName, checkpointId); + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(virshCmd); + String result = script.execute(); + if (result != null) { + return new Answer(cmd, false, "Failed to delete checkpoint: " + result); + } + return new Answer(cmd, true, "Checkpoint deleted"); + } + + /** + * Stopped VM: persistent bitmaps on disk images ({@code qemu-img bitmap --remove}), matching {@link LibvirtStartBackupCommandWrapper} bitmap --add. + */ + private Answer deleteBitmapsOnDisks(DeleteVmCheckpointCommand cmd) { + String checkpointId = cmd.getCheckpointId(); + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + if (diskPathUuidMap == null || diskPathUuidMap.isEmpty()) { + return new Answer(cmd, false, "No disks provided for bitmap removal"); + } + for (Map.Entry entry : diskPathUuidMap.entrySet()) { + String diskPath = entry.getKey(); + Script script = new Script("qemu-img"); + script.add("bitmap"); + script.add("--remove"); + script.add(diskPath); + script.add(checkpointId); + String result = script.execute(); + if (result != null) { + return new Answer(cmd, false, + "Failed to remove bitmap " + checkpointId + " from disk " + diskPath + ": " + result); + } + } + return new Answer(cmd, true, "Checkpoint bitmap removed from disks"); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java new file mode 100644 index 000000000000..4b02c20c6d26 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeBackupCompressionCommandWrapper.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +import org.apache.cloudstack.backup.FinalizeBackupCompressionCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +@ResourceWrapper(handles = FinalizeBackupCompressionCommand.class) +public class LibvirtFinalizeBackupCompressionCommandWrapper extends CommandWrapper { + @Override + public Answer execute(FinalizeBackupCompressionCommand command, LibvirtComputingResource serverResource) { + KVMStoragePool storagePool = null; + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + long totalPhysicalSize = 0; + + if (command.isCleanup()) { + logger.info("Cleaning up compressed backup deltas [{}].", command.getBackupDeltaTOList()); + } else { + logger.info("Finalizing backup compression for deltas [{}].", command.getBackupDeltaTOList()); + } + try { + storagePool = storagePoolManager.getStoragePoolByURI(command.getBackupDeltaTOList().get(0).getDataStore().getUrl()); + for (BackupDeltaTO delta : command.getBackupDeltaTOList()) { + Path deltaPath = Path.of(storagePool.getLocalPathFor(delta.getPath())); + Path compressedDeltaPath = Path.of(deltaPath + ".comp"); + + if (command.isCleanup()) { + logger.debug("Cleaning up backup delta at [{}].", compressedDeltaPath); + Files.deleteIfExists(compressedDeltaPath); + continue; + } + + logger.debug("Moving compressed backup delta at [{}] to [{}].", compressedDeltaPath, deltaPath); + Files.move(compressedDeltaPath, deltaPath, StandardCopyOption.REPLACE_EXISTING); + totalPhysicalSize += Files.size(deltaPath); + } + } catch (IOException e) { + return new Answer(command, e); + } finally { + if (storagePool != null) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + return new Answer(command, true, String.valueOf(totalPhysicalSize)); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java new file mode 100644 index 000000000000..3d9f6563d5eb --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java @@ -0,0 +1,101 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.FinalizeImageTransferCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.ImageServerControlSocket; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = FinalizeImageTransferCommand.class) +public class LibvirtFinalizeImageTransferCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + private boolean stopImageServer(int imageServerPort, LibvirtComputingResource resource) { + String unitName = resource.IMAGE_SERVER_SYSTEMD_UNIT_NAME; + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult != null) { + logger.info("Image server not running, resetting failed state"); + resetService(unitName); + removeFirewallRule(imageServerPort); + return true; + } + + Script stopScript = new Script("/bin/bash", logger); + stopScript.add("-c"); + stopScript.add(String.format("systemctl stop %s", unitName)); + stopScript.execute(); + resetService(unitName); + logger.info("Image server {} stopped", unitName); + + removeFirewallRule(imageServerPort); + + return true; + } + + private void removeFirewallRule(int port) { + String rule = String.format("-p tcp -m state --state NEW -m tcp --dport %d -j ACCEPT", port); + Script removeScript = new Script("/bin/bash", logger); + removeScript.add("-c"); + removeScript.add(String.format("iptables -D INPUT %s || true", rule)); + String result = removeScript.execute(); + if (result != null && !result.isEmpty() && !result.contains("iptables: Bad rule")) { + logger.debug("Firewall rule removal result for port {}: {}", port, result); + } else { + logger.info("Firewall rule removed for port {} (or did not exist)", port); + } + } + + public Answer execute(FinalizeImageTransferCommand cmd, LibvirtComputingResource resource) { + final String transferId = cmd.getTransferId(); + final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT; + if (StringUtils.isBlank(transferId)) { + return new Answer(cmd, false, "transferId is empty."); + } + + int activeTransfers = ImageServerControlSocket.unregisterTransfer(transferId); + if (activeTransfers < 0) { + logger.warn("Could not reach image server to unregister transfer {}; assuming server is down", transferId); + stopImageServer(imageServerPort, resource); + return new Answer(cmd, true, "Image transfer finalized (server unreachable, forced stop)."); + } + + if (activeTransfers == 0) { + stopImageServer(imageServerPort, resource); + } + + return new Answer(cmd, true, "Image transfer finalized."); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java index 114b27d3a5b7..c0887415c650 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java @@ -22,6 +22,7 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.GetRemoteVmsAnswer; import com.cloud.agent.api.GetRemoteVmsCommand; +import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtConnection; import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; @@ -97,6 +98,7 @@ private UnmanagedInstanceTO getUnmanagedInstance(LibvirtComputingResource libvir if (parser.getCpuTuneDef() !=null) { instance.setCpuSpeed(parser.getCpuTuneDef().getShares()); } + instance.setHypervisorType(Hypervisor.HypervisorType.KVM.name()); instance.setPowerState(getPowerState(libvirtComputingResource.getVmState(conn,domain.getName()))); instance.setNics(getUnmanagedInstanceNics(parser.getInterfaces())); instance.setDisks(getUnmanagedInstanceDisks(parser.getDisks(),libvirtComputingResource, domain)); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java index 419b54492583..424622dfc2ce 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetStorageStatsCommandWrapper.java @@ -36,7 +36,7 @@ public final class LibvirtGetStorageStatsCommandWrapper extends CommandWrapper getIpAddresses(String output, String macAddress) { continue; } String device = parts[0]; - String mac = parts[1]; + String mac = parts[parts.length - 3]; if (found) { if (!device.equals("-") || !mac.equals("-")) { break; @@ -128,8 +128,8 @@ private Pair getIpAddresses(String output, String macAddress) { continue; } found = true; - String ipFamily = parts[2]; - String ipPart = parts[3].split("/")[0]; + String ipFamily = parts[parts.length - 2]; + String ipPart = parts[parts.length - 1].split("/")[0]; if (ipFamily.equals("ipv4")) { ipv4 = ipPart; } else if (ipFamily.equals("ipv6")) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java index 6d918435277b..1109d240a035 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java @@ -52,7 +52,7 @@ public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapper { static final List STORAGE_POOL_TYPES_SUPPORTED_BY_QEMU_IMG = Arrays.asList(StoragePoolType.NetworkFilesystem, - StoragePoolType.Filesystem, StoragePoolType.RBD); + StoragePoolType.Filesystem, StoragePoolType.RBD, StoragePoolType.SharedMountPoint); @Override public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtComputingResource libvirtComputingResource) { @@ -62,7 +62,7 @@ public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtCom final String keyword = command.getKeyword(); final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); - final KVMStoragePool storagePool = storagePoolMgr.getStoragePool(pool.getType(), pool.getUuid(), true); + final KVMStoragePool storagePool = storagePoolMgr.getStoragePool(pool.getType(), pool.getUuid(), true, true); if (StringUtils.isNotBlank(volumePath)) { return addVolumeByVolumePath(command, storagePool, volumePath); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java index 5602da156799..28e24a9e0f2d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java @@ -18,22 +18,9 @@ // package com.cloud.hypervisor.kvm.resource.wrapper; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; -import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.vm.UnmanagedInstanceTO; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; import com.cloud.agent.api.Answer; import com.cloud.agent.api.ImportConvertedInstanceAnswer; @@ -44,20 +31,14 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; -import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; -import com.cloud.storage.Storage; -import com.cloud.utils.FileUtil; -import com.cloud.utils.Pair; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.utils.script.Script; +import org.apache.commons.collections4.CollectionUtils; @ResourceWrapper(handles = ImportConvertedInstanceCommand.class) -public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper { +public class LibvirtImportConvertedInstanceCommandWrapper extends LibvirtBaseConvertCommandWrapper { @Override public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResource serverResource) { @@ -79,16 +60,22 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour List temporaryDisks = xmlParser == null ? getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid) : - getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath); + getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath, temporaryConvertPath, temporaryConvertUuid); - List disks = null; + List disks; if (forceConvertToPool) { // Force flag to use the conversion path, no need to move disks disks = temporaryDisks; } else { disks = moveTemporaryDisksToDestination(temporaryDisks, destinationStoragePools, storagePoolMgr); - cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid); + cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid, true); + } + + if (CollectionUtils.isEmpty(disks)) { + String msg = String.format("Unable to import the converted disks for VM %s from destination pools", sourceInstanceName); + logger.error(msg); + return new ImportConvertedInstanceAnswer(cmd, false, msg); } UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid, @@ -106,200 +93,4 @@ public Answer execute(ImportConvertedInstanceCommand cmd, LibvirtComputingResour } } } - - protected KVMStoragePool getTemporaryStoragePool(DataStoreTO conversionTemporaryLocation, KVMStoragePoolManager storagePoolMgr) { - if (conversionTemporaryLocation instanceof NfsTO) { - NfsTO nfsTO = (NfsTO) conversionTemporaryLocation; - return storagePoolMgr.getStoragePoolByURI(nfsTO.getUrl()); - } else { - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) conversionTemporaryLocation; - return storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - } - } - - protected List getTemporaryDisksFromParsedXml(KVMStoragePool pool, LibvirtDomainXMLParser xmlParser, String convertedBasePath) { - List disksDefs = xmlParser.getDisks(); - disksDefs = disksDefs.stream().filter(x -> x.getDiskType() == LibvirtVMDef.DiskDef.DiskType.FILE && - x.getDeviceType() == LibvirtVMDef.DiskDef.DeviceType.DISK).collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksDefs)) { - String err = String.format("Cannot find any disk defined on the converted XML domain %s.xml", convertedBasePath); - logger.error(err); - throw new CloudRuntimeException(err); - } - sanitizeDisksPath(disksDefs); - return getPhysicalDisksFromDefPaths(disksDefs, pool); - } - - private List getPhysicalDisksFromDefPaths(List disksDefs, KVMStoragePool pool) { - List disks = new ArrayList<>(); - for (LibvirtVMDef.DiskDef diskDef : disksDefs) { - KVMPhysicalDisk physicalDisk = pool.getPhysicalDisk(diskDef.getDiskPath()); - disks.add(physicalDisk); - } - return disks; - } - - protected List getTemporaryDisksWithPrefixFromTemporaryPool(KVMStoragePool pool, String path, String prefix) { - String msg = String.format("Could not parse correctly the converted XML domain, checking for disks on %s with prefix %s", path, prefix); - logger.info(msg); - pool.refresh(); - List disksWithPrefix = pool.listPhysicalDisks() - .stream() - .filter(x -> x.getName().startsWith(prefix) && !x.getName().endsWith(".xml")) - .collect(Collectors.toList()); - if (CollectionUtils.isEmpty(disksWithPrefix)) { - msg = String.format("Could not find any converted disk with prefix %s on temporary location %s", prefix, path); - logger.error(msg); - throw new CloudRuntimeException(msg); - } - return disksWithPrefix; - } - - private void cleanupDisksAndDomainFromTemporaryLocation(List disks, - KVMStoragePool temporaryStoragePool, - String temporaryConvertUuid) { - for (KVMPhysicalDisk disk : disks) { - logger.info(String.format("Cleaning up temporary disk %s after conversion from temporary location", disk.getName())); - temporaryStoragePool.deletePhysicalDisk(disk.getName(), Storage.ImageFormat.QCOW2); - } - logger.info(String.format("Cleaning up temporary domain %s after conversion from temporary location", temporaryConvertUuid)); - FileUtil.deleteFiles(temporaryStoragePool.getLocalPath(), temporaryConvertUuid, ".xml"); - } - - protected void sanitizeDisksPath(List disks) { - for (LibvirtVMDef.DiskDef disk : disks) { - String[] diskPathParts = disk.getDiskPath().split("/"); - String relativePath = diskPathParts[diskPathParts.length - 1]; - disk.setDiskPath(relativePath); - } - } - - protected List moveTemporaryDisksToDestination(List temporaryDisks, - List destinationStoragePools, - KVMStoragePoolManager storagePoolMgr) { - List targetDisks = new ArrayList<>(); - if (temporaryDisks.size() != destinationStoragePools.size()) { - String warn = String.format("Discrepancy between the converted instance disks (%s) " + - "and the expected number of disks (%s)", temporaryDisks.size(), destinationStoragePools.size()); - logger.warn(warn); - } - for (int i = 0; i < temporaryDisks.size(); i++) { - String poolPath = destinationStoragePools.get(i); - KVMStoragePool destinationPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, poolPath); - if (destinationPool == null) { - String err = String.format("Could not find a storage pool by URI: %s", poolPath); - logger.error(err); - continue; - } - if (destinationPool.getType() != Storage.StoragePoolType.NetworkFilesystem) { - String err = String.format("Storage pool by URI: %s is not an NFS storage", poolPath); - logger.error(err); - continue; - } - KVMPhysicalDisk sourceDisk = temporaryDisks.get(i); - if (logger.isDebugEnabled()) { - String msg = String.format("Trying to copy converted instance disk number %s from the temporary location %s" + - " to destination storage pool %s", i, sourceDisk.getPool().getLocalPath(), destinationPool.getUuid()); - logger.debug(msg); - } - - String destinationName = UUID.randomUUID().toString(); - - KVMPhysicalDisk destinationDisk = storagePoolMgr.copyPhysicalDisk(sourceDisk, destinationName, destinationPool, 7200 * 1000); - targetDisks.add(destinationDisk); - } - return targetDisks; - } - - private UnmanagedInstanceTO getConvertedUnmanagedInstance(String baseName, - List vmDisks, - LibvirtDomainXMLParser xmlParser) { - UnmanagedInstanceTO instanceTO = new UnmanagedInstanceTO(); - instanceTO.setName(baseName); - instanceTO.setDisks(getUnmanagedInstanceDisks(vmDisks, xmlParser)); - instanceTO.setNics(getUnmanagedInstanceNics(xmlParser)); - return instanceTO; - } - - private List getUnmanagedInstanceNics(LibvirtDomainXMLParser xmlParser) { - List nics = new ArrayList<>(); - if (xmlParser != null) { - List interfaces = xmlParser.getInterfaces(); - for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { - UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); - nic.setMacAddress(interfaceDef.getMacAddress()); - nic.setNicId(interfaceDef.getBrName()); - nic.setAdapterType(interfaceDef.getModel().toString()); - nics.add(nic); - } - } - return nics; - } - - protected List getUnmanagedInstanceDisks(List vmDisks, LibvirtDomainXMLParser xmlParser) { - List instanceDisks = new ArrayList<>(); - List diskDefs = xmlParser != null ? xmlParser.getDisks() : null; - for (int i = 0; i< vmDisks.size(); i++) { - KVMPhysicalDisk physicalDisk = vmDisks.get(i); - KVMStoragePool storagePool = physicalDisk.getPool(); - UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); - disk.setPosition(i); - Pair storagePoolHostAndPath = getNfsStoragePoolHostAndPath(storagePool); - disk.setDatastoreHost(storagePoolHostAndPath.first()); - disk.setDatastorePath(storagePoolHostAndPath.second()); - disk.setDatastoreName(storagePool.getUuid()); - disk.setDatastoreType(storagePool.getType().name()); - disk.setCapacity(physicalDisk.getVirtualSize()); - disk.setFileBaseName(physicalDisk.getName()); - if (CollectionUtils.isNotEmpty(diskDefs)) { - LibvirtVMDef.DiskDef diskDef = diskDefs.get(i); - disk.setController(diskDef.getBusType() != null ? diskDef.getBusType().toString() : LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } else { - // If the job is finished but we cannot parse the XML, the guest VM can use the virtio driver - disk.setController(LibvirtVMDef.DiskDef.DiskBus.VIRTIO.toString()); - } - instanceDisks.add(disk); - } - return instanceDisks; - } - - protected Pair getNfsStoragePoolHostAndPath(KVMStoragePool storagePool) { - String sourceHostIp = null; - String sourcePath = null; - List commands = new ArrayList<>(); - commands.add(new String[]{Script.getExecutableAbsolutePath("mount")}); - commands.add(new String[]{Script.getExecutableAbsolutePath("grep"), storagePool.getLocalPath()}); - String storagePoolMountPoint = Script.executePipedCommands(commands, 0).second(); - logger.debug(String.format("NFS Storage pool: %s - local path: %s, mount point: %s", storagePool.getUuid(), storagePool.getLocalPath(), storagePoolMountPoint)); - if (StringUtils.isNotEmpty(storagePoolMountPoint)) { - String[] res = storagePoolMountPoint.strip().split(" "); - res = res[0].split(":"); - if (res.length > 1) { - sourceHostIp = res[0].strip(); - sourcePath = res[1].strip(); - } - } - return new Pair<>(sourceHostIp, sourcePath); - } - - protected LibvirtDomainXMLParser parseMigratedVMXmlDomain(String installPath) throws IOException { - String xmlPath = String.format("%s.xml", installPath); - if (!new File(xmlPath).exists()) { - String err = String.format("Conversion failed. Unable to find the converted XML domain, expected %s", xmlPath); - logger.error(err); - throw new CloudRuntimeException(err); - } - InputStream is = new BufferedInputStream(new FileInputStream(xmlPath)); - String xml = IOUtils.toString(is, Charset.defaultCharset()); - final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); - try { - parser.parseDomainXML(xml); - return parser; - } catch (RuntimeException e) { - String err = String.format("Error parsing the converted instance XML domain at %s: %s", xmlPath, e.getMessage()); - logger.error(err, e); - logger.debug(xml); - return null; - } - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java index ad0678080ed7..ad687cbb3be1 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMergeDiskOnlyVMSnapshotCommandWrapper.java @@ -21,42 +21,26 @@ import com.cloud.agent.api.Answer; import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand; -import com.cloud.agent.api.storage.SnapshotMergeTreeTO; -import com.cloud.agent.api.to.DataObjectType; -import com.cloud.agent.api.to.DataTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; -import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; -import com.cloud.hypervisor.kvm.storage.KVMStoragePool; -import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; -import org.apache.cloudstack.storage.to.SnapshotObjectTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; -import org.apache.cloudstack.utils.qemu.QemuImgFile; -import org.libvirt.Connect; -import org.libvirt.Domain; import org.libvirt.LibvirtException; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; -import java.util.stream.Collectors; @ResourceWrapper(handles = MergeDiskOnlyVmSnapshotCommand.class) public class LibvirtMergeDiskOnlyVMSnapshotCommandWrapper extends CommandWrapper { @Override public Answer execute(MergeDiskOnlyVmSnapshotCommand command, LibvirtComputingResource serverResource) { - VirtualMachine.State vmState = command.getVmState(); + boolean isVmRunning = command.isVmRunning(); try { - if (VirtualMachine.State.Running.equals(vmState)) { + if (isVmRunning) { return mergeDiskOnlySnapshotsForRunningVM(command, serverResource); } return mergeDiskOnlySnapshotsForStoppedVM(command, serverResource); @@ -66,83 +50,28 @@ public Answer execute(MergeDiskOnlyVmSnapshotCommand command, LibvirtComputingRe } protected Answer mergeDiskOnlySnapshotsForStoppedVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws QemuImgException, LibvirtException { - QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout()); - KVMStoragePoolManager storageManager = resource.getStoragePoolMgr(); + List deltaMergeTreeTOList = cmd.getDeltaMergeTreeToList(); - List snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList(); + logger.debug("Merging deltas for stopped VM [{}] using the following Delta Merge Trees [{}].", cmd.getVmName(), deltaMergeTreeTOList); - logger.debug("Merging disk-only snapshots for stopped VM [{}] using the following Snapshot Merge Trees [{}].", cmd.getVmName(), snapshotMergeTreeTOList); - - for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) { - DataTO parentTo = snapshotMergeTreeTO.getParent(); - PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) parentTo.getDataStore(); - KVMStoragePool storagePool = storageManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); - String childLocalPath = storagePool.getLocalPathFor(snapshotMergeTreeTO.getChild().getPath()); - - QemuImgFile parent = new QemuImgFile(storagePool.getLocalPathFor(parentTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2); - QemuImgFile child = new QemuImgFile(childLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); - - logger.debug("Committing child delta [{}] into parent snapshot [{}].", parentTo, snapshotMergeTreeTO.getChild()); - qemuImg.commit(child, parent, true); - - List grandChildren = snapshotMergeTreeTO.getGrandChildren().stream() - .map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) - .collect(Collectors.toList()); - - logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parent.getFileName()); - for (QemuImgFile grandChild : grandChildren) { - qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); - } - - logger.debug("Deleting child at [{}] as it is useless.", childLocalPath); + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOList) { try { - Files.deleteIfExists(Path.of(childLocalPath)); - } catch (IOException e) { - return new Answer(cmd, e); + resource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } catch (IOException ex) { + return new Answer(cmd, ex); } } return new Answer(cmd, true, null); } - protected Answer mergeDiskOnlySnapshotsForRunningVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws LibvirtException, QemuImgException { + protected Answer mergeDiskOnlySnapshotsForRunningVM(MergeDiskOnlyVmSnapshotCommand cmd, LibvirtComputingResource resource) throws QemuImgException, LibvirtException { String vmName = cmd.getVmName(); - List snapshotMergeTreeTOList = cmd.getSnapshotMergeTreeToList(); - - LibvirtUtilitiesHelper libvirtUtilitiesHelper = resource.getLibvirtUtilitiesHelper(); - Connect conn = libvirtUtilitiesHelper.getConnection(); - Domain domain = resource.getDomain(conn, vmName); - List disks = resource.getDisks(conn, vmName); - KVMStoragePoolManager storageManager = resource.getStoragePoolMgr(); - QemuImg qemuImg = new QemuImg(resource.getCmdsTimeout()); + List deltaMergeTreeTOs = cmd.getDeltaMergeTreeToList(); - logger.debug("Merging disk-only snapshots for running VM [{}] using the following Snapshot Merge Trees [{}].", vmName, snapshotMergeTreeTOList); + logger.debug("Merging deltas for running VM [{}] using the following Delta Merge Trees [{}].", vmName, deltaMergeTreeTOs); - for (SnapshotMergeTreeTO mergeTreeTO : snapshotMergeTreeTOList) { - DataTO childTO = mergeTreeTO.getChild(); - SnapshotObjectTO parentSnapshotTO = (SnapshotObjectTO) mergeTreeTO.getParent(); - VolumeObjectTO volumeObjectTO = parentSnapshotTO.getVolume(); - KVMStoragePool storagePool = libvirtUtilitiesHelper.getPrimaryPoolFromDataTo(volumeObjectTO, storageManager); - - boolean active = DataObjectType.VOLUME.equals(childTO.getObjectType()); - String label = resource.getDiskWithPathOfVolumeObjectTO(disks, volumeObjectTO).getDiskLabel(); - String parentSnapshotLocalPath = storagePool.getLocalPathFor(parentSnapshotTO.getPath()); - String childDeltaPath = storagePool.getLocalPathFor(childTO.getPath()); - - logger.debug("Found label [{}] for [{}]. Will merge delta at [{}] into delta at [{}].", label, volumeObjectTO, parentSnapshotLocalPath, childDeltaPath); - - resource.mergeSnapshotIntoBaseFile(domain, label, parentSnapshotLocalPath, childDeltaPath, active, childTO.getPath(), - volumeObjectTO, conn); - - QemuImgFile parent = new QemuImgFile(parentSnapshotLocalPath, QemuImg.PhysicalDiskFormat.QCOW2); - - List grandChildren = mergeTreeTO.getGrandChildren().stream() - .map(snapshotTo -> new QemuImgFile(storagePool.getLocalPathFor(snapshotTo.getPath()), QemuImg.PhysicalDiskFormat.QCOW2)) - .collect(Collectors.toList()); - - logger.debug("Rebasing grandChildren [{}] into parent at [{}].", grandChildren, parentSnapshotLocalPath); - for (QemuImgFile grandChild : grandChildren) { - qemuImg.rebase(grandChild, parent, parent.getFormat().toString(), false); - } + for (DeltaMergeTreeTO deltaMergeTreeTO : deltaMergeTreeTOs) { + resource.mergeDeltaForRunningVm(deltaMergeTreeTO, vmName, deltaMergeTreeTO.getVolumeObjectTO()); } return new Answer(cmd, true, null); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java new file mode 100644 index 000000000000..f786a0821d05 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper.java @@ -0,0 +1,132 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.MigrateBackupsBetweenSecondaryStoragesCommand; +import com.cloud.agent.api.MigrateBetweenSecondaryStoragesCommandAnswer; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtMigrateResourceBetweenSecondaryStorages; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.commons.lang3.BooleanUtils; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@ResourceWrapper(handles = MigrateBackupsBetweenSecondaryStoragesCommand.class) +public class LibvirtMigrateBackupsBetweenSecondaryStoragesCommandWrapper extends LibvirtMigrateResourceBetweenSecondaryStorages { + + @Override + public Answer execute(MigrateBackupsBetweenSecondaryStoragesCommand command, LibvirtComputingResource serverResource) { + resourceType = BACKUP; + filesToRemove = new HashSet<>(); + resourcesToUpdate = new ArrayList<>(); + wait = command.getWait() * 1000; + + DataStoreTO srcDataStore = command.getSrcDataStore(); + DataStoreTO destDataStore = command.getDestDataStore(); + KVMStoragePoolManager storagePoolManager = serverResource.getStoragePoolMgr(); + + Set imagePools = new HashSet<>(); + KVMStoragePool destImagePool = storagePoolManager.getStoragePoolByURI(destDataStore.getUrl()); + imagePools.add(destImagePool); + + String imagePoolUrl; + KVMStoragePool imagePool = null; + + List> backupChains = command.getBackupChain(); + + try { + Map parentBackupPathMap = new HashMap<>(); + Map parentBackupMigrationMap = new HashMap<>(); + + Map backupPathMap = new HashMap<>(); + Map backupMigrationMap = new HashMap<>(); + + for (List chain : backupChains) { + long lastBackupId = 0; + boolean backupWasMigrated = false; + + backupPathMap.clear(); + backupMigrationMap.clear(); + + for (DataTO backup : chain) { + lastBackupId = backup.getId(); + + imagePoolUrl = backup.getDataStore().getUrl(); + imagePool = storagePoolManager.getStoragePoolByURI(imagePoolUrl); + imagePools.add(imagePool); + + String volumeId = backup.getPath().split("/")[2]; + String resourceCurrentPath = imagePool.getLocalPathFor(backup.getPath()); + String resourceParentPath = parentBackupPathMap.get(volumeId); + + if (imagePoolUrl.equals(srcDataStore.getUrl())) { + backupPathMap.put(volumeId, copyResourceToDestDataStore(backup, resourceCurrentPath, destImagePool, resourceParentPath)); + backupMigrationMap.put(volumeId, true); + backupWasMigrated = true; + } else { + if (BooleanUtils.isTrue(parentBackupMigrationMap.get(volumeId))) { + backupPathMap.put(volumeId, rebaseResourceToNewParentPath(resourceCurrentPath, resourceParentPath)); + } else { + backupPathMap.put(volumeId, resourceCurrentPath); + } + backupMigrationMap.put(volumeId, false); + } + } + + parentBackupPathMap.clear(); + parentBackupPathMap.putAll(backupPathMap); + + parentBackupMigrationMap.clear(); + parentBackupMigrationMap.putAll(backupMigrationMap); + + if (backupWasMigrated) { + resourcesToUpdate.add(new Pair<>(lastBackupId, null)); + } + } + } catch (LibvirtException | QemuImgException e) { + logger.error("Exception while migrating backups [{}] to secondary storage [{}] due to: [{}].", + command.getBackupChain(), imagePool, e.getMessage(), e); + return new MigrateBetweenSecondaryStoragesCommandAnswer(command, false, "Migration of backups between secondary storages failed", resourcesToUpdate); + } finally { + for (String file : filesToRemove) { + removeResourceFromSourceDataStore(file); + } + + for (KVMStoragePool storagePool : imagePools) { + storagePoolManager.deleteStoragePool(storagePool.getType(), storagePool.getUuid()); + } + } + + return new MigrateBetweenSecondaryStoragesCommandAnswer(command, true, "success", resourcesToUpdate); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index 43607edc53a5..ed02ae6da38d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -1,5 +1,3 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file @@ -42,9 +40,16 @@ import javax.xml.transform.TransformerException; import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.hypervisor.kvm.resource.LibvirtGpuDef; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.Ternary; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.collections4.CollectionUtils; @@ -69,7 +74,6 @@ import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; -import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; @@ -82,11 +86,6 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; import com.cloud.hypervisor.kvm.resource.MigrateKVMAsync; import com.cloud.hypervisor.kvm.resource.VifDriver; -import com.cloud.resource.CommandWrapper; -import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.Ternary; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VirtualMachine; @ResourceWrapper(handles = MigrateCommand.class) public final class LibvirtMigrateCommandWrapper extends CommandWrapper { @@ -117,7 +116,8 @@ public Answer execute(final MigrateCommand command, final LibvirtComputingResour Command.State commandState = null; List ifaces = null; - List disks; + List disks = new ArrayList<>(); + VirtualMachineTO to = null; Domain dm = null; Connect dconn = null; @@ -136,7 +136,7 @@ public Answer execute(final MigrateCommand command, final LibvirtComputingResour if (logger.isDebugEnabled()) { logger.debug(String.format("Found domain with name [%s]. Starting VM migration to host [%s].", vmName, destinationUri)); } - VirtualMachineTO to = command.getVirtualMachine(); + to = command.getVirtualMachine(); dm = conn.domainLookupByName(vmName); /* @@ -259,6 +259,12 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. final int migrateDowntime = libvirtComputingResource.getMigrateDowntime(); boolean isMigrateDowntimeSet = false; + final int migrateWait = libvirtComputingResource.getMigrateWait(); + logger.info("vm.migrate.wait value set to: {} secs for VM: {}", migrateWait, vmName); + + final int migratePauseAfter = libvirtComputingResource.getMigratePauseAfter(); + logger.info("vm.migrate.pauseafter value set to: {} ms for VM: {}", migratePauseAfter, vmName); + while (!executor.isTerminated()) { Thread.sleep(100); sleeptime += 100; @@ -278,8 +284,6 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } // abort the vm migration if the job is executed more than vm.migrate.wait - final int migrateWait = libvirtComputingResource.getMigrateWait(); - logger.info("vm.migrate.wait value set to: {}for VM: {}", migrateWait, vmName); if (migrateWait > 0 && sleeptime > migrateWait * 1000) { DomainState state = null; try { @@ -306,8 +310,6 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. } // pause vm if we meet the vm.migrate.pauseafter threshold and not already paused - final int migratePauseAfter = libvirtComputingResource.getMigratePauseAfter(); - logger.info("vm.migrate.pauseafter value set to: {} for VM: {}", migratePauseAfter, vmName); if (migratePauseAfter > 0 && sleeptime > migratePauseAfter) { DomainState state = null; try { @@ -336,6 +338,14 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. logger.debug(String.format("Cleaning the disks of VM [%s] in the source pool after VM migration finished.", vmName)); } resumeDomainIfPaused(destDomain, vmName); + + // For cross-pool CLVM migration, skip deactivation so the source LV stays + // active (in shared mode) and deletion can route directly to the source host + // without fanning out across the cluster to find an inactive LV. + if (to != null && !command.isClvmCrossPoolMigration()) { + LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.DEACTIVATE); + } + deleteOrDisconnectDisksOnSourcePool(libvirtComputingResource, migrateDiskInfoList, disks); libvirtComputingResource.cleanOldSecretsByDiskDef(conn, disks); } @@ -382,6 +392,10 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. if (destDomain != null) { destDomain.free(); } + // Revert CLVM volumes to exclusive mode on failure + if (to != null && result != null) { + LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE); + } } catch (final LibvirtException e) { logger.trace("Ignoring libvirt error.", e); } @@ -681,7 +695,7 @@ protected String replaceDpdkInterfaces(String xmlDesc, Map dpdkP protected void deleteOrDisconnectDisksOnSourcePool(final LibvirtComputingResource libvirtComputingResource, final List migrateDiskInfoList, List disks) { for (DiskDef disk : disks) { - MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); if (migrateDiskInfo != null && migrateDiskInfo.isSourceDiskOnStorageFileSystem()) { deleteLocalVolume(disk.getDiskPath()); } else { @@ -798,7 +812,10 @@ protected String replaceStorage(String xmlDesc, Map]*type=['\"]vnc['\"][^>]*passwd=['\"])([^'\"]*)(['\"])", "$1*****$3"); } + + /** + * Checks if any of the destination disks in the migration target a CLVM or CLVM_NG storage pool. + * This is used to determine if incremental migration should be disabled to avoid libvirt + * precreate errors with QCOW2-on-LVM setups. + * + * @param mapMigrateStorage the map containing migration disk information with destination pool types + * @return true if any destination disk targets CLVM or CLVM_NG, false otherwise + */ + protected boolean hasClvmDestinationDisks(Map mapMigrateStorage) { + if (MapUtils.isEmpty(mapMigrateStorage)) { + return false; + } + + try { + for (Map.Entry entry : mapMigrateStorage.entrySet()) { + MigrateCommand.MigrateDiskInfo diskInfo = entry.getValue(); + if (isClvmBlockDevice(diskInfo)) { + logger.debug("Found disk targeting CLVM/CLVM_NG destination pool"); + return true; + } + } + } catch (final Exception e) { + logger.debug("Failed to check for CLVM destination disks: {}. Assuming no CLVM disks.", e.getMessage()); + } + + return false; + } + + private boolean isClvmBlockDevice(MigrateCommand.MigrateDiskInfo diskInfo) { + if (diskInfo == null ||diskInfo.getDestPoolType() == null) { + return false; + } + return (Storage.StoragePoolType.CLVM.equals(diskInfo.getDestPoolType()) || Storage.StoragePoolType.CLVM_NG.equals(diskInfo.getDestPoolType())); + } + + /** + * Determines if the driver type should be updated during migration based on CLVM involvement. + * The driver type needs to be updated when: + * - Managed storage is being migrated, OR + * - Source pool is CLVM or CLVM_NG, OR + * - Destination pool is CLVM or CLVM_NG + * + * This ensures the libvirt XML driver type matches the destination format (raw/qcow2/etc). + * + * @param migrateStorageManaged true if migrating managed storage + * @param migrateDiskInfo the migration disk information containing source and destination pool types + * @return true if driver type should be updated, false otherwise + */ + private boolean shouldUpdateDriverTypeForMigration(boolean migrateStorageManaged, + MigrateCommand.MigrateDiskInfo migrateDiskInfo) { + boolean sourceIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getSourcePoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getSourcePoolType(); + + boolean destIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getDestPoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getDestPoolType(); + + boolean isClvmRelatedMigration = sourceIsClvm || destIsClvm; + return migrateStorageManaged || isClvmRelatedMigration; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java index 990cefda8f33..bc22d7bfd70a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java @@ -52,9 +52,19 @@ public Answer execute(final ModifyStoragePoolCommand command, final LibvirtCompu final KVMStoragePool storagepool; try { + Map poolDetails = command.getDetails(); + if (poolDetails == null) { + poolDetails = new HashMap<>(); + } + + // Ensure CLVM secure zero-fill setting has a default value if not provided by MS + if (!poolDetails.containsKey(KVMStoragePool.CLVM_SECURE_ZERO_FILL)) { + poolDetails.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + } + storagepool = storagePoolMgr.createStoragePool(command.getPool().getUuid(), command.getPool().getHost(), command.getPool().getPort(), command.getPool().getPath(), command.getPool() - .getUserInfo(), command.getPool().getType(), command.getDetails()); + .getUserInfo(), command.getPool().getType(), poolDetails); if (storagepool == null) { return new Answer(command, false, " Failed to create storage pool"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java new file mode 100644 index 000000000000..608770974dc1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java @@ -0,0 +1,82 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.PostMigrationAnswer; +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +/** + * Wrapper for PostMigrationCommand on KVM hypervisor. + * Handles post-migration tasks on the destination host after a VM has been successfully migrated. + * Primary responsibility: Convert CLVM volumes from shared mode to exclusive mode on destination. + */ +@ResourceWrapper(handles = PostMigrationCommand.class) +public final class LibvirtPostMigrationCommandWrapper extends CommandWrapper { + + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(final PostMigrationCommand command, final LibvirtComputingResource libvirtComputingResource) { + final VirtualMachineTO vm = command.getVirtualMachine(); + final String vmName = command.getVmName(); + + if (vm == null || vmName == null) { + return new PostMigrationAnswer(command, "VM or VM name is null"); + } + + logger.debug("Executing post-migration tasks for VM {} on destination host", vmName); + + try { + final Connect conn = LibvirtConnection.getConnectionByVmName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.debug("[CLVM Post-Migration] Processing volumes for VM {} to claim exclusive locks on any CLVM volumes", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE + ); + + logger.debug("Successfully completed post-migration tasks for VM {}", vmName); + return new PostMigrationAnswer(command); + + } catch (final LibvirtException e) { + logger.error("Libvirt error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } catch (final Exception e) { + logger.error("Error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java new file mode 100644 index 000000000000..c47760040c88 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java @@ -0,0 +1,84 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.PreMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import java.util.List; + +/** + * Handles PreMigrationCommand on the source host before live migration. + * Converts CLVM volume locks from exclusive to shared mode so the destination host can access them. + */ +@ResourceWrapper(handles = PreMigrationCommand.class) +public class LibvirtPreMigrationCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(PreMigrationCommand command, LibvirtComputingResource libvirtComputingResource) { + String vmName = command.getVmName(); + VirtualMachineTO vmSpec = command.getVirtualMachine(); + + logger.info("Preparing source host for migration of VM: {}", vmName); + + Connect conn = null; + Domain dm = null; + + try { + LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + dm = conn.domainLookupByName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.info("Converting CLVM volumes to shared mode for VM: {}", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vmSpec, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + + logger.info("Successfully prepared source host for migration of VM: {}", vmName); + return new Answer(command, true, "Source host prepared for migration"); + + } catch (LibvirtException e) { + logger.error("Failed to prepare source host for migration of VM: {}", vmName, e); + return new Answer(command, false, "Failed to prepare source host: " + e.getMessage()); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException e) { + logger.warn("Failed to free domain {}: {}", vmName, e.getMessage()); + } + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java index d9323df4477d..f7ca79127dad 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java @@ -21,6 +21,7 @@ import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.cloudstack.storage.configdrive.ConfigDrive; @@ -124,6 +125,19 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom return new PrepareForMigrationAnswer(command, "failed to connect physical disks to host"); } + // Activate CLVM volumes in shared mode on destination host for live migration + try { + List disks = libvirtComputingResource.getDisks(conn, vm.getName()); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + } catch (Exception e) { + logger.warn("Failed to activate CLVM volumes in shared mode on destination for VM {}: {}", + vm.getName(), e.getMessage(), e); + } + logger.info("Successfully prepared destination host for migration of VM {}", vm.getName()); return createPrepareForMigrationAnswer(command, dpdkInterfaceMapping, libvirtComputingResource, vm); } catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java new file mode 100644 index 000000000000..1d9ed5631fa1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareValidationCommandWrapper.java @@ -0,0 +1,91 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.backup.PrepareValidationCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.List; + +@ResourceWrapper(handles = PrepareValidationCommand.class) +public class LibvirtPrepareValidationCommandWrapper extends CommandWrapper { + @Override + public Answer execute(PrepareValidationCommand command, LibvirtComputingResource resource) { + List> backingFileAndVolumeList = command.getBackupToVolumeList(); + + List secondaryReferences = new ArrayList<>(); + KVMStoragePoolManager storagePoolMgr = resource.getStoragePoolMgr(); + try { + for (String imageStoreUri : command.getImageStoreSet()) { + secondaryReferences.add(storagePoolMgr.getStoragePoolByURI(imageStoreUri)); + } + + for (Pair backingFileAndVolume : backingFileAndVolumeList) { + logger.debug("Preparing volume [{}] for validation.", backingFileAndVolume.second()); + BackupDeltaTO backupDelta = backingFileAndVolume.first(); + DataStoreTO dataStoreTO = backupDelta.getDataStore(); + KVMStoragePool imageStore = storagePoolMgr.getStoragePoolByURI(dataStoreTO.getUrl()); + secondaryReferences.add(imageStore); + + createVolume(command, backingFileAndVolume, imageStore, backupDelta, storagePoolMgr); + } + } catch (LibvirtException | QemuImgException e) { + logger.error("Failed to prepare VM [{}] for validation due to:", backingFileAndVolumeList.get(0).second().getVmName(), e); + throw new CloudRuntimeException(e); + } finally { + for (KVMStoragePool secondary : secondaryReferences) { + storagePoolMgr.deleteStoragePool(secondary.getType(), secondary.getUuid()); + } + } + return new Answer(command); + } + + private void createVolume(PrepareValidationCommand command, Pair volumeAndBackingFile, KVMStoragePool imageStore, BackupDeltaTO backupDelta, + KVMStoragePoolManager storagePoolMgr) throws LibvirtException, QemuImgException { + String fullBackupPath = imageStore.getLocalPathFor(backupDelta.getPath()); + + VolumeObjectTO volumeObjectTO = volumeAndBackingFile.second(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullVolumePath = primaryStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + + QemuImgFile backup = new QemuImgFile(fullBackupPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile volume = new QemuImgFile(fullVolumePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = new QemuImg(command.getWait() * 1000); + + qemuImg.create(volume, backup); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java index e74923b281f6..5a7d6d2c203a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtReadyCommandWrapper.java @@ -34,6 +34,7 @@ import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.utils.script.Script; +import org.apache.commons.lang3.StringUtils; @ResourceWrapper(handles = ReadyCommand.class) public final class LibvirtReadyCommandWrapper extends CommandWrapper { @@ -50,6 +51,9 @@ public Answer execute(final ReadyCommand command, final LibvirtComputingResource if (libvirtComputingResource.hostSupportsInstanceConversion()) { hostDetails.put(Host.HOST_VIRTV2V_VERSION, libvirtComputingResource.getHostVirtV2vVersion()); } + hostDetails.put(Host.HOST_VDDK_SUPPORT, Boolean.toString(libvirtComputingResource.hostSupportsVddk())); + hostDetails.put(Host.HOST_VDDK_LIB_DIR, StringUtils.defaultString(libvirtComputingResource.getVddkLibDir())); + hostDetails.put(Host.HOST_VDDK_VERSION, StringUtils.defaultString(libvirtComputingResource.getVddkVersion())); if (libvirtComputingResource.hostSupportsOvfExport()) { hostDetails.put(Host.HOST_OVFTOOL_VERSION, libvirtComputingResource.getHostOvfToolVersion()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java index f2af46d4cc8a..a43b584dd6d6 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java @@ -113,7 +113,8 @@ public Answer execute(final ResizeVolumeCommand command, final LibvirtComputingR logger.debug("Resizing volume: " + path + ", from: " + toHumanReadableSize(currentSize) + ", to: " + toHumanReadableSize(newSize) + ", type: " + type + ", name: " + vmInstanceName + ", shrinkOk: " + shrinkOk); /* libvirt doesn't support resizing (C)LVM devices, and corrupts QCOW2 in some scenarios, so we have to do these via qemu-img */ - if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex + if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG + && pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex && vol.getFormat() != PhysicalDiskFormat.QCOW2) { logger.debug("Volume " + path + " can be resized by libvirt. Asking libvirt to resize the volume."); try { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index fd94013dd50c..cc2a0868fe17 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -41,9 +41,9 @@ import org.apache.commons.lang3.StringUtils; import org.libvirt.LibvirtException; -import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Locale; @@ -56,9 +56,33 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper/dev/null | grep -q '\"backing-filename\"'"; + + private String getVolumeUuidFromPath(String volumePath, PrimaryDataStoreTO volumePool) { + if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) { + Path path = Paths.get(volumePath); + String rscName = path.getParent().getFileName().toString(); + if (rscName.startsWith("cs-")) { + rscName = rscName.substring(3); + } + return rscName; + } else { + int lastIndex = volumePath.lastIndexOf("/"); + return volumePath.substring(lastIndex + 1); + } + } @Override public Answer execute(RestoreBackupCommand command, LibvirtComputingResource serverResource) { @@ -72,10 +96,10 @@ public Answer execute(RestoreBackupCommand command, LibvirtComputingResource ser List backedVolumeUUIDs = command.getBackupVolumesUUIDs(); List restoreVolumePools = command.getRestoreVolumePools(); List restoreVolumePaths = command.getRestoreVolumePaths(); - String restoreVolumeUuid = command.getRestoreVolumeUUID(); Integer mountTimeout = command.getMountTimeout() * 1000; - int timeout = command.getWait(); + int timeout = command.getWait() > 0 ? command.getWait() * 1000 : serverResource.getCmdsTimeout(); KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + List backupFiles = command.getBackupFiles(); String newVolumeId = null; try { @@ -83,14 +107,15 @@ public Answer execute(RestoreBackupCommand command, LibvirtComputingResource ser if (Objects.isNull(vmExists)) { PrimaryDataStoreTO volumePool = restoreVolumePools.get(0); String volumePath = restoreVolumePaths.get(0); - int lastIndex = volumePath.lastIndexOf("/"); - newVolumeId = volumePath.substring(lastIndex + 1); - restoreVolume(storagePoolMgr, backupPath, volumePool, volumePath, diskType, restoreVolumeUuid, + String backupFile = backupFiles.get(0); + newVolumeId = getVolumeUuidFromPath(volumePath, volumePool); + Long size = command.getRestoreVolumeSizes().get(0); + restoreVolume(storagePoolMgr, backupPath, volumePool, volumePath, diskType, backupFile, size, new Pair<>(vmName, command.getVmState()), mountDirectory, timeout); } else if (Boolean.TRUE.equals(vmExists)) { - restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory, timeout); + restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, backupFiles, mountDirectory, timeout); } else { - restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, vmName, backupPath, mountDirectory, timeout); + restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backupPath, backupFiles, mountDirectory, timeout); } } catch (CloudRuntimeException e) { String errorMessage = e.getMessage() != null ? e.getMessage() : ""; @@ -109,19 +134,22 @@ private void verifyBackupFile(String backupPath, String volUuid) { } } - private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, List restoreVolumePaths, List backedVolumesUUIDs, - String backupPath, String mountDirectory, int timeout) { + private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, + List restoreVolumePaths, List backedVolumesUUIDs, + String backupPath, List backupFiles, String mountDirectory, int timeout) { String diskType = "root"; try { for (int idx = 0; idx < restoreVolumePaths.size(); idx++) { PrimaryDataStoreTO restoreVolumePool = restoreVolumePools.get(idx); String restoreVolumePath = restoreVolumePaths.get(idx); + String backupFile = backupFiles.get(idx); String backupVolumeUuid = backedVolumesUUIDs.get(idx); - Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid); + String fullPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); diskType = "datadisk"; - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, bkpPathAndVolUuid.first(), timeout)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + + verifyBackupFile(fullPath, backupVolumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, fullPath, timeout)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", backupVolumeUuid)); } } } finally { @@ -130,17 +158,20 @@ private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, Li } } - private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, List volumePaths, String vmName, String backupPath, String mountDirectory, int timeout) { + private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, + List volumePaths, String backupPath, List backupFiles, String mountDirectory, int timeout) { String diskType = "root"; try { for (int i = 0; i < volumePaths.size(); i++) { PrimaryDataStoreTO volumePool = volumePools.get(i); String volumePath = volumePaths.get(i); - Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null); + String backupFile = backupFiles.get(i); + String bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); + String volumeUuid = getVolumeUuidFromPath(volumePath, volumePool); diskType = "datadisk"; - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + verifyBackupFile(bkpPath, volumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPath, timeout)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", volumeUuid)); } } } finally { @@ -149,14 +180,17 @@ private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, } } - private void restoreVolume(KVMStoragePoolManager storagePoolMgr, String backupPath, PrimaryDataStoreTO volumePool, String volumePath, String diskType, String volumeUUID, - Pair vmNameAndState, String mountDirectory, int timeout) { - Pair bkpPathAndVolUuid; + private void restoreVolume(KVMStoragePoolManager storagePoolMgr, String backupPath, PrimaryDataStoreTO volumePool, String volumePath, String diskType, String backupFile, + Long size, Pair vmNameAndState, String mountDirectory, int timeout) { + String bkpPath; + String volumeUuid; try { - bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout, true)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); + volumeUuid = getVolumeUuidFromPath(volumePath, volumePool); + verifyBackupFile(bkpPath, volumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPath, timeout, true, size)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", volumeUuid)); + } if (VirtualMachine.State.Running.equals(vmNameAndState.second())) { if (!attachVolumeToVm(storagePoolMgr, vmNameAndState.first(), volumePool, volumePath)) { @@ -177,7 +211,7 @@ private String mountBackupDirectory(String backupRepoAddress, String backupRepoT try { mountDirectory = Files.createTempDirectory(mountDirectory).toString(); } catch (IOException e) { - logger.error(String.format("Failed to create the tmp mount directory {} for restore", mountDirectory), e); + logger.error("Failed to create the tmp mount directory {} for restore", mountDirectory, e); throw new CloudRuntimeException("Failed to create the tmp mount directory for restore on the KVM host"); } @@ -195,7 +229,7 @@ private String mountBackupDirectory(String backupRepoAddress, String backupRepoT int exitValue = Script.runSimpleBashScriptForExitValue(mount, mountTimeout, false); if (exitValue != 0) { - logger.error(String.format("Failed to mount repository {} of type {} to the directory {}", backupRepoAddress, backupRepoType, mountDirectory)); + logger.error("Failed to mount repository {} of type {} to the directory {}", backupRepoAddress, backupRepoType, mountDirectory); throw new CloudRuntimeException("Failed to mount the backup repository on the KVM host"); } return mountDirectory; @@ -205,7 +239,7 @@ private void unmountBackupDirectory(String backupDirectory) { String umountCmd = String.format(UMOUNT_COMMAND, backupDirectory); int exitValue = Script.runSimpleBashScriptForExitValue(umountCmd); if (exitValue != 0) { - logger.error(String.format("Failed to unmount backup directory {}", backupDirectory)); + logger.error("Failed to unmount backup directory {}", backupDirectory); throw new CloudRuntimeException("Failed to unmount the backup directory"); } } @@ -214,17 +248,16 @@ private void deleteTemporaryDirectory(String backupDirectory) { try { Files.deleteIfExists(Paths.get(backupDirectory)); } catch (IOException e) { - logger.error(String.format("Failed to delete backup directory: %s", backupDirectory), e); + logger.error("Failed to delete backup directory: {}}", backupDirectory, e); throw new CloudRuntimeException("Failed to delete the backup directory"); } } - private Pair getBackupPath(String mountDirectory, String volumePath, String backupPath, String diskType, String volumeUuid) { + private String getBackupPath(String mountDirectory, String backupPath, String backupFile, String diskType) { String bkpPath = String.format(FILE_PATH_PLACEHOLDER, mountDirectory, backupPath); - String volUuid = Objects.isNull(volumeUuid) ? volumePath.substring(volumePath.lastIndexOf(File.separator) + 1) : volumeUuid; - String backupFileName = String.format("%s.%s.qcow2", diskType.toLowerCase(Locale.ROOT), volUuid); + String backupFileName = String.format("%s.%s.qcow2", diskType.toLowerCase(Locale.ROOT), backupFile); bkpPath = String.format(FILE_PATH_PLACEHOLDER, bkpPath, backupFileName); - return new Pair<>(bkpPath, volUuid); + return bkpPath; } private boolean checkBackupFileImage(String backupPath) { @@ -238,42 +271,83 @@ private boolean checkBackupPathExists(String backupPath) { } private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout) { - return replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, false); + return replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, false, null); } - private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume) { - if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { - int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath)); - return exitValue == 0; + private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume, Long size) { + if (List.of(Storage.StoragePoolType.RBD, Storage.StoragePoolType.Linstor).contains(volumePool.getPoolType())) { + return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size); } - return replaceRbdVolumeWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume); + // For NAS-backed incremental backups, the source qcow2 has a backing-file + // reference to its parent (set by nasbackup.sh's qemu-img rebase). A plain + // rsync would copy only the differential blocks, leaving a volume that + // depends on a backing file the primary storage doesn't have. Flatten the + // chain via qemu-img convert, which follows the backing-file links and + // produces a single self-contained qcow2. + if (hasBackingChain(backupPath)) { + int flattenExit = Script.runSimpleBashScriptForExitValue( + String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false); + return flattenExit == 0; + } + + int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false); + return exitValue == 0; + } + + private boolean hasBackingChain(String qcow2Path) { + return Script.runSimpleBashScriptForExitValue( + String.format(QEMU_IMG_HAS_BACKING_COMMAND, qcow2Path)) == 0; } - private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume) { + private boolean replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume, Long size) { KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid()); QemuImg qemu; try { - qemu = new QemuImg(timeout * 1000, true, false); - if (!createTargetVolume) { - KVMPhysicalDisk rdbDisk = volumeStoragePool.getPhysicalDisk(volumePath); - logger.debug("Restoring RBD volume: {}", rdbDisk.toString()); + qemu = new QemuImg(timeout, true, false); + String volumeUuid = getVolumeUuidFromPath(volumePath, volumePool); + KVMPhysicalDisk disk = null; + if (createTargetVolume) { + if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) { + if (size == null) { + throw new CloudRuntimeException("Restore volume size is required for Linstor pool when creating target volume"); + } + disk = volumeStoragePool.createPhysicalDisk(volumeUuid, QemuImg.PhysicalDiskFormat.RAW, Storage.ProvisioningType.THIN, size, null); + } + } else { + if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) { + storagePoolMgr.connectPhysicalDisk(volumePool.getPoolType(), volumePool.getUuid(), volumeUuid, null); + } else { + disk = volumeStoragePool.getPhysicalDisk(volumePath); + } qemu.setSkipTargetVolumeCreation(true); } + if (disk != null) { + logger.debug("Restoring volume: {}", disk.toString()); + } } catch (LibvirtException ex) { - throw new CloudRuntimeException("Failed to create qemu-img command to restore RBD volume with backup", ex); + throw new CloudRuntimeException(String.format("Failed to create qemu-img command to restore %s volume with backup", volumePool.getPoolType()), ex); } QemuImgFile srcBackupFile = null; QemuImgFile destVolumeFile = null; try { srcBackupFile = new QemuImgFile(backupPath, QemuImg.PhysicalDiskFormat.QCOW2); - String rbdDestVolumeFile = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); - destVolumeFile = new QemuImgFile(rbdDestVolumeFile, QemuImg.PhysicalDiskFormat.RAW); - - logger.debug("Starting convert backup {} to RBD volume {}", backupPath, volumePath); + String destVolume; + switch(volumePool.getPoolType()) { + case Linstor: + destVolume = volumePath; + break; + case RBD: + destVolume = KVMPhysicalDisk.RBDStringBuilder(volumeStoragePool, volumePath); + break; + default: + throw new CloudRuntimeException(String.format("Unsupported storage pool type [%s] for block device restore with backup.", volumePool.getPoolType())); + } + destVolumeFile = new QemuImgFile(destVolume, QemuImg.PhysicalDiskFormat.RAW); + logger.debug("Starting convert backup {} to volume {}", backupPath, volumePath); qemu.convert(srcBackupFile, destVolumeFile); - logger.debug("Successfully converted backup {} to RBD volume {}", backupPath, volumePath); + logger.debug("Successfully converted backup {} to volume {}", backupPath, volumePath); } catch (QemuImgException | LibvirtException e) { String srcFilename = srcBackupFile != null ? srcBackupFile.getFileName() : null; String destFilename = destVolumeFile != null ? destVolumeFile.getFileName() : null; @@ -287,12 +361,14 @@ private boolean replaceRbdVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, private boolean attachVolumeToVm(KVMStoragePoolManager storagePoolMgr, String vmName, PrimaryDataStoreTO volumePool, String volumePath) { String deviceToAttachDiskTo = getDeviceToAttachDisk(vmName); int exitValue; - if (volumePool.getPoolType() != Storage.StoragePoolType.RBD) { - exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_QCOW2_DISK_COMMAND, vmName, volumePath, deviceToAttachDiskTo)); - } else { + if (volumePool.getPoolType() == Storage.StoragePoolType.RBD) { String xmlForRbdDisk = getXmlForRbdDisk(storagePoolMgr, volumePool, volumePath, deviceToAttachDiskTo); logger.debug("RBD disk xml to attach: {}", xmlForRbdDisk); exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_RBD_DISK_XML_COMMAND, vmName, xmlForRbdDisk)); + } else if (volumePool.getPoolType() == Storage.StoragePoolType.Linstor) { + exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_RAW_DISK_COMMAND, vmName, volumePath, deviceToAttachDiskTo)); + } else { + exitValue = Script.runSimpleBashScriptForExitValue(String.format(ATTACH_QCOW2_DISK_COMMAND, vmName, volumePath, deviceToAttachDiskTo)); } return exitValue == 0; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java new file mode 100644 index 000000000000..a9010b46c1d4 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreKbossBackupCommandWrapper.java @@ -0,0 +1,123 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.Pair; +import org.apache.cloudstack.backup.RestoreKbossBackupAnswer; +import org.apache.cloudstack.backup.RestoreKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.libvirt.LibvirtException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +@ResourceWrapper(handles = RestoreKbossBackupCommand.class) +public class LibvirtRestoreKbossBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(RestoreKbossBackupCommand cmd, LibvirtComputingResource resource) { + Set> backupToAndVolumeObjectPairs = cmd.getBackupAndVolumePairs(); + Set deltasToRemove = cmd.getDeltasToRemove(); + Set secondaryStorageUrls = cmd.getSecondaryStorageUrls(); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + + Set secondaryStorageUuids = new HashSet<>(); + try { + KVMStoragePool secondaryStorage = mountSecondaryStorages(secondaryStorageUrls, backupToAndVolumeObjectPairs.stream().findFirst().get().first().getDataStore().getUrl(), + storagePoolManager, secondaryStorageUuids); + + restoreVolumes(backupToAndVolumeObjectPairs, secondaryStorage, storagePoolManager, cmd.isQuickRestore(), cmd.getWait() * 1000); + + deleteDeltas(deltasToRemove, storagePoolManager); + } catch (LibvirtException | QemuImgException | IOException e) { + return new RestoreKbossBackupAnswer(cmd, e, secondaryStorageUuids); + } finally { + if (!cmd.isQuickRestore()) { + for (String uuid : secondaryStorageUuids) { + storagePoolManager.deleteStoragePool(Storage.StoragePoolType.NetworkFilesystem, uuid); + } + } + } + return new RestoreKbossBackupAnswer(cmd, secondaryStorageUuids); + } + + protected void restoreVolumes(Set> backupToAndVolumeObjectPairs, KVMStoragePool secondaryStorage, KVMStoragePoolManager storagePoolManager, + boolean quickRestore, int timeoutInMillis) throws LibvirtException, QemuImgException { + for (Pair backupToVolumeToPair : backupToAndVolumeObjectPairs) { + String fullBackupPath = secondaryStorage.getLocalPathFor(backupToVolumeToPair.first().getPath()); + + VolumeObjectTO volumeObjectTO = backupToVolumeToPair.second(); + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullVolumePath = primaryStoragePool.getLocalPathFor(volumeObjectTO.getPath()); + + QemuImgFile backup = new QemuImgFile(fullBackupPath, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile volume = new QemuImgFile(fullVolumePath, QemuImg.PhysicalDiskFormat.QCOW2); + + QemuImg qemuImg = getQemuImg(timeoutInMillis); + + if (quickRestore) { + logger.info("Creating delta over old volume [{}] at [{}] with backing store stored at [{}].", volumeObjectTO.getUuid(), fullVolumePath, fullBackupPath); + qemuImg.create(volume, backup); + } else { + logger.info("Restoring volume [{}] at [{}] with backup stored at [{}].", volumeObjectTO.getUuid(), fullVolumePath, fullBackupPath); + qemuImg.convert(backup, volume); + } + } + } + + protected QemuImg getQemuImg(int timeoutInMillis) throws LibvirtException, QemuImgException { + return new QemuImg(timeoutInMillis); + } + + protected void deleteDeltas(Set deltasToRemove, KVMStoragePoolManager storagePoolManager) throws IOException { + for (BackupDeltaTO deltaToRemove : deltasToRemove) { + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) deltaToRemove.getDataStore(); + KVMStoragePool primaryStoragePool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + String fullDeltaPath = primaryStoragePool.getLocalPathFor(deltaToRemove.getPath()); + logger.debug("Deleting leftover delta [{}].", fullDeltaPath); + Files.deleteIfExists(Path.of(fullDeltaPath)); + } + } + + protected KVMStoragePool mountSecondaryStorages(Set parentSecondaryStorageUrls, String secondaryStorageUrl, KVMStoragePoolManager storagePoolManager, Set secondaryStorageUuids) { + for (String url : parentSecondaryStorageUrls) { + KVMStoragePool pool = storagePoolManager.getStoragePoolByURI(url); + secondaryStorageUuids.add(pool.getUuid()); + } + KVMStoragePool pool = storagePoolManager.getStoragePoolByURI(secondaryStorageUrl); + secondaryStorageUuids.add(pool.getUuid()); + return pool; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java index 5d76d140f229..507744fdc316 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java @@ -28,6 +28,7 @@ import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; +import com.cloud.storage.Storage; import org.apache.cloudstack.storage.command.RevertSnapshotCommand; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; @@ -58,6 +59,8 @@ import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.libvirt.LibvirtException; +import static com.cloud.hypervisor.kvm.storage.KVMStorageProcessor.poolTypesToDeleteChainInfo; + @ResourceWrapper(handles = RevertSnapshotCommand.class) public class LibvirtRevertSnapshotCommandWrapper extends CommandWrapper { @@ -117,7 +120,7 @@ public Answer execute(final RevertSnapshotCommand command, final LibvirtComputin secondaryStoragePool = storagePoolMgr.getStoragePoolByURI(snapshotImageStore.getUrl()); } - if (primaryPool.getType() == StoragePoolType.CLVM) { + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { Script cmd = new Script(libvirtComputingResource.manageSnapshotPath(), libvirtComputingResource.getCmdsTimeout(), logger); cmd.add("-v", getFullPathAccordingToStorage(secondaryStoragePool, snapshotRelPath)); cmd.add("-n", snapshotDisk.getName()); @@ -128,7 +131,7 @@ public Answer execute(final RevertSnapshotCommand command, final LibvirtComputin return new Answer(command, false, result); } } else { - revertVolumeToSnapshot(secondaryStoragePool, snapshotOnPrimaryStorage, snapshot, primaryPool, libvirtComputingResource); + revertVolumeToSnapshot(secondaryStoragePool, snapshotOnPrimaryStorage, snapshot, primaryPool, libvirtComputingResource, command.isDeleteChain()); } } @@ -161,7 +164,7 @@ protected String getFullPathAccordingToStorage(KVMStoragePool kvmStoragePool, St * Reverts the volume to the snapshot. */ protected void revertVolumeToSnapshot(KVMStoragePool kvmStoragePoolSecondary, SnapshotObjectTO snapshotOnPrimaryStorage, SnapshotObjectTO snapshotOnSecondaryStorage, - KVMStoragePool kvmStoragePoolPrimary, LibvirtComputingResource resource) { + KVMStoragePool kvmStoragePoolPrimary, LibvirtComputingResource resource, boolean deleteChain) { VolumeObjectTO volumeObjectTo = snapshotOnSecondaryStorage.getVolume(); String volumePath = getFullPathAccordingToStorage(kvmStoragePoolPrimary, volumeObjectTo.getPath()); @@ -178,6 +181,11 @@ protected void revertVolumeToSnapshot(KVMStoragePool kvmStoragePoolSecondary, Sn try { replaceVolumeWithSnapshot(volumePath, snapshotPath); + if (volumeObjectTo.getChainInfo() != null && poolTypesToDeleteChainInfo.contains(kvmStoragePoolPrimary.getType()) && + volumeObjectTo.getFormat() == Storage.ImageFormat.QCOW2 && deleteChain) { + logger.debug("Deleting leftover backup delta at [{}].", volumeObjectTo.getChainInfo()); + kvmStoragePoolPrimary.deletePhysicalDisk(volumeObjectTo.getChainInfo(), volumeObjectTo.getFormat()); + } logger.debug(String.format("Successfully reverted volume [%s] to snapshot [%s].", volumeObjectTo, snapshotToPrint)); } catch (LibvirtException | QemuImgException ex) { throw new CloudRuntimeException(String.format("Unable to revert volume [%s] to snapshot [%s] due to [%s].", volumeObjectTo, snapshotToPrint, ex.getMessage()), ex); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java index 1536984f2e85..b1f64c8c6dbb 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtScaleVmCommandWrapper.java @@ -49,10 +49,11 @@ public Answer execute(ScaleVmCommand command, LibvirtComputingResource libvirtCo conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); Domain dm = conn.domainLookupByName(vmName); - logger.debug(String.format("Scaling %s.", scalingDetails)); + logger.debug("Scaling {}.", scalingDetails); scaleMemory(dm, newMemory, vmDefinition); scaleVcpus(dm, newVcpus, vmDefinition); updateCpuShares(dm, newCpuShares); + libvirtComputingResource.updateCpuQuotaAndPeriod(dm, vmSpec, command.getLimitCpuUseChange()); return new ScaleVmAnswer(command, true, String.format("Successfully scaled %s.", scalingDetails)); } catch (LibvirtException | CloudRuntimeException e) { @@ -74,7 +75,7 @@ protected void updateCpuShares(Domain dm, int newCpuShares) throws LibvirtExcept if (oldCpuShares < newCpuShares) { LibvirtComputingResource.setCpuShares(dm, newCpuShares); - logger.info(String.format("Successfully increased cpu_shares of VM [%s] from [%s] to [%s].", dm.getName(), oldCpuShares, newCpuShares)); + logger.info("Successfully increased cpu_shares of VM [{}] from [{}] to [{}].", dm.getName(), oldCpuShares, newCpuShares); } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java new file mode 100644 index 000000000000..629fd179afb2 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java @@ -0,0 +1,300 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.backup.StartBackupAnswer; +import org.apache.cloudstack.backup.StartBackupCommand; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.cloudstack.utils.qemu.QemuCommand; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.json.JSONArray; +import org.json.JSONObject; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StartBackupCommand.class) +public class LibvirtStartBackupCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(StartBackupCommand cmd, LibvirtComputingResource resource) { + if (cmd.isStoppedVM()) { + return handleStoppedVmBackup(cmd, cmd.getToCheckpointId()); + } + return handleRunningVmBackup(cmd, resource); + } + + public Answer handleRunningVmBackup(StartBackupCommand cmd, LibvirtComputingResource resource) { + String vmName = cmd.getVmName(); + String toCheckpointId = cmd.getToCheckpointId(); + String fromCheckpointId = cmd.getFromCheckpointId(); + Long fromCheckpointCreateTime = cmd.getFromCheckpointCreateTime(); + String socket = cmd.getSocket(); + + try { + if (StringUtils.isNotBlank(fromCheckpointId)) { + Answer redefineAnswer = ensureFromCheckpointExists(cmd, fromCheckpointId, fromCheckpointCreateTime); + if (redefineAnswer != null) { + return redefineAnswer; + } + } + + File dir = new File("/tmp/imagetransfer"); + if (!dir.exists()) { + dir.mkdirs(); + } + + // Create backup XML + String backupXml = createBackupXml(cmd, fromCheckpointId, socket, resource); + String checkpointXml = createCheckpointXml(toCheckpointId); + + // Write XMLs to temp files + File backupXmlFile = File.createTempFile("backup-", ".xml"); + File checkpointXmlFile = File.createTempFile("checkpoint-", ".xml"); + + try (FileWriter writer = new FileWriter(backupXmlFile)) { + writer.write(backupXml); + } + try (FileWriter writer = new FileWriter(checkpointXmlFile)) { + writer.write(checkpointXml); + } + + // Execute virsh backup-begin + String backupCmd = String.format("virsh backup-begin %s %s --checkpointxml %s", + vmName, backupXmlFile.getAbsolutePath(), checkpointXmlFile.getAbsolutePath()); + + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(backupCmd); + String result = script.execute(); + + backupXmlFile.delete(); + checkpointXmlFile.delete(); + + if (result != null) { + return new StartBackupAnswer(cmd, false, "Backup begin failed: " + result); + } + + long checkpointCreateTime = getCheckpointCreateTime(); + return new StartBackupAnswer(cmd, true, "Backup started successfully", checkpointCreateTime); + + } catch (Exception e) { + return new StartBackupAnswer(cmd, false, "Error starting backup: " + e.getMessage()); + } + } + + private Answer ensureFromCheckpointExists(StartBackupCommand cmd, String fromCheckpointId, Long fromCheckpointCreateTime) { + String vmName = cmd.getVmName(); + Script dumpScript = new Script("/bin/bash"); + dumpScript.add("-c"); + dumpScript.add(String.format("virsh checkpoint-dumpxml --domain %s --checkpointname %s --no-domain", + vmName, fromCheckpointId)); + if (dumpScript.execute() == null) { + return null; + } + if (fromCheckpointCreateTime == null) { + return new StartBackupAnswer(cmd, false, "From checkpoint create time is null for checkpoint " + fromCheckpointId); + } + + String redefineXml = createCheckpointXmlForRedefine(fromCheckpointId, fromCheckpointCreateTime); + File redefineFile; + try { + redefineFile = File.createTempFile("checkpoint-redefine-", ".xml"); + } catch (Exception e) { + return new StartBackupAnswer(cmd, false, "Failed to create temp file for checkpoint redefine: " + e.getMessage()); + } + try (FileWriter writer = new FileWriter(redefineFile)) { + writer.write(redefineXml); + } catch (Exception e) { + redefineFile.delete(); + return new StartBackupAnswer(cmd, false, "Failed to write checkpoint redefine XML: " + e.getMessage()); + } + String createCmd = String.format(LibvirtComputingResource.CHECKPOINT_CREATE_COMMAND, vmName, redefineFile.getAbsolutePath()); + Script createScript = new Script("/bin/bash"); + createScript.add("-c"); + createScript.add(createCmd); + String result = createScript.execute(); + redefineFile.delete(); + if (result != null) { + return new StartBackupAnswer(cmd, false, "Failed to redefine from-checkpoint " + fromCheckpointId + ": " + result); + } + return null; + } + + private String createCheckpointXmlForRedefine(String checkpointName, Long createTime) { + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + xml.append(" ").append(checkpointName).append("\n"); + xml.append(" ").append(createTime).append("\n"); + xml.append(""); + return xml.toString(); + } + + private String createBackupXml(StartBackupCommand cmd, String fromCheckpointId, String socket, LibvirtComputingResource resource) throws LibvirtException { + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + + xml.append(String.format(" \n", socket)); + + xml.append(" \n"); + + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + Map diskPathLabelMap = resource.getDiskPathLabelMap(cmd.getVmName()); + Map diskPathHasFromCheckpointMap = new HashMap<>(); + if (StringUtils.isNotBlank(fromCheckpointId)) { + Domain vm = null; + try { + vm = resource.getDomain(resource.getLibvirtUtilitiesHelper().getConnection(), cmd.getVmName()); + if (vm != null) { + diskPathHasFromCheckpointMap = getVmDiskPathHasFromCheckpointMap(vm, fromCheckpointId); + } else { + logger.warn("Failed to get domain for VM [{}] while evaluating export bitmap [{}]. Falling back to full Backup", + cmd.getVmName(), fromCheckpointId); + } + } finally { + if (vm != null) { + vm.free(); + } + } + } + + for (Map.Entry entry : diskPathLabelMap.entrySet()) { + String diskPath = entry.getKey(); + if (!diskPathUuidMap.containsKey(diskPath)) { + continue; + } + String diskName = entry.getValue(); + String export = diskPathUuidMap.get(diskPath); + String scratchFile = "/var/tmp/scratch-" + export + ".qcow2"; + xml.append(" \n"); + xml.append(" \n"); + xml.append(" \n"); + } + + xml.append(" \n"); + xml.append(""); + + return xml.toString(); + } + + private String createCheckpointXml(String checkpointId) { + return "\n" + + " " + checkpointId + "\n" + + ""; + } + + private Answer handleStoppedVmBackup(StartBackupCommand cmd, String toCheckpointId) { + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + Map diskPathPassphraseMap = cmd.getDiskPathPassphraseMap(); + for (Map.Entry entry : diskPathUuidMap.entrySet()) { + String diskPath = entry.getKey(); + Script script = new Script("qemu-img"); + byte[] passphrase = diskPathPassphraseMap.get(diskPath); + + script.add("bitmap"); + script.add("--add"); + + if (passphrase != null && passphrase.length > 0) { + KeyFile srcKey; + try { + srcKey = new KeyFile(passphrase); + } catch (IOException ex) { + return new StartBackupAnswer(cmd, false, "Failed to create KeyFile while adding bitmap " + toCheckpointId + " to disk " + diskPath); + } + script.add("--object"); + script.add(String.format("secret,id=sec0,file=%s", srcKey)); + script.add("--image-opts"); + script.add(String.format("driver=qcow2,file.driver=file,file.filename=%s,encrypt.key-secret=sec0", diskPath)); + } else { + script.add(diskPath); + } + + script.add(toCheckpointId); + String result = script.execute(); + if (result != null) { + return new StartBackupAnswer(cmd, false, + "Failed to add bitmap " + toCheckpointId + " to disk " + diskPath + ": " + result); + } + } + long checkpointCreateTime = getCheckpointCreateTime(); + return new StartBackupAnswer(cmd, true, "Stopped VM backup: checkpoint bitmap added successfully", + checkpointCreateTime); + } + + private long getCheckpointCreateTime() { + return System.currentTimeMillis() / 1000; + } + + private Map getVmDiskPathHasFromCheckpointMap(Domain vm, String fromCheckpointId) throws LibvirtException { + Map diskPathHasFromCheckpointMap = new HashMap<>(); + String queryBlock = vm.qemuMonitorCommand(QemuCommand.buildQemuCommand("query-block", null), 0); + JSONObject response = new JSONObject(queryBlock); + JSONArray blocks = response.optJSONArray("return"); + if (blocks == null) { + logger.warn("Couldn't get bitmap information for the VM [{}]. Falling back to full Backup", vm.getName()); + return diskPathHasFromCheckpointMap; + } + for (int i = 0; i < blocks.length(); i++) { + JSONObject block = blocks.getJSONObject(i); + JSONObject inserted = block.optJSONObject("inserted"); + if (inserted == null) { + continue; + } + String file = inserted.optString("file"); + if (StringUtils.isBlank(file)) { + continue; + } + JSONArray dirtyBitmaps = inserted.optJSONArray("dirty-bitmaps"); + boolean hasFromCheckpointBitmap = false; + if (dirtyBitmaps != null) { + for (int j = 0; j < dirtyBitmaps.length(); j++) { + JSONObject dirtyBitmap = dirtyBitmaps.optJSONObject(j); + if (dirtyBitmap == null) { + continue; + } + String bitmapName = dirtyBitmap.optString("name"); + if (fromCheckpointId.equals(bitmapName)) { + hasFromCheckpointBitmap = true; + break; + } + } + } + diskPathHasFromCheckpointMap.put(file, hasFromCheckpointBitmap); + } + return diskPathHasFromCheckpointMap; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 567986465906..486989661909 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -21,13 +21,16 @@ import java.io.File; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import com.cloud.agent.resource.virtualnetwork.VRScripts; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.utils.FileUtil; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections4.CollectionUtils; import org.libvirt.Connect; import org.libvirt.DomainInfo.DomainState; import org.libvirt.LibvirtException; @@ -64,7 +67,9 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); Connect conn = null; + List secondaryStorages = new ArrayList<>(); try { + mountSecondaryStoragesIfNeeded(command, libvirtComputingResource, secondaryStorages); vm = libvirtComputingResource.createVMFromSpec(vmSpec); conn = libvirtUtilitiesHelper.getConnectionByType(vm.getHvsType()); @@ -167,6 +172,17 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource } finally { if (state != DomainState.VIR_DOMAIN_RUNNING) { storagePoolMgr.disconnectPhysicalDisksViaVmSpec(vmSpec); + for (KVMStoragePool secondaryStorage : secondaryStorages) { + libvirtComputingResource.getStoragePoolMgr().deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + } + } + + private void mountSecondaryStoragesIfNeeded(StartCommand command, LibvirtComputingResource libvirtComputingResource, List secondaryStorages) { + if (CollectionUtils.isNotEmpty(command.getSecondaryStorages())) { + for (String secondaryStorageUrl : command.getSecondaryStorages()) { + secondaryStorages.add(libvirtComputingResource.getStoragePoolMgr().getStoragePoolByURI(secondaryStorageUrl)); } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java new file mode 100644 index 000000000000..661344677640 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java @@ -0,0 +1,197 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.io.IOException; + +import org.apache.cloudstack.backup.StartNBDServerAnswer; +import org.apache.cloudstack.backup.StartNBDServerCommand; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.json.JSONArray; +import org.json.JSONObject; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StartNBDServerCommand.class) +public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private String createSystemdRunCmd(StartNBDServerCommand cmd, String unitName, String volumePath, String exportName, String socket) throws IOException { + String socketName = "/tmp/imagetransfer/" + socket + ".sock"; + + String bitmapArg = ""; + if (StringUtils.isNotBlank(cmd.getFromCheckpointId()) + && isBitmapPresentOnDisk(volumePath, cmd.getFromCheckpointId())) { + bitmapArg = "-B " + cmd.getFromCheckpointId(); + } + + byte[] passphrase = cmd.getPassphrase(); // may be null + String readOnlyArg = cmd.getDirection().equals("download") ? "--read-only" : ""; + String imageArg = volumePath; + String secretArg = ""; + if (passphrase != null && passphrase.length > 0) { + KeyFile srcKey = new KeyFile(passphrase); + secretArg = String.format("--object secret,id=sec0,file=%s", srcKey); + imageArg = String.format("--image-opts driver=qcow2,file.driver=file,file.filename=%s,encrypt.key-secret=sec0", volumePath); + } + + // --persistent: Don't stop the service when the last client disconnects. + // --shared=NUM: Allow up to NUM clients to share the device (default 1), 0 for unlimited. Number of parallel connections is managed by the image server. + String systemdRunCmd = String.format( + "systemd-run --unit=%s --property=Restart=no qemu-nbd %s " + + "--export-name %s --socket %s --persistent --shared=0 %s %s %s", + unitName, + secretArg, + exportName, + socketName, + bitmapArg, + readOnlyArg, + imageArg + ); + return systemdRunCmd; + } + + @Override + public Answer execute(StartNBDServerCommand cmd, LibvirtComputingResource resource) { + String volumePath = cmd.getVolumePath(); + String socket = cmd.getSocket(); + String exportName = cmd.getExportName(); + String transferId = cmd.getTransferId(); + + if (StringUtils.isBlank(volumePath)) { + return new StartNBDServerAnswer(cmd, false, "Volume path is required for the nbd server"); + } + if (StringUtils.isBlank(exportName)) { + return new StartNBDServerAnswer(cmd, false, "Export name is required for the nbd server"); + } + if (StringUtils.isBlank(socket)) { + return new StartNBDServerAnswer(cmd, false, "Socket is required for the nbd server"); + } + + String unitName = "qemu-nbd-" + transferId.hashCode(); + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult == null) { + return new StartNBDServerAnswer(cmd, false, "A qemu-nbd service is already running on the port."); + } + + File dir = new File("/tmp/imagetransfer"); + if (!dir.exists()) { + dir.mkdirs(); + } + + String systemdRunCmd = ""; + try { + systemdRunCmd = createSystemdRunCmd(cmd, unitName, volumePath, exportName, socket); + } catch (IOException e) { + logger.error("Failed to create the KeyFile for qemu-nbd service", e); + return new StartNBDServerAnswer(cmd, false, "Failed to create systemd run command for qemu-nbd service: " + e.getMessage()); + } + + Script startScript = new Script("/bin/bash", logger); + startScript.add("-c"); + startScript.add(systemdRunCmd); + String startResult = startScript.execute(); + + if (startResult != null) { + logger.error(String.format("Failed to start qemu-nbd service: %s", startResult)); + return new StartNBDServerAnswer(cmd, false, "Failed to start qemu-nbd service: " + startResult); + } + + // Wait with timeout until the service is up + int maxWaitSeconds = 20; + int pollIntervalMs = 5000; + int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs; + boolean serviceActive = false; + + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + Thread.sleep(pollIntervalMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new StartNBDServerAnswer(cmd, false, "Interrupted while waiting for qemu-nbd service to start"); + } + Script verifyScript = new Script("/bin/bash", logger); + verifyScript.add("-c"); + verifyScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String verifyResult = verifyScript.execute(); + if (verifyResult == null) { + serviceActive = true; + logger.info(String.format("qemu-nbd service %s is now active (attempt %d)", unitName, attempt + 1)); + break; + } + } + + if (!serviceActive) { + logger.error(String.format("qemu-nbd service %s failed to become active within %d seconds", unitName, maxWaitSeconds)); + return new StartNBDServerAnswer(cmd, false, + String.format("qemu-nbd service failed to start within %d seconds", maxWaitSeconds)); + } + + String transferUrl = String.format("nbd+unix:///%s", cmd.getSocket()); + return new StartNBDServerAnswer(cmd, true, "qemu-nbd service started for upload", + transferId, transferUrl); + } + + private boolean isBitmapPresentOnDisk(String volumePath, String fromCheckpointId) { + String qemuImgInfo = Script.runBashScriptIgnoreExitValue( + String.format("qemu-img info --output=json %s", volumePath), 0); + if (StringUtils.isBlank(qemuImgInfo)) { + logger.warn("Unable to read qemu-img info output for disk path [{}].", volumePath); + return false; + } + try { + JSONObject info = new JSONObject(qemuImgInfo); + JSONObject formatSpecific = info.optJSONObject("format-specific"); + if (formatSpecific == null) { + return false; + } + JSONObject formatData = formatSpecific.optJSONObject("data"); + if (formatData == null) { + return false; + } + JSONArray bitmaps = formatData.optJSONArray("bitmaps"); + if (bitmaps == null) { + return false; + } + for (int i = 0; i < bitmaps.length(); i++) { + JSONObject bitmap = bitmaps.optJSONObject(i); + if (bitmap == null) { + continue; + } + if (fromCheckpointId.equals(bitmap.optString("name"))) { + return true; + } + } + } catch (Exception e) { + logger.warn("Failed to parse qemu-img info output for disk path [{}].", volumePath, e); + } + return false; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java new file mode 100644 index 000000000000..1185d89bc0b3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.StopBackupAnswer; +import org.apache.cloudstack.backup.StopBackupCommand; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.Domain; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StopBackupCommand.class) +public class LibvirtStopBackupCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(StopBackupCommand cmd, LibvirtComputingResource resource) { + String vmName = cmd.getVmName(); + + try { + Connect conn = LibvirtConnection.getConnection(); + Domain dm = conn.domainLookupByName(vmName); + + if (dm == null) { + return new StopBackupAnswer(cmd, false, "Domain not found: " + vmName); + } + + // Execute virsh domjobabort + String abortCmd = String.format("virsh domjobabort %s", vmName); + + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(abortCmd); + String result = script.execute(); + + if (result != null && !result.isEmpty()) { + // Job abort may fail if no job is running, which is acceptable + logger.debug("domjobabort result: " + result); + } + + return new StopBackupAnswer(cmd, true, "Backup stopped successfully"); + + } catch (Exception e) { + return new StopBackupAnswer(cmd, false, "Error stopping backup: " + e.getMessage()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java new file mode 100644 index 000000000000..57c7ebb706bc --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java @@ -0,0 +1,72 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.StopNBDServerCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StopNBDServerCommand.class) +public class LibvirtStopNBDServerCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + @Override + public Answer execute(StopNBDServerCommand cmd, LibvirtComputingResource resource) { + try { + String unitName = "qemu-nbd-" + cmd.getTransferId().hashCode(); + + // Check if the service is running + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult != null) { + // Service is not running, but still reset-failed to clear any stale state + logger.info(String.format("qemu-nbd service %s is not running, resetting failed state", unitName)); + resetService(unitName); + return new Answer(cmd, true, "Image transfer finalized"); + } + + // Stop the systemd service + Script stopScript = new Script("/bin/bash", logger); + stopScript.add("-c"); + stopScript.add(String.format("systemctl stop %s", unitName)); + stopScript.execute(); + resetService(unitName); + + return new Answer(cmd, true, "Image transfer finalized"); + + } catch (Exception e) { + logger.error("Error finalizing image transfer for upload", e); + return new Answer(cmd, false, "Error finalizing image transfer: " + e.getMessage()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java index 11fa605908a6..106fe31a0f18 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupCommandWrapper.java @@ -42,6 +42,16 @@ @ResourceWrapper(handles = TakeBackupCommand.class) public class LibvirtTakeBackupCommandWrapper extends CommandWrapper { private static final Integer EXIT_CLEANUP_FAILED = 20; + // nasbackup.sh prints this on stdout when it could not proceed as an incremental and + // completed a full backup instead; the orchestrator then records the backup as a full. + private static final String INCREMENTAL_FALLBACK_MARKER = "INCREMENTAL_FALLBACK=true"; + + private static final String MODE_FULL = "full"; + private static final String MODE_INCREMENTAL = "incremental"; + // Incremental feature disabled: plain full backup with no QEMU bitmap/checkpoint and no + // chain metadata. Matches nasbackup.sh's "legacy-full" mode (make_checkpoint=0). + private static final String MODE_LEGACY_FULL = "legacy-full"; + @Override public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvirtComputingResource) { final String vmName = command.getVmName(); @@ -52,6 +62,14 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir List volumePools = command.getVolumePools(); final List volumePaths = command.getVolumePaths(); KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + int timeout = command.getWait() > 0 ? command.getWait() * 1000 : libvirtComputingResource.getCmdsTimeout(); + + // Pre-validate incremental args here rather than relying on the script to error out. + // Keeps the script agnostic to caller policy (it just does what it's told). + String validationError = validateBackupArgs(command); + if (validationError != null) { + return new BackupAnswer(command, false, validationError); + } List diskPaths = new ArrayList<>(); if (Objects.nonNull(volumePaths)) { @@ -68,8 +86,63 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir } } - List commands = new ArrayList<>(); - commands.add(new String[]{ + Pair result = runBackupScript(libvirtComputingResource, command, vmName, backupRepoType, backupRepoAddress, + mountOptions, backupPath, diskPaths, command.getMode(), + command.getBitmapNew(), command.getBitmapParent(), command.getParentPaths(), timeout); + + if (result.first() != 0) { + logger.debug("Failed to take VM backup: " + result.second()); + BackupAnswer answer = new BackupAnswer(command, false, result.second().trim()); + if (EXIT_CLEANUP_FAILED.equals(result.first())) { + logger.debug("Backup cleanup failed"); + answer.setNeedsCleanup(true); + } + return answer; + } + + // The script self-heals to a full backup when an incremental can't proceed (e.g. the + // parent checkpoint can't be re-registered) and signals it with INCREMENTAL_FALLBACK + // on stdout. Detect it, then strip the marker line before parsing the backup size. + String rawStdout = result.second(); + boolean incrementalFallback = rawStdout.contains(INCREMENTAL_FALLBACK_MARKER); + String stdout = stripMarkerLines(rawStdout).trim(); + long backupSize = parseBackupSize(stdout, diskPaths); + + BackupAnswer answer = new BackupAnswer(command, true, stdout); + answer.setSize(backupSize); + // A successful run always created command.getBitmapNew() (full and incremental both do; + // it is null for legacy-full, which the orchestrator treats as "no bitmap"). + answer.setBitmapCreated(command.getBitmapNew()); + answer.setIncrementalFallback(incrementalFallback); + return answer; + } + + /** Remove nasbackup.sh's stdout signalling marker lines so they don't pollute size parsing. */ + private String stripMarkerLines(String stdout) { + if (stdout == null || stdout.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (String line : stdout.split("\n", -1)) { + if (line.contains(INCREMENTAL_FALLBACK_MARKER)) { + continue; + } + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(line); + } + return sb.toString(); + } + + /** + * Run nasbackup.sh once with the given args. Returns the exit code + captured stdout. + */ + private Pair runBackupScript(LibvirtComputingResource libvirtComputingResource, + TakeBackupCommand command, String vmName, String backupRepoType, String backupRepoAddress, + String mountOptions, String backupPath, List diskPaths, String mode, + String bitmapNew, String bitmapParent, List parentPaths, int timeout) { + List argv = new ArrayList<>(Arrays.asList( libvirtComputingResource.getNasBackupPath(), "-o", "backup", "-v", vmName, @@ -79,35 +152,79 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir "-p", backupPath, "-q", command.getQuiesce() != null && command.getQuiesce() ? "true" : "false", "-d", diskPaths.isEmpty() ? "" : String.join(",", diskPaths) - }); + )); + if (mode != null && !mode.isEmpty()) { + argv.add("-M"); + argv.add(mode); + } + if (bitmapNew != null && !bitmapNew.isEmpty()) { + argv.add("--bitmap-new"); + argv.add(bitmapNew); + } + if (bitmapParent != null && !bitmapParent.isEmpty()) { + argv.add("--bitmap-parent"); + argv.add(bitmapParent); + } + if (parentPaths != null && !parentPaths.isEmpty()) { + argv.add("--parent-paths"); + argv.add(String.join(",", parentPaths)); + } - Pair result = Script.executePipedCommands(commands, libvirtComputingResource.getCmdsTimeout()); + List commands = new ArrayList<>(); + commands.add(argv.toArray(new String[0])); + return Script.executePipedCommands(commands, timeout); + } - if (result.first() != 0) { - logger.debug("Failed to take VM backup: " + result.second()); - BackupAnswer answer = new BackupAnswer(command, false, result.second().trim()); - if (result.first() == EXIT_CLEANUP_FAILED) { - logger.debug("Backup cleanup failed"); - answer.setNeedsCleanup(true); + /** + * Return a human-readable validation error string, or {@code null} if the command's + * incremental-backup args are internally consistent. + */ + private String validateBackupArgs(TakeBackupCommand command) { + String mode = command.getMode(); + if (mode == null || mode.isEmpty()) { + return null; // legacy full-only — no extra args expected + } + if (MODE_INCREMENTAL.equals(mode)) { + if (command.getBitmapNew() == null || command.getBitmapNew().isEmpty()) { + return "incremental mode requires bitmapNew"; } - return answer; + if (command.getBitmapParent() == null || command.getBitmapParent().isEmpty()) { + return "incremental mode requires bitmapParent"; + } + if (command.getParentPaths() == null || command.getParentPaths().isEmpty()) { + return "incremental mode requires parentPaths"; + } + return null; + } + if (MODE_FULL.equals(mode)) { + if (command.getBitmapNew() == null || command.getBitmapNew().isEmpty()) { + return "full mode requires bitmapNew (the bitmap to create for the next incremental)"; + } + return null; + } + if (MODE_LEGACY_FULL.equals(mode)) { + return null; // feature-off full backup — no bitmap or chain args expected } + return "Unknown backup mode: " + mode; + } + /** + * Sum the per-disk size lines emitted by nasbackup.sh. Single-volume mode emits one + * line containing just the byte count; multi-volume mode emits one line per disk + * whose first whitespace-separated token is the byte count. + */ + private long parseBackupSize(String stdout, List diskPaths) { long backupSize = 0L; if (CollectionUtils.isNullOrEmpty(diskPaths)) { - List outputLines = Arrays.asList(result.second().trim().split("\n")); + List outputLines = Arrays.asList(stdout.split("\n")); if (!outputLines.isEmpty()) { backupSize = Long.parseLong(outputLines.get(outputLines.size() - 1).trim()); } } else { - String[] outputLines = result.second().trim().split("\n"); - for(String line : outputLines) { + for (String line : stdout.split("\n")) { backupSize = backupSize + Long.parseLong(line.split(" ")[0].trim()); } } - - BackupAnswer answer = new BackupAnswer(command, true, result.second().trim()); - answer.setSize(backupSize); - return answer; + return backupSize; } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java new file mode 100644 index 000000000000..64669b3b4b5c --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeBackupHashCommandWrapper.java @@ -0,0 +1,72 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.dynatrace.hash4j.hashing.HashStream128; +import com.dynatrace.hash4j.hashing.HashValue128; +import com.dynatrace.hash4j.hashing.Hashing; +import org.apache.cloudstack.backup.TakeBackupHashCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +@ResourceWrapper(handles = TakeBackupHashCommand.class) +public class LibvirtTakeBackupHashCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(TakeBackupHashCommand command, LibvirtComputingResource resource) { + String backupUuid = command.getBackupUuid(); + logger.info("Taking hash of backup [{}].", backupUuid); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + KVMStoragePool imagePool = null; + try { + imagePool = storagePoolManager.getStoragePoolByURI(command.getBackupDeltaTOList().get(0).getDataStore().getUrl()); + HashStream128 hashStream128 = Hashing.xxh3_128().hashStream(); + for (BackupDeltaTO backupDelta : command.getBackupDeltaTOList()) { + try (InputStream is = new BufferedInputStream(new FileInputStream(imagePool.getLocalPathFor(backupDelta.getPath())))) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + hashStream128.putBytes(buffer, 0, bytesRead); + } + } catch (IOException e) { + throw new CloudRuntimeException(e); + } + } + HashValue128 hash = hashStream128.get(); + String hashString = hash.toString(); + logger.info("The xxHash128 of backup [{}] is [{}].", backupUuid, hashString); + return new Answer(command, true, hashString); + } finally { + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java new file mode 100644 index 000000000000..d2332f4f99b1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtTakeKbossBackupCommandWrapper.java @@ -0,0 +1,393 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; +import org.apache.cloudstack.backup.TakeKbossBackupAnswer; +import org.apache.cloudstack.backup.TakeKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImageOptions; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.collections4.CollectionUtils; +import org.libvirt.LibvirtException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +@ResourceWrapper(handles = TakeKbossBackupCommand.class) +public class LibvirtTakeKbossBackupCommandWrapper extends CommandWrapper { + @Override + public Answer execute(TakeKbossBackupCommand command, LibvirtComputingResource resource) { + String vmName = command.getVmName(); + logger.info("Starting backup process for VM [{}].", vmName); + List kbossTOS = command.getKbossTOs(); + List> volumeTosAndNewPaths = + kbossTOS.stream().map(kbossTO -> new Pair<>(kbossTO.getVolumeObjectTO(), kbossTO.getDeltaPathOnPrimary())).collect(Collectors.toList()); + + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + Map mapVolumeUuidToNewVolumePath = new HashMap<>(); + + KVMStoragePoolManager storagePoolManager = resource.getStoragePoolMgr(); + boolean runningVM = command.isRunningVM(); + + try { + if (runningVM) { + resource.createDiskOnlyVmSnapshotForRunningVm(volumeTosAndNewPaths, vmName, UUID.randomUUID().toString(), command.isQuiesceVm()); + } else { + resource.createDiskOnlyVMSnapshotOfStoppedVm(volumeTosAndNewPaths, vmName); + } + + backupVolumes(command, resource, storagePoolManager, kbossTOS, volumeTosAndNewPaths, vmName, runningVM, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + cleanupVm(command, resource, kbossTOS, vmName, runningVM, mapVolumeUuidToNewVolumePath); + } catch (BackupException ex) { + return new TakeKbossBackupAnswer(command, ex); + } + + return new TakeKbossBackupAnswer(command, true, mapVolumeUuidToNewVolumePath, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + } + + /** + * Backup (copy) volumes to secondary storage. Will also populate the mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize argument. + * The timeout for this method is guided by the wait time for the given command, if the wait time is bigger than 24 days, there will be an overflow on the timeout. + *
    + * If an exception is caught while copying the volumes, will try to recover the VM to the previous state so that it is consistent. + * */ + protected void backupVolumes(TakeKbossBackupCommand command, LibvirtComputingResource resource, KVMStoragePoolManager storagePoolManager, List kbossTOS, + List> volumeTosAndNewPaths, String vmName, boolean runningVM, + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize) { + try { + int maxWaitInMillis = command.getWait() * 1000; + for (KbossTO kbossTO : kbossTOS) { + long startTimeMillis = System.currentTimeMillis(); + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + String volumeUuid = volumeObjectTO.getUuid(); + + logger.debug("Backing up volume [{}].", volumeUuid); + Pair deltaPathOnSecondaryAndSize = copyBackupDeltaToSecondary(storagePoolManager, kbossTO, command.getBackupChainImageStoreUrls(), + command.getImageStoreUrl(), maxWaitInMillis); + + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.put(volumeUuid, deltaPathOnSecondaryAndSize); + maxWaitInMillis = calculateRemainingTime(maxWaitInMillis, startTimeMillis); + } + } catch (Exception ex) { + logger.error("There has been an exception during the backup creation process. We will try to revert the VM [{}] to its previous state. The exception is: {}", vmName, + ex.getMessage(), ex); + recoverPreviousVmStateAndDeletePartialBackup(resource, volumeTosAndNewPaths, vmName, runningVM, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, storagePoolManager, + command.getImageStoreUrl()); + + throw new BackupException(String.format("There was an exception during the backup process for VM [%s], but the VM has been successfully normalized.", vmName), ex, + true); + } + } + + protected int calculateRemainingTime(int maxWaitInMillis, long startTimeMillis) throws TimeoutException { + maxWaitInMillis -= (int)(System.currentTimeMillis() - startTimeMillis); + if (maxWaitInMillis < 0) { + throw new TimeoutException("Timeout while converting backups to secondary storage."); + } + return maxWaitInMillis; + } + + /** + * For each KbossTO, will merge its DeltaMergeTreeTO (if it exists). Also, if this is the end of the chain, will also end the chain for the volume. + * Will populate the mapVolumeUuidToNewVolumePath argument. + * */ + protected void cleanupVm(TakeKbossBackupCommand command, LibvirtComputingResource resource, List kbossTOS, String vmName, boolean runningVM, + Map mapVolumeUuidToNewVolumePath) { + for (KbossTO kbossTO : kbossTOS) { + VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); + String currentVolumePath = volumeObjectTO.getPath(); + String volumeUuid = volumeObjectTO.getUuid(); + DeltaMergeTreeTO deltaMergeTreeTO = kbossTO.getDeltaMergeTreeTO(); + volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); + + if (deltaMergeTreeTO != null) { + List snapshotDataStoreVos = kbossTO.getVmSnapshotDeltaPaths(); + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVM, volumeUuid, snapshotDataStoreVos.isEmpty()); + } + + if (command.isEndChain() || command.isIsolated()) { + String baseVolumePath = currentVolumePath; + if (deltaMergeTreeTO != null && deltaMergeTreeTO.getChild().getPath().equals(baseVolumePath)) { + baseVolumePath = deltaMergeTreeTO.getParent().getPath(); + } + endChainForVolume(resource, volumeObjectTO, vmName, runningVM, volumeUuid, baseVolumePath); + mapVolumeUuidToNewVolumePath.put(volumeUuid, baseVolumePath); + } else { + mapVolumeUuidToNewVolumePath.put(volumeUuid, kbossTO.getDeltaPathOnPrimary()); + } + } + } + + /** + * Copy the backup delta to the secondary storage. Since we created a snapshot on top of the volume, the volume is now the backup delta. + * If there were snapshots created after the last backup, they'll be copied alongside and merged in the secondary storage. + * */ + protected Pair copyBackupDeltaToSecondary(KVMStoragePoolManager storagePoolManager, KbossTO kbossTO, List chainImageStoreUrls, String imageStoreUrl, + int waitInMillis) { + VolumeObjectTO delta = kbossTO.getVolumeObjectTO(); + String parentDeltaPathOnSecondary = kbossTO.getPathBackupParentOnSecondary(); + List deltaPathsToCopy = CollectionUtils.isEmpty(kbossTO.getVmSnapshotDeltaPaths()) ? new ArrayList<>() : new ArrayList<>(kbossTO.getVmSnapshotDeltaPaths()); + deltaPathsToCopy.add(delta.getPath()); + + KVMStoragePool parentImagePool = null; + List chainImagePools = null; + KVMStoragePool imagePool = null; + long backupSize; + final String backupOnSecondary = kbossTO.getDeltaPathOnSecondary(); + ArrayList temporaryDeltasToRemove = new ArrayList<>(); + boolean result = false; + try { + imagePool = storagePoolManager.getStoragePoolByURI(imageStoreUrl); + if (chainImageStoreUrls != null) { + parentImagePool = storagePoolManager.getStoragePoolByURI(chainImageStoreUrls.get(0)); + chainImagePools = chainImageStoreUrls.subList(1, chainImageStoreUrls.size()).stream().map(storagePoolManager::getStoragePoolByURI).collect(Collectors.toList()); + } + + PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) delta.getDataStore(); + KVMStoragePool primaryPool = storagePoolManager.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid()); + + String topDelta = backupOnSecondary; + while (!deltaPathsToCopy.isEmpty()) { + String backupDeltaFullPathOnSecondary = imagePool.getLocalPathFor(topDelta); + temporaryDeltasToRemove.add(backupDeltaFullPathOnSecondary); + String parentBackupFullPath = null; + + if (parentDeltaPathOnSecondary != null) { + parentBackupFullPath = parentImagePool.getLocalPathFor(parentDeltaPathOnSecondary); + } + + String backupDeltaFullPathOnPrimary = primaryPool.getLocalPathFor(deltaPathsToCopy.remove(0)); + convertDeltaToSecondary(backupDeltaFullPathOnPrimary, backupDeltaFullPathOnSecondary, parentBackupFullPath, delta.getUuid(), waitInMillis); + + if (!deltaPathsToCopy.isEmpty()) { + parentDeltaPathOnSecondary = topDelta; + topDelta = getRelativePathOnSecondaryForBackup(delta.getAccountId(), delta.getVolumeId(), UUID.randomUUID().toString()); + parentImagePool = imagePool; + } + } + + String backupOnSecondaryFullPath = imagePool.getLocalPathFor(backupOnSecondary); + + commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(topDelta, backupOnSecondary, imagePool, backupOnSecondaryFullPath, waitInMillis); + + backupSize = Files.size(Path.of(backupOnSecondaryFullPath)); + result = true; + } catch (LibvirtException | QemuImgException | IOException e) { + logger.error("Exception while converting backup [{}] to secondary storage [{}] due to: [{}].", delta.getPath(), imagePool, e.getMessage(), e); + throw new BackupException("Exception while converting backup to secondary storage.", e, true); + } finally { + removeTemporaryDeltas(temporaryDeltasToRemove, result); + + if (parentImagePool != null) { + storagePoolManager.deleteStoragePool(parentImagePool.getType(), parentImagePool.getUuid()); + } + if (chainImagePools != null) { + chainImagePools.forEach(pool -> storagePoolManager.deleteStoragePool(pool.getType(), pool.getUuid())); + } + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + return new Pair<>(backupOnSecondary, backupSize); + } + + /** + * If there were VM snapshots created after the last backup, we will have copied them alongside the backup delta. If this is the case, we will commit all of them into a single + * base file so that we are left with one file per volume per backup. + * */ + protected void commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(String topDelta, String backupOnSecondary, KVMStoragePool imagePool, String backupOnSecondaryFullPath, + int waitInMillis) throws LibvirtException, QemuImgException { + if (topDelta.equals(backupOnSecondary)) { + return; + } + + QemuImg qemuImg = new QemuImg(waitInMillis); + QemuImgFile topDeltaImg = new QemuImgFile(imagePool.getLocalPathFor(topDelta), QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile baseDeltaImg = new QemuImgFile(backupOnSecondaryFullPath, QemuImg.PhysicalDiskFormat.QCOW2); + + logger.debug("Committing top delta [{}] on base delta [{}].", topDeltaImg, baseDeltaImg); + qemuImg.commit(topDeltaImg, baseDeltaImg, true); + } + + /** + * Will remove any temporary deltas created on secondary storage. If result is true, this means that the backup was a success and the first "temporary delta" is our backup, so + * it will not be removed. + *
    + * There are two uses for this method:
    + * - If we fail to backup we have to clean up the secondary storage.
    + * - If we had VM snapshots created after the last backup, we copied multiple files to secondary storage, and thus we have to clean them up after merging them. + * */ + protected void removeTemporaryDeltas(List temporaryDeltasToRemove, boolean result) { + if (result) { + temporaryDeltasToRemove.remove(0); + } + logger.debug("Removing temporary deltas {}.", temporaryDeltasToRemove); + for (String delta : temporaryDeltasToRemove) { + try { + Files.deleteIfExists(Path.of(delta)); + } catch (IOException ex) { + logger.error("Failed to remove temporary delta [{}]. Will not stop the backup process, but this should be investigated.", delta, ex); + } + } + } + + /** + * Converts a delta from primary storage to secondary storage, if a parent was given, will set it as the backing file for the delta being copied. + * + * @param pathDeltaOnPrimary absolute path of the delta to be copied. + * @param pathDeltaOnSecondary absolute path of the destination of the delta to be copied. + * @param pathParentOnSecondary absolute path of the parent delta, if it exists. + * @param volumeUuid volume uuid, used for logging. + * @param waitInMillis timeout in milliseconds. + * */ + protected void convertDeltaToSecondary(String pathDeltaOnPrimary, String pathDeltaOnSecondary, String pathParentOnSecondary, String volumeUuid, int waitInMillis) + throws QemuImgException, LibvirtException { + QemuImgFile backupDestination = new QemuImgFile(pathDeltaOnSecondary, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile backupOrigin = new QemuImgFile(pathDeltaOnPrimary, QemuImg.PhysicalDiskFormat.QCOW2); + QemuImgFile parentBackup = null; + + if (pathParentOnSecondary != null) { + parentBackup = new QemuImgFile(pathParentOnSecondary, QemuImg.PhysicalDiskFormat.QCOW2); + } + + logger.debug("Converting delta [{}] to [{}] with {}", backupOrigin, backupDestination, parentBackup == null ? "no parent." : String.format("parent [%s].", parentBackup)); + + createDirsIfNeeded(pathDeltaOnSecondary, volumeUuid); + + QemuImg qemuImg = new QemuImg(waitInMillis); + qemuImg.convert(backupOrigin, backupDestination, parentBackup, null, null, new QemuImageOptions(backupOrigin.getFormat(), backupOrigin.getFileName(), null), null, + true, false, false, false, null, null); + } + + + protected void endChainForVolume(LibvirtComputingResource resource, VolumeObjectTO volumeObjectTO, String vmName, boolean isVmRunning, String volumeUuid, String baseVolumePath) + throws BackupException { + + BackupDeltaTO baseVolume = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, baseVolumePath); + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, baseVolume, volumeObjectTO, new ArrayList<>()); + + logger.debug("Ending backup chain for volume [{}], the next backup will be a full backup.", volumeObjectTO.getUuid()); + + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, isVmRunning, volumeUuid, false); + } + + /** + * Tries to recover the previous state of the VM. Should only be called if an exception in the backup creation process happened.
    + * For each volume, will:
    + * - Merge back any backup deltas created; + * - Remove the data backed up to the secondary storage; + * */ + protected void recoverPreviousVmStateAndDeletePartialBackup(LibvirtComputingResource resource, List> volumeTosAndNewPaths, String vmName, + boolean runningVm, Map> mapVolumeUuidToDeltaPathOnSecondaryAndSize, KVMStoragePoolManager storagePoolManager, String imageStoreUrl) { + for (Pair volumeObjectTOAndNewPath : volumeTosAndNewPaths) { + VolumeObjectTO volumeObjectTO = volumeObjectTOAndNewPath.first(); + String volumeUuid = volumeObjectTO.getUuid(); + + BackupDeltaTO oldDelta = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, volumeObjectTO.getPath()); + volumeObjectTO.setPath(volumeObjectTOAndNewPath.second()); + DeltaMergeTreeTO deltaMergeTreeTO = new DeltaMergeTreeTO(volumeObjectTO, oldDelta, volumeObjectTO, new ArrayList<>()); + + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVm, volumeUuid, false); + + Pair deltaPathOnSecondaryAndSize = mapVolumeUuidToDeltaPathOnSecondaryAndSize.get(volumeUuid); + if (deltaPathOnSecondaryAndSize == null) { + continue; + } + + cleanupDeltaOnSecondary(storagePoolManager, imageStoreUrl, deltaPathOnSecondaryAndSize.first()); + } + } + + protected void cleanupDeltaOnSecondary(KVMStoragePoolManager storagePoolManager, String imageStoreUrl, String deltaPath) { + KVMStoragePool imagePool = null; + + try { + imagePool = storagePoolManager.getStoragePoolByURI(imageStoreUrl); + String fullDeltaPath = imagePool.getLocalPathFor(deltaPath); + + logger.debug("Cleaning up delta at [{}] as part of the post backup error normalization effort.", fullDeltaPath); + + Files.deleteIfExists(Path.of(fullDeltaPath)); + } catch (IOException e) { + logger.error("Exception while trying to cleanup delta at [{}].", deltaPath, e); + } finally { + if (imagePool != null) { + storagePoolManager.deleteStoragePool(imagePool.getType(), imagePool.getUuid()); + } + } + } + + + protected void mergeBackupDelta(LibvirtComputingResource resource, DeltaMergeTreeTO deltaMergeTreeTO, VolumeObjectTO volumeObjectTO, String vmName, boolean isVmRunning, + String volumeUuid, boolean countNewestDeltaAsGrandchild) throws BackupException { + try { + if (isVmRunning) { + resource.mergeDeltaForRunningVm(deltaMergeTreeTO, vmName, volumeObjectTO); + } else { + if (countNewestDeltaAsGrandchild) { + deltaMergeTreeTO.addGrandChild(volumeObjectTO); + } + resource.mergeDeltaForStoppedVm(deltaMergeTreeTO); + } + } catch (LibvirtException | QemuImgException | IOException e) { + logger.error("Exception while merging the last backup delta using delta merge tree [{}] for VM [{}] and volume [{}].", deltaMergeTreeTO, vmName, volumeUuid, e); + throw new BackupException(String.format("Exception during backup wrap-up phase for VM [%s].", vmName), e, false); + } + } + + protected String getRelativePathOnSecondaryForBackup(long accountId, long volumeId, String backupPath) { + return String.format("%s%s%s%s%s%s%s", "backups", File.separator, accountId, File.separator, volumeId, File.separator, backupPath); + } + + protected void createDirsIfNeeded(String deltaFullPath, String volumeUuid) { + String dirs = deltaFullPath.substring(0, deltaFullPath.lastIndexOf(File.separator)); + try { + Files.createDirectories(Path.of(dirs)); + } catch (IOException e) { + throw new BackupException(String.format("Error while creating directories for backup of volume [%s].", volumeUuid), e, true); + } + } + +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateVmNicCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateVmNicCommandWrapper.java new file mode 100644 index 000000000000..85f00e1dbd49 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtUpdateVmNicCommandWrapper.java @@ -0,0 +1,67 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.UpdateVmNicAnswer; +import com.cloud.agent.api.UpdateVmNicCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +@ResourceWrapper(handles = UpdateVmNicCommand.class) +public final class LibvirtUpdateVmNicCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(UpdateVmNicCommand command, LibvirtComputingResource libvirtComputingResource) { + String nicMacAddress = command.getNicMacAddress(); + String vmName = command.getVmName(); + boolean isEnabled = command.isEnabled(); + + Domain vm = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + final Connect conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + vm = libvirtComputingResource.getDomain(conn, vmName); + + final InterfaceDef nic = libvirtComputingResource.getInterface(conn, vmName, nicMacAddress); + nic.setLinkStateUp(isEnabled); + vm.updateDeviceFlags(nic.toString(), Domain.DeviceModifyFlags.LIVE); + + return new UpdateVmNicAnswer(command, true, "success"); + } catch (final LibvirtException e) { + final String msg = String.format(" Update NIC failed due to %s.", e); + logger.warn(msg, e); + return new UpdateVmNicAnswer(command, false, msg); + } finally { + if (vm != null) { + try { + vm.free(); + } catch (final LibvirtException l) { + logger.trace("Ignoring libvirt error.", l); + } + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java new file mode 100644 index 000000000000..eec68cd464e3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtValidateKbossVmCommandWrapper.java @@ -0,0 +1,225 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.DateUtil; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.json.JsonSanitizer; +import org.apache.cloudstack.backup.ValidateKbossVmAnswer; +import org.apache.cloudstack.backup.ValidateKbossVmCommand; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +@ResourceWrapper(handles = ValidateKbossVmCommand.class) +public class LibvirtValidateKbossVmCommandWrapper extends CommandWrapper { + + private static final String SCREENSHOT_COMMAND = "virsh screenshot --domain %s --file %s"; + private static final String GUEST_SYNC_COMMAND = "{\"execute\": \"guest-sync\", \"arguments\":{\"id\":%s}}"; + private static final String GUEST_EXEC_COMMAND = "{\"execute\": \"guest-exec\", \"arguments\":{\"path\":\"%s\",\"arg\":%s,\"capture-output\":true}}"; + private static final String GUEST_EXEC_STATUS_COMMAND = "{\"execute\": \"guest-exec-status\", \"arguments\":{\"pid\":%s}}"; + + @Override + public Answer execute(ValidateKbossVmCommand command, LibvirtComputingResource serverResource) { + VirtualMachineTO vmTo = command.getVm(); + KVMStoragePool secondaryStorage = null; + KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + try { + Domain vm = serverResource.getDomain(serverResource.getLibvirtUtilitiesHelper().getConnection(), vmTo.getName()); + secondaryStorage = storagePoolMgr.getStoragePoolByURI(command.getBackupDeltaTO().getDataStore().getUrl()); + logger.info("Validating VM [{}].", vm.getName()); + boolean bootValidated = waitForBoot(command, vm); + String screenshotPath = takeScreenshot(command, vm, secondaryStorage); + String scriptResult = runScript(command, vm); + return new ValidateKbossVmAnswer(command, bootValidated, screenshotPath, scriptResult); + } catch (LibvirtException e) { + logger.error("Received Libvirt exception while trying to validate VM [{}].", vmTo.getName(), e); + return new Answer(command, e); + } finally { + if (secondaryStorage != null) { + storagePoolMgr.deleteStoragePool(secondaryStorage.getType(), secondaryStorage.getUuid()); + } + } + } + + private boolean waitForBoot(ValidateKbossVmCommand cmd, Domain vm) throws LibvirtException { + if (!cmd.isWaitForBoot()) { + return false; + } + Random random = new Random(); + Integer bootTimeout = cmd.getBootTimeout(); + logger.debug("Waiting for validation VM [{}] to boot. We will wait for [{}] seconds at most.", vm.getName(), bootTimeout); + while (bootTimeout > 0) { + bootTimeout -= 5; + int randomInt = random.nextInt(); + try { + String result = vm.qemuAgentCommand(String.format(GUEST_SYNC_COMMAND, randomInt), 1, 0); + if (result.contains(String.valueOf(randomInt))) { + logger.info("Validation VM [{}] has booted.", vm.getName()); + return true; + } + } catch (LibvirtException ex) { + if (!ex.getMessage().contains(LibvirtComputingResource.AGENT_IS_NOT_CONNECTED)) { + logger.error("Got an unexpected Libvirt Exception, giving up on validating VM [{}].", vm.getName(), ex); + throw ex; + } + } + try { + Thread.sleep(5 * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting for VM [{}] to boot. Ignoring.", vm.getName()); + } + } + logger.debug("Boot wait timed out for VM [{}].", vm.getName()); + return false; + } + + private String takeScreenshot(ValidateKbossVmCommand command, Domain vm, KVMStoragePool secondaryStorage) throws LibvirtException { + if (!command.isTakeScreenshot()) { + return null; + } + String vmName = vm.getName(); + try { + logger.info("Waiting [{}] seconds to take screenshot of validation VM [{}].", command.getScreenshotWait(), vm.getName()); + Thread.sleep(command.getScreenshotWait() * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting to take screenshot of validation VM [{}].", vm.getName()); + } + logger.info("Taking screenshot of VM [{}].", vmName); + String ssPath = secondaryStorage.getLocalPathFor(command.getBackupDeltaTO().getPath()) + String.format("-screenshot-%s", DateUtil.getDateInSystemTimeZone()); + if (Script.runSimpleBashScript(String.format(SCREENSHOT_COMMAND, vmName, ssPath)) == null) { + throw new CloudRuntimeException(String.format("Got an unexpected error while trying to execute the screenshot validation step for VM [%s].", vmName)); + } + try { + return tryToConvertFileToPng(ssPath); + } catch (IOException ex) { + throw new CloudRuntimeException(ex); + } + } + + private String tryToConvertFileToPng(String ssPath) throws IOException { + File inputFile = new File(ssPath); + String pngPath = ssPath + ".png"; + File outputFile = new File(pngPath); + BufferedImage image = ImageIO.read(inputFile); + + String warnMessage = String.format("Unable to convert screenshot at [%s] to PNG. Leaving it as is.", ssPath); + if (image == null) { + logger.warn(warnMessage); + return ssPath.substring(ssPath.indexOf("backups")); + } + boolean result = ImageIO.write(image, "png", outputFile); + + if (result) { + logger.debug("Successfully converted image at [{}] to PNG at [{}].", ssPath, pngPath); + Files.deleteIfExists(Path.of(ssPath)); + return pngPath.substring(ssPath.indexOf("backups")); + } else { + logger.warn(warnMessage); + return ssPath.substring(ssPath.indexOf("backups")); + } + } + + private String runScript(ValidateKbossVmCommand command, Domain vm) throws LibvirtException { + if (!command.isExecuteScript()) { + return null; + } + String script = command.getScriptToExecute(); + if (script == null) { + logger.warn("This command is malformed, we should execute an script for VM [{}], but no script was configured. Please review the original VM configurations.", vm.getName()); + return null; + } + String arguments = command.getScriptArguments(); + if (arguments == null) { + arguments = "[]"; + } else { + arguments = "[\"" + arguments.replace(",", "\",\"") + "\"]"; + } + logger.debug("Running validation script [{}] with arguments [{}] on dummy validation VM [{}].", script, arguments, vm.getName()); + String guestCommand = String.format(GUEST_EXEC_COMMAND, script, arguments); + String sanitizedGuestCommand = JsonSanitizer.sanitize(guestCommand); + String execResult; + try { + execResult = vm.qemuAgentCommand(sanitizedGuestCommand, command.getScriptTimeout(), 0); + } catch (LibvirtException ex) { + return ex.getMessage(); + } + JsonObject root = new JsonParser().parse(execResult).getAsJsonObject(); + JsonObject ret = root.getAsJsonObject("return"); + String pid = ret.get("pid").getAsString(); + + return waitForCommandResult(command, vm, pid, script, arguments); + } + + private String waitForCommandResult(ValidateKbossVmCommand command, Domain vm, String pid, String script, String arguments) throws LibvirtException { + JsonObject root; + JsonObject ret; + String expectedResult = command.getExpectedResult(); + Integer timeout = command.getScriptTimeout(); + while (timeout > 0) { + timeout -= 5; + try { + String statusResult = vm.qemuAgentCommand(String.format(GUEST_EXEC_STATUS_COMMAND, pid), 1, 0); + root = new JsonParser().parse(statusResult).getAsJsonObject(); + ret = root.getAsJsonObject("return"); + + boolean exited = ret.get("exited").getAsBoolean(); + if (exited) { + int exitCode = ret.get("exitcode").getAsInt(); + String outData64Coded = ret.get("out-data").getAsString(); + logger.debug("Script [{}] with arguments [{}] that ran on validation VM [{}] exited with code [{}] and had output [{}].", script, arguments, vm.getName(), + exitCode, outData64Coded); + if (expectedResult.equals("0") && exitCode == 0) { + return null; + } + if (outData64Coded.equals(expectedResult)) { + return null; + } + return outData64Coded; + } + } catch (LibvirtException ex) { + logger.error("Caught unexpected Libvirt exception while waiting for validation script result of VM [{}]. Will try again later.", vm.getName(), ex); + } + try { + Thread.sleep(5 * 1000L); + } catch (InterruptedException e) { + logger.debug("Got interrupted while waiting for execution of validation script for VM [{}]. Ignoring.", vm.getName()); + } + } + logger.error("Script [{}] that was executed on validation VM [{}] has timed out, giving up.", script, vm.getName()); + return "Timeout"; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java new file mode 100644 index 000000000000..e9ec65505e40 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java @@ -0,0 +1,1071 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.hypervisor.kvm.storage; + +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef; +import com.cloud.storage.Storage; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.StorageLayer; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.joda.time.Duration; +import org.libvirt.Connect; +import org.libvirt.LibvirtException; +import org.libvirt.StoragePool; +import org.libvirt.StorageVol; + +/** + * Storage adaptor for CLVM and CLVM_NG pool types. + * Extends LibvirtStorageAdaptor and overrides methods with CLVM-specific logic, + * using direct LVM commands instead of libvirt for volume operations. + */ +public class ClvmStorageAdaptor extends LibvirtStorageAdaptor { + + public ClvmStorageAdaptor(StorageLayer storage) { + super(storage); + } + + @Override + public StoragePoolType getStoragePoolType() { + // Registered manually for both CLVM and CLVM_NG in KVMStoragePoolManager + return null; + } + + @Override + public KVMStoragePool createStoragePool(String name, String host, int port, String path, + String userInfo, StoragePoolType type, Map details, boolean isPrimaryStorage) { + logger.info("Attempting to create CLVM/CLVM_NG storage pool {} in libvirt", name); + + Connect conn; + try { + conn = LibvirtConnection.getConnection(); + } catch (LibvirtException e) { + throw new CloudRuntimeException(e.toString()); + } + + StoragePool sp = createCLVMStoragePool(conn, name, host, path); + if (sp == null) { + logger.info("Falling back to virtual CLVM/CLVM_NG pool without libvirt for: {}", name); + return createVirtualClvmPool(name, host, path, type, details); + } + + try { + if (!isPrimaryStorage) { + incStoragePoolRefCount(name); + } + // CLVM/CLVM_NG pools are kept inactive in libvirt; we use direct LVM commands + return getStoragePool(name); + } catch (Exception e) { + decStoragePoolRefCount(name); + throw new CloudRuntimeException("Failed to create CLVM storage pool: " + name, e); + } + } + + @Override + public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { + logger.info("Fetching CLVM/CLVM_NG storage pool {} ", uuid); + try { + Connect conn = LibvirtConnection.getConnection(); + StoragePool storage = conn.storagePoolLookupByUUIDString(uuid); + + LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); + if (spd == null) { + throw new CloudRuntimeException("Unable to parse storage pool definition for pool " + uuid); + } + + // CLVM pools in libvirt are always LOGICAL type + StoragePoolType type = StoragePoolType.CLVM; + + // Do NOT activate the pool — CLVM/CLVM_NG pools stay inactive in libvirt + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); + pool.setLocalPath(spd.getTargetPath()); + + // Always read capacity from LVM directly + String vgName = storage.getName(); + try { + long[] vgStats = getVgStats(vgName); + setPoolCapacityFromVgStats(pool, vgStats, vgName); + } catch (CloudRuntimeException e) { + logger.warn("Failed to get VG stats for CLVM/CLVM_NG pool {}: {}. Using libvirt values (may be 0)", vgName, e.getMessage()); + pool.setCapacity(storage.getInfo().capacity); + pool.setUsed(storage.getInfo().allocation); + pool.setAvailable(storage.getInfo().available); + } + + return pool; + } catch (LibvirtException e) { + logger.debug("CLVM/CLVM_NG pool {} not found in libvirt, creating virtual pool", uuid); + throw new CloudRuntimeException(e.toString(), e); + } + } + + @Override + public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + + // Pool has no libvirt backing - go directly to block device + if (libvirtPool.getPool() == null) { + logger.debug("CLVM/CLVM_NG pool has no libvirt backing, using direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } + + try { + StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + logger.debug("Volume {} not found in libvirt, falling back to CLVM direct access", volumeUuid); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW); + return disk; + } catch (LibvirtException e) { + logger.warn("LibvirtException looking up volume {}: {}", volumeUuid, e.getMessage()); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + } + + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, + PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + logger.info("Creating CLVM/CLVM_NG volume {} in pool {} with size {}", name, pool.getUuid(), toHumanReadableSize(size)); + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + return createClvmNgDiskWithBacking(name, 0, size, null, pool, provisioningType); + } else { + return createClvmVolume(name, size, pool); + } + } + + @Override + public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { + if (isVMMigrate) { + logger.info("Activating CLVM/CLVM_NG volume {} in shared mode for VM migration", name); + Script activateVol = new Script("lvchange", 30000, logger); + activateVol.add("-asy"); + activateVol.add(pool.getLocalPath() + File.separator + name); + String result = activateVol.execute(); + if (result != null) { + logger.error("Failed to activate CLVM/CLVM_NG volume {} in shared mode. Output: {}", name, result); + return false; + } + } + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + ensureClvmNgBackingFileAccessible(name, pool); + } + + return true; + } + + @Override + public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, + String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, + long size, KVMStoragePool destPool, int timeout, byte[] passphrase) { + + if (StoragePoolType.CLVM_NG.equals(destPool.getType()) && format == PhysicalDiskFormat.QCOW2) { + logger.info("Creating CLVM_NG volume {} with backing file from template {}", name, template.getName()); + String backingFile = getClvmBackingFile(template, destPool); + return createClvmNgDiskWithBacking(name, timeout, size, backingFile, destPool, provisioningType); + } + + return super.createDiskFromTemplate(template, name, format, provisioningType, size, destPool, timeout, passphrase); + } + + @Override + public void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String lvName = "template-" + templateUuid; + String lvPath = "/dev/" + vgName + "/" + lvName; + + if (lvExists(lvPath)) { + logger.info("Template LV {} already exists in VG {}. Skipping creation.", lvName, vgName); + return; + } + + logger.info("Creating new template LV {} in VG {} for template {}", lvName, vgName, templateUuid); + + long virtualSize = getQcow2VirtualSize(templatePath); + long physicalSize = getQcow2PhysicalSize(templatePath); + long lvSize = virtualSize; + + logger.info("Template source - Physical: {} bytes, Virtual: {} bytes, LV will be: {} bytes", physicalSize, virtualSize, lvSize); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", lvName); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG template: " + result); + } + + Script qemuImgConvert = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImgConvert.add("convert"); + qemuImgConvert.add(templatePath); + qemuImgConvert.add("-O", "qcow2"); + qemuImgConvert.add("-o", "cluster_size=64k,extended_l2=off,preallocation=off"); + qemuImgConvert.add(lvPath); + result = qemuImgConvert.execute(); + + if (result != null) { + removeLvOnFailure(lvPath, timeout); + throw new CloudRuntimeException("Failed to convert template to CLVM_NG volume: " + result); + } + + long actualVirtualSize = getQcow2VirtualSize(lvPath); + + try { + ensureTemplateLvInSharedMode(lvPath, true); + } catch (CloudRuntimeException e) { + logger.error("Failed to activate template LV {} in shared mode. Cleaning up.", lvPath); + removeLvOnFailure(lvPath, timeout); + throw e; + } + + KVMPhysicalDisk templateDisk = new KVMPhysicalDisk(lvPath, lvName, pool); + templateDisk.setFormat(PhysicalDiskFormat.QCOW2); + templateDisk.setVirtualSize(actualVirtualSize); + templateDisk.setSize(lvSize); + } + + private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { + String volgroupPath = "/dev/" + path; + String volgroupName = path; + volgroupName = volgroupName.replaceFirst("^/", ""); + + Script checkVgExists = new Script("vgs", 30000, logger); + checkVgExists.add("--noheadings"); + checkVgExists.add("-o", "vg_name"); + checkVgExists.add(volgroupName); + String vgCheckResult = checkVgExists.execute(); + + if (vgCheckResult != null) { + logger.error("Volume group {} does not exist or is not accessible", volgroupName); + return null; + } + + logger.info("Volume group {} verified, creating libvirt pool definition for CLVM/CLVM_NG", volgroupName); + LibvirtStoragePoolDef poolDef = new LibvirtStoragePoolDef( + LibvirtStoragePoolDef.PoolType.LOGICAL, + volgroupName, + uuid, + null, + volgroupName, + volgroupPath + ); + + try { + StoragePool pool = conn.storagePoolDefineXML(poolDef.toString(), 0); + logger.info("Created libvirt pool definition for CLVM/CLVM_NG VG: {} (pool will remain inactive)", volgroupName); + pool.setAutostart(1); + return pool; + } catch (LibvirtException e) { + logger.warn("Failed to define CLVM/CLVM_NG pool in libvirt: {}", e.getMessage()); + return null; + } + } + + private void setPoolCapacityFromVgStats(LibvirtStoragePool pool, long[] vgStats, String vgName) { + long capacity = vgStats[0]; + long available = vgStats[1]; + long used = capacity - available; + + pool.setCapacity(capacity); + pool.setAvailable(available); + pool.setUsed(used); + + logger.debug("CLVM/CLVM_NG pool {} - Capacity: {}, Used: {}, Available: {}", + vgName, toHumanReadableSize(capacity), toHumanReadableSize(used), toHumanReadableSize(available)); + } + + private long[] getVgStats(String vgName) { + Script getVgStats = new Script("vgs", 30000, logger); + getVgStats.add("--noheadings"); + getVgStats.add("--units", "b"); + getVgStats.add("--nosuffix"); + getVgStats.add("-o", "vg_size,vg_free"); + getVgStats.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = getVgStats.execute(parser); + + if (result != null) { + String errorMsg = "Failed to get statistics for volume group " + vgName + ": " + result; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String output = parser.getLines().trim(); + String[] lines = output.split("\\n"); + String dataLine = null; + + for (String line : lines) { + line = line.trim(); + if (!line.isEmpty() && Character.isDigit(line.charAt(0))) { + dataLine = line; + break; + } + } + + if (dataLine == null) { + String errorMsg = "No valid data line found in vgs output for " + vgName + ": " + output; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String[] stats = dataLine.split("\\s+"); + + if (stats.length < 2) { + String errorMsg = "Unexpected output from vgs command for " + vgName + ": " + dataLine; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + try { + long capacity = Long.parseLong(stats[0].trim()); + long available = Long.parseLong(stats[1].trim()); + return new long[]{capacity, available}; + } catch (NumberFormatException e) { + String errorMsg = "Failed to parse VG statistics for " + vgName + ": " + e.getMessage(); + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg, e); + } + } + + private KVMStoragePool createVirtualClvmPool(String uuid, String host, String path, StoragePoolType type, Map details) { + String volgroupName = path.replaceFirst("^/", ""); + String volgroupPath = "/dev/" + volgroupName; + + logger.info("Creating virtual CLVM/CLVM_NG pool {} without libvirt using direct LVM access", volgroupName); + + long[] vgStats = getVgStats(volgroupName); + + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, volgroupName, type, this, null); + pool.setLocalPath(volgroupPath); + setPoolCapacityFromVgStats(pool, vgStats, volgroupName); + + if (details != null) { + pool.setDetails(details); + } + + return pool; + } + + /** + * CLVM fallback: First tries to refresh libvirt pool to make volume visible, + * if that fails, accesses volume directly via block device path. + */ + private KVMPhysicalDisk getPhysicalDiskWithClvmFallback(String volumeUuid, KVMStoragePool pool, LibvirtStoragePool libvirtPool) { + logger.info("CLVM volume not visible to libvirt, attempting pool refresh for volume: {}", volumeUuid); + + try { + logger.debug("Refreshing libvirt storage pool: {}", pool.getUuid()); + libvirtPool.getPool().refresh(0); + + StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol != null) { + logger.info("Volume found after pool refresh: {}", volumeUuid); + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW); + return disk; + } + } catch (LibvirtException refreshEx) { + logger.debug("Pool refresh failed or volume still not found: {}", refreshEx.getMessage()); + } + + logger.info("Falling back to direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } + + private String getVgName(String sourceDir) { + String vgName = sourceDir; + if (vgName.startsWith("/")) { + String[] parts = vgName.split("/"); + List tokens = Arrays.stream(parts) + .filter(s -> !s.isEmpty()).collect(Collectors.toList()); + + vgName = tokens.size() > 1 ? tokens.get(1) + : tokens.size() == 1 ? tokens.get(0) + : ""; + } + return vgName; + } + + private String extractVgNameFromPool(KVMStoragePool pool) { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + throw new CloudRuntimeException("CLVM pool sourceDir is not set, cannot determine VG name"); + } + String vgName = getVgName(sourceDir); + logger.debug("Using VG name: {} (from sourceDir: {})", vgName, sourceDir); + return vgName; + } + + /** + * For CLVM volumes that exist in LVM but are not visible to libvirt, + * access them directly via block device path. + */ + private KVMPhysicalDisk getPhysicalDiskViaDirectBlockDevice(String volumeUuid, KVMStoragePool pool) { + try { + String vgName = extractVgNameFromPool(pool); + + verifyLvExistsInVg(volumeUuid, vgName); + + logger.info("Volume {} exists in LVM but not visible to libvirt, accessing directly", volumeUuid); + + String lvPath = findAccessibleDeviceNode(volumeUuid, vgName, pool); + long size = getClvmVolumeSize(lvPath); + + KVMPhysicalDisk disk = createPhysicalDiskFromClvmLv(lvPath, volumeUuid, pool, size); + ensureTemplateAccessibility(volumeUuid, lvPath, pool); + + return disk; + } catch (CloudRuntimeException ex) { + throw ex; + } catch (Exception ex) { + logger.error("Failed to access CLVM volume via direct block device: {}", volumeUuid, ex); + throw new CloudRuntimeException(String.format("Could not find volume %s: %s", volumeUuid, ex.getMessage())); + } + } + + private void verifyLvExistsInVg(String volumeUuid, String vgName) { + logger.debug("Checking if volume {} exists in VG {}", volumeUuid, vgName); + Script checkLvCmd = new Script("/usr/sbin/lvs", 30000, logger); + checkLvCmd.add("--noheadings"); + checkLvCmd.add("--unbuffered"); + checkLvCmd.add(vgName + "/" + volumeUuid); + String checkResult = checkLvCmd.execute(); + if (checkResult != null) { + throw new CloudRuntimeException(String.format("Storage volume not found: no storage vol with matching name '%s'", volumeUuid)); + } + } + + private String findAccessibleDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + String lvPath = "/dev/" + vgName + "/" + volumeUuid; + File lvDevice = new File(lvPath); + + if (!lvDevice.exists()) { + lvPath = tryDeviceMapperPath(volumeUuid, vgName); + if (!new File(lvPath).exists()) { + lvPath = handleMissingDeviceNode(volumeUuid, vgName, pool); + } + } + + return lvPath; + } + + private String tryDeviceMapperPath(String volumeUuid, String vgName) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + return "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + } + + private String handleMissingDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + return activateTemplateAndGetPath(volumeUuid, vgName); + } + throw new CloudRuntimeException(String.format("Could not find volume %s in VG %s - volume exists in LVM but device node not accessible", volumeUuid, vgName)); + } + + private String activateTemplateAndGetPath(String volumeUuid, String vgName) { + logger.info("Template volume {} device node not found. Attempting to activate in shared mode.", volumeUuid); + String templateLvPath = "/dev/" + vgName + "/" + volumeUuid; + + try { + ensureTemplateLvInSharedMode(templateLvPath, false); + + String lvPath = findDeviceNodeAfterActivation(templateLvPath, volumeUuid, vgName); + + logger.info("Successfully activated template volume {} at {}", volumeUuid, lvPath); + return lvPath; + } catch (CloudRuntimeException e) { + throw new CloudRuntimeException(String.format("Failed to activate template volume %s in VG %s: %s", volumeUuid, vgName, e.getMessage()), e); + } + } + + private String findDeviceNodeAfterActivation(String templateLvPath, String volumeUuid, String vgName) { + File lvDevice = new File(templateLvPath); + String lvPath = templateLvPath; + + if (!lvDevice.exists()) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + lvPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + lvDevice = new File(lvPath); + } + + if (!lvDevice.exists()) { + logger.error("Template volume {} still not accessible after activation attempt", volumeUuid); + throw new CloudRuntimeException(String.format("Could not activate template volume %s in VG %s - device node not accessible after activation", volumeUuid, vgName)); + } + + return lvPath; + } + + private void ensureTemplateAccessibility(String volumeUuid, String lvPath, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + logger.info("Detected template volume {}. Ensuring it's activated in shared mode.", volumeUuid); + ensureTemplateLvInSharedMode(lvPath, false); + } + } + + private long getClvmVolumeSize(String lvPath) { + try { + Script lvsCmd = new Script("/usr/sbin/lvs", 30000, logger); + lvsCmd.add("--noheadings"); + lvsCmd.add("--units"); + lvsCmd.add("b"); + lvsCmd.add("-o"); + lvsCmd.add("lv_size"); + lvsCmd.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = lvsCmd.execute(parser); + + String output = (result == null) ? parser.getLines() : result; + + if (output != null && !output.isEmpty()) { + String sizeStr = output.trim().replaceAll("[^0-9]", ""); + if (!sizeStr.isEmpty()) { + return Long.parseLong(sizeStr); + } + } + } catch (Exception sizeEx) { + logger.warn("Failed to get size for CLVM volume via lvs: {}", sizeEx.getMessage()); + File lvDevice = new File(lvPath); + if (lvDevice.isFile()) { + return lvDevice.length(); + } + } + return 0; + } + + private KVMPhysicalDisk createPhysicalDiskFromClvmLv(String lvPath, String volumeUuid, KVMStoragePool pool, long size) { + PhysicalDiskFormat diskFormat = StoragePoolType.CLVM_NG.equals(pool.getType()) + ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW; + + logger.debug("{} pool detected, setting disk format to {} for volume {}", pool.getType(), diskFormat, volumeUuid); + + KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); + disk.setFormat(diskFormat); + disk.setSize(size); + disk.setVirtualSize(diskFormat == PhysicalDiskFormat.QCOW2 ? getQcow2VirtualSize(lvPath) : size); + + logger.info("Successfully accessed CLVM/CLVM_NG volume via direct block device: {} with format: {} and size: {} bytes", + lvPath, diskFormat, size); + return disk; + } + + /** + * Checks if a CLVM_NG QCOW2 volume has a backing file (template) and ensures it's activated in shared mode. + */ + private void ensureClvmNgBackingFileAccessible(String volumeName, KVMStoragePool pool) { + try { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + + logger.debug("Checking if CLVM_NG volume {} has a backing file that needs activation", volumePath); + + Script qemuImgInfo = new Script("qemu-img", Duration.millis(10000), logger); + qemuImgInfo.add("info"); + qemuImgInfo.add("--output=json"); + qemuImgInfo.add(volumePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImgInfo.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String jsonOutput = parser.getLines(); + + if (jsonOutput.contains("\"backing-filename\"")) { + int backingStart = jsonOutput.indexOf("\"backing-filename\""); + if (backingStart > 0) { + int valueStart = jsonOutput.indexOf(":", backingStart); + if (valueStart > 0) { + valueStart = jsonOutput.indexOf("\"", valueStart) + 1; + int valueEnd = jsonOutput.indexOf("\"", valueStart); + + if (valueEnd > valueStart) { + String backingFile = jsonOutput.substring(valueStart, valueEnd).trim(); + if (!backingFile.isEmpty() && backingFile.startsWith("/dev/")) { + logger.info("Volume {} has backing file: {}. Ensuring backing file is in shared mode.", volumePath, backingFile); + ensureTemplateLvInSharedMode(backingFile, false); + } + } + } + } + } else { + logger.debug("Volume {} does not have a backing file (full clone)", volumePath); + } + } + } catch (Exception e) { + logger.warn("Failed to check/activate backing file for volume {}: {}. VM deployment may fail if template is not accessible.", + volumeName, e.getMessage()); + } + } + + private String getClvmBackingFile(KVMPhysicalDisk template, KVMStoragePool destPool) { + String templateLvName = template.getName(); + KVMPhysicalDisk templateOnPrimary = null; + + try { + templateOnPrimary = destPool.getPhysicalDisk(templateLvName); + } catch (CloudRuntimeException e) { + logger.warn("Template {} not found on CLVM_NG pool {}.", templateLvName, destPool.getUuid()); + } + + if (templateOnPrimary != null) { + String backingFile = templateOnPrimary.getPath(); + logger.info("Using template on primary storage as backing file: {}", backingFile); + ensureTemplateLvInSharedMode(backingFile); + return backingFile; + } + + logger.error("Template {} should be on primary storage before creating volumes from it", templateLvName); + throw new CloudRuntimeException(String.format("Template not found on CLVM_NG primary storage: %s. Template must be copied to primary storage first.", templateLvName)); + } + + /** + * Ensures a template LV is activated in shared mode so multiple VMs can use it as a backing file. + */ + private void ensureTemplateLvInSharedMode(String templatePath, boolean throwOnFailure) { + try { + Script checkLvs = new Script("lvs", Duration.millis(30000), logger); + checkLvs.add("--noheadings"); + checkLvs.add("-o", "lv_attr"); + checkLvs.add(templatePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = checkLvs.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String lvAttr = parser.getLines().trim(); + if (lvAttr.length() >= 6) { + boolean isActive = (lvAttr.indexOf('a') >= 0); + boolean isShared = (lvAttr.indexOf('s') >= 0); + + if (!isShared || !isActive) { + logger.info("Template LV {} is not in shared mode (attr: {}). Activating in shared mode.", templatePath, lvAttr); + LibvirtComputingResource.setClvmVolumeToSharedMode(templatePath); + } else { + logger.debug("Template LV {} is already in shared mode (attr: {})", templatePath, lvAttr); + } + } + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = "Failed to check/ensure template LV shared mode for " + templatePath + ": " + e.getMessage(); + if (throwOnFailure) { + throw new CloudRuntimeException(errorMsg, e); + } else { + logger.warn(errorMsg, e); + } + } + } + + private void ensureTemplateLvInSharedMode(String templatePath) { + ensureTemplateLvInSharedMode(templatePath, false); + } + + private long getVgPhysicalExtentSize(String vgName) { + final long DEFAULT_PE_SIZE = 4 * 1024 * 1024L; + String warningMessage = String.format("Failed to get PE size for VG %s, defaulting to 4MiB", vgName); + + try { + Script vgDisplay = new Script("vgdisplay", 300000, logger); + vgDisplay.add("--units", "b"); + vgDisplay.add("-C"); + vgDisplay.add("--noheadings"); + vgDisplay.add("-o", "vg_extent_size"); + vgDisplay.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = vgDisplay.execute(parser); + + if (result != null) { + logger.warn("{}: {}", warningMessage, result); + return DEFAULT_PE_SIZE; + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + logger.warn("{}: empty output", warningMessage); + return DEFAULT_PE_SIZE; + } + + output = output.trim(); + if (output.endsWith("B")) { + output = output.substring(0, output.length() - 1).trim(); + } + + long peSize = Long.parseLong(output); + logger.debug("Physical Extent size for VG {} is {} bytes", vgName, peSize); + return peSize; + } catch (NumberFormatException e) { + logger.warn("{}: failed to parse PE size", warningMessage, e); + } catch (Exception e) { + logger.warn("{}: {}", warningMessage, e.getMessage()); + } + + logger.info("Using default PE size for VG {}: {} bytes (4 MiB)", vgName, DEFAULT_PE_SIZE); + return DEFAULT_PE_SIZE; + } + + /** + * Calculate LVM LV size for CLVM_NG volume allocation. + * {@code peSize} must be the Physical Extent size of the VG (from {@link #getVgPhysicalExtentSize}). + */ + private long calculateClvmNgLvSize(long virtualSize, long peSize) { + long clusterSize = 64 * 1024L; + long l2Multiplier = 4096L; + + long numDataClusters = (virtualSize + clusterSize - 1) / clusterSize; + long numL2Clusters = (numDataClusters + l2Multiplier - 1) / l2Multiplier; + long l2TableSize = numL2Clusters * clusterSize; + long refcountTableSize = l2TableSize; + + long headerOverhead = 2 * 1024 * 1024L; + long metadataOverhead = l2TableSize + refcountTableSize + headerOverhead; + long targetSize = virtualSize + metadataOverhead; + long roundedSize = ((targetSize + peSize - 1) / peSize) * peSize; + long virtualSizeGiB = virtualSize / (1024 * 1024 * 1024L); + long overheadMiB = metadataOverhead / (1024 * 1024L); + + logger.info("Calculated volume LV size: {} bytes (virtual: {} GiB, QCOW2 metadata overhead: {} MiB, rounded to {} PEs, PE size = {} bytes)", + roundedSize, virtualSizeGiB, overheadMiB, roundedSize / peSize, peSize); + + return roundedSize; + } + + private long getQcow2VirtualSize(String imagePath) { + Script qemuImg = new Script("qemu-img", 300000, logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add("-U"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 virtual size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("virtual-size").getAsLong(); + } + + private long getQcow2PhysicalSize(String imagePath) { + Script qemuImg = new Script("qemu-img", Duration.millis(300000), logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 physical size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("actual-size").getAsLong(); + } + + private KVMPhysicalDisk createClvmNgDiskWithBacking(String volumeUuid, int timeout, long virtualSize, String backingFile, + KVMStoragePool pool, Storage.ProvisioningType provisioningType) { + String vgName = getVgName(pool.getLocalPath()); + // Query PE size once and reuse for both the QCOW2 virtual-size alignment and the + long peSize = getVgPhysicalExtentSize(vgName); + long peAlignedVirtualSize = ((virtualSize + peSize - 1) / peSize) * peSize; + long lvSize = calculateClvmNgLvSize(peAlignedVirtualSize, peSize); + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + logger.debug("Creating CLVM_NG volume {} with LV size {} bytes (requested virtual: {} bytes, PE-aligned virtual: {} bytes, provisioning: {})", + volumeUuid, lvSize, virtualSize, peAlignedVirtualSize, provisioningType); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeUuid); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG volume: " + result); + } + + Script qemuImg = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImg.add("create"); + qemuImg.add("-f", "qcow2"); + + StringBuilder qcow2Options = new StringBuilder(); + String preallocation = (provisioningType == Storage.ProvisioningType.THIN) ? "off" : "metadata"; + qcow2Options.append("preallocation=").append(preallocation); + qcow2Options.append(",extended_l2=on"); + qcow2Options.append(",cluster_size=64k"); + + if (backingFile != null && !backingFile.isEmpty()) { + qcow2Options.append(",backing_file=").append(backingFile); + qcow2Options.append(",backing_fmt=qcow2"); + logger.debug("Creating CLVM_NG volume with backing file: {}", backingFile); + } + + qemuImg.add("-o", qcow2Options.toString()); + qemuImg.add(volumePath); + qemuImg.add(peAlignedVirtualSize + ""); + + result = qemuImg.execute(); + if (result != null) { + removeLvOnFailure(volumePath, timeout); + throw new CloudRuntimeException("Failed to create QCOW2 on CLVM_NG volume: " + result); + } + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeUuid, pool); + disk.setFormat(PhysicalDiskFormat.QCOW2); + disk.setSize(actualSize); + disk.setVirtualSize(peAlignedVirtualSize); + + logger.info("Successfully created CLVM_NG volume {} (LV size: {}, PE-aligned virtual size: {}, provisioning: {}, preallocation: {})", + volumeUuid, lvSize, peAlignedVirtualSize, provisioningType, preallocation); + + return disk; + } + + private boolean lvExists(String lvPath) { + Script checkLv = new Script("lvs", Duration.millis(30000), logger); + checkLv.add("--noheadings"); + checkLv.add("--unbuffered"); + checkLv.add(lvPath); + return checkLv.execute() == null; + } + + private void removeLvOnFailure(String lvPath, int timeout) { + Script lvremove = new Script("lvremove", Duration.millis(timeout), logger); + lvremove.add("-f"); + lvremove.add(lvPath); + lvremove.execute(); + } + + private KVMPhysicalDisk createClvmVolume(String volumeName, long size, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + int timeout = 30000; + + logger.info("Creating CLVM volume {} in VG {} with size {} bytes", volumeName, vgName, size); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeName); + lvcreate.add("-L", size + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create CLVM volume: " + result); + } + + logger.info("Successfully created CLVM volume {} at {} with size {}", volumeName, volumePath, toHumanReadableSize(size)); + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeName, pool); + disk.setFormat(PhysicalDiskFormat.RAW); + disk.setSize(actualSize); + disk.setVirtualSize(actualSize); + + return disk; + } + + @Override + public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.ImageFormat format) { + logger.info("CLVM/CLVM_NG pool detected - using direct LVM cleanup with secure zero-fill for volume {}", uuid); + return cleanupCLVMVolume(uuid, pool); + } + + /** + * Clean up CLVM volume and its snapshots directly using LVM commands. + */ + private boolean cleanupCLVMVolume(String uuid, KVMStoragePool pool) { + logger.info("Starting direct LVM cleanup for CLVM volume: {} in pool: {}", uuid, pool.getUuid()); + + try { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + logger.debug("Source directory is null or empty, cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + String vgName = getVgName(sourceDir); + logger.info("Determined VG name: {} for pool: {}", vgName, pool.getUuid()); + + if (vgName == null || vgName.isEmpty()) { + logger.warn("Cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + + String lvPath = "/dev/" + vgName + "/" + uuid; + logger.debug("Volume path: {}", lvPath); + + Script checkLvs = new Script("lvs", 30000, logger); + checkLvs.add("--noheadings"); + checkLvs.add("--unbuffered"); + checkLvs.add(lvPath); + + logger.info("Checking if volume exists: lvs --noheadings --unbuffered {}", lvPath); + String checkResult = checkLvs.execute(); + + if (checkResult != null) { + logger.info("CLVM volume {} does not exist in LVM (check returned: {}), considering it as already deleted", uuid, checkResult); + return true; + } + + logger.info("Volume {} exists, proceeding with cleanup", uuid); + + boolean secureZeroFillEnabled = shouldSecureZeroFill(pool); + + if (secureZeroFillEnabled) { + logger.info("Step 1: Zero-filling volume {} for security", uuid); + secureZeroFillVolume(lvPath, uuid); + } else { + logger.info("Secure zero-fill is disabled, skipping zero-filling for volume {}", uuid); + } + + logger.info("Step 2: Removing volume {}", uuid); + Script removeLv = new Script("lvremove", 30000, logger); + removeLv.add("-f"); + removeLv.add(lvPath); + + logger.info("Executing command: lvremove -f {}", lvPath); + String removeResult = removeLv.execute(); + + if (removeResult == null) { + logger.info("Successfully removed CLVM volume {} using direct LVM cleanup", uuid); + return true; + } else { + logger.warn("Command 'lvremove -f {}' returned error: {}", lvPath, removeResult); + if (removeResult.contains("not found") || removeResult.contains("Failed to find")) { + logger.info("CLVM volume {} not found during cleanup, considering it as already deleted", uuid); + return true; + } + return false; + } + } catch (Exception ex) { + logger.error("Exception during CLVM volume cleanup for {}: {}", uuid, ex.getMessage(), ex); + return true; + } + } + + private boolean shouldSecureZeroFill(KVMStoragePool pool) { + Map details = pool.getDetails(); + String secureZeroFillStr = (details != null) ? details.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL) : null; + return Boolean.parseBoolean(secureZeroFillStr); + } + + /** + * Securely zero-fill a volume before deletion to prevent data leakage. + * Uses blkdiscard (fast TRIM) as primary method, with dd zero-fill as fallback. + */ + private void secureZeroFillVolume(String lvPath, String volumeUuid) { + logger.info("Starting secure zero-fill for CLVM volume: {} at path: {}", volumeUuid, lvPath); + + boolean blkdiscardSuccess = false; + + try { + Script blkdiscard = new Script("blkdiscard", 300000, logger); + blkdiscard.add("-f"); + blkdiscard.add(lvPath); + + String result = blkdiscard.execute(); + if (result == null) { + logger.info("Successfully zero-filled CLVM volume {} using blkdiscard (TRIM)", volumeUuid); + blkdiscardSuccess = true; + } else { + if (result.contains("Operation not supported") || result.contains("BLKDISCARD ioctl failed")) { + logger.info("blkdiscard not supported for volume {} (device doesn't support TRIM/DISCARD), using dd fallback", volumeUuid); + } else { + logger.warn("blkdiscard failed for volume {}: {}, will try dd fallback", volumeUuid, result); + } + } + } catch (Exception e) { + logger.warn("Exception during blkdiscard for volume {}: {}, will try dd fallback", volumeUuid, e.getMessage()); + } + + if (!blkdiscardSuccess) { + logger.info("Attempting zero-fill using dd for CLVM volume: {}", volumeUuid); + try { + String command = String.format( + "nice -n 19 ionice -c 2 -n 7 dd if=/dev/zero of=%s bs=1M oflag=direct 2>&1 || true", + lvPath + ); + + Script ddZeroFill = new Script("/bin/bash", 3600000, logger); + ddZeroFill.add("-c"); + ddZeroFill.add(command); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String ddResult = ddZeroFill.execute(parser); + String output = parser.getLines(); + + if (output != null && (output.contains("copied") || output.contains("records in") || + output.contains("No space left on device"))) { + logger.info("Successfully zero-filled CLVM volume {} using dd", volumeUuid); + } else if (ddResult == null) { + logger.info("Zero-fill completed for CLVM volume {}", volumeUuid); + } else { + logger.warn("dd zero-fill for volume {} completed with output: {}", volumeUuid, + output != null ? output : ddResult); + } + } catch (Exception e) { + logger.warn("Failed to zero-fill CLVM volume {} before deletion: {}. Proceeding with deletion anyway.", + volumeUuid, e.getMessage()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java index ba689d5107f7..495ae45a2896 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java @@ -16,9 +16,14 @@ // under the License. package com.cloud.hypervisor.kvm.storage; +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.nio.file.Files; +import java.nio.file.Paths; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; @@ -95,14 +100,8 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map/device/delete — the standard Linux kernel SCSI + * API for removing a single device without tearing down the entire iSCSI session. + * Once the kernel processes the delete, it also removes the by-path symlink. + * + * This is used instead of iscsiadm --logout when other LUNs on the same IQN are still + * active (ONTAP single-IQN-per-SVM model), since logout would tear down ALL LUNs. + */ + private void removeStaleScsiDevice(String host, int port, String iqn, String lun) { + String byPath = getByPath(host, port, "/" + iqn + "/" + lun); + Path byPathLink = Paths.get(byPath); + if (!Files.exists(byPathLink)) { + logger.debug("by-path entry for LUN " + lun + " already gone, nothing to remove"); + return; + } + try { + Path realDevice = byPathLink.toRealPath(); + String devName = realDevice.getFileName().toString(); + File deleteFile = new File("/sys/block/" + devName + "/device/delete"); + if (!deleteFile.exists()) { + logger.warn("sysfs delete entry not found for device " + devName + " — cannot remove stale SCSI device"); + return; + } + try (FileWriter fw = new FileWriter(deleteFile)) { + fw.write("1"); + } + logger.info("Removed stale SCSI device " + devName + " for LUN /" + iqn + "/" + lun + " via sysfs"); + } catch (Exception e) { + logger.warn("Failed to remove stale SCSI device for LUN /" + iqn + "/" + lun + ": " + e.getMessage()); + } + } + private boolean disconnectPhysicalDisk(String host, int port, String iqn, String lun) { - // use iscsiadm to log out of the iSCSI target and un-discover it + // Check if other LUNs on the same IQN target are still in use. + // ONTAP (and similar) uses a single IQN per SVM with multiple LUNs. + // Doing iscsiadm --logout tears down the ENTIRE target session, + // which would destroy access to ALL LUNs — not just the one being disconnected. + if (hasOtherActiveLuns(host, port, iqn, lun)) { + logger.info("Skipping iSCSI logout for /" + iqn + "/" + lun + + " — other LUNs on the same target are still active. Removing stale SCSI device for this LUN only."); + removeStaleScsiDevice(host, port, iqn, lun); + // After removing this LUN's device, re-check: if no other LUNs remain active, + // If it is the last one then must logout to clean up the iSCSI session entirely. + if (hasOtherActiveLuns(host, port, iqn, lun)) { + logger.info("Other LUNs still active after removing /" + iqn + "/" + lun + " — session kept alive."); + return true; + } + logger.info("No more active LUNs on target after removing /" + iqn + "/" + lun + " — proceeding with iSCSI logout."); + } + + // No other LUNs active on this target — safe to logout and delete the node record. // ex. sudo iscsiadm -m node -T iqn.2012-03.com.test:volume1 -p 192.168.233.10:3260 --logout Script iScsiAdmCmd = new Script(true, "iscsiadm", 0, logger); @@ -422,6 +571,19 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk srcDisk, String destVolu try { QemuImg q = new QemuImg(timeout); q.convert(srcFile, destFile); + // Below fix is required when vendor depends on host based copy rather than storage CAN_CREATE_VOLUME_FROM_VOLUME capability + // When host based template copy is triggered , small size template sits in RAM(depending on host memory and RAM) and copy is marked successful and by the time flush to storage is triggered + // disconnectPhysicalDisk would disconnect the lun , hence template staying in RAM is not copied to storage lun. Below does flushing of data to storage and marking + // copy as successful once flush is complete. + Script flushCmd = new Script(true, "blockdev", 0, logger); + flushCmd.add("--flushbufs", destDisk.getPath()); + String flushResult = flushCmd.execute(); + if (flushResult != null) { + logger.warn("iSCSI copyPhysicalDisk: blockdev --flushbufs returned: {}", flushResult); + } + Script syncCmd = new Script(true, "sync", 0, logger); + syncCmd.execute(); + logger.info("iSCSI copyPhysicalDisk: flush/sync completed "); } catch (QemuImgException | LibvirtException ex) { String msg = "Failed to copy data from " + srcDisk.getPath() + " to " + destDisk.getPath() + ". The error was the following: " + ex.getMessage(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStoragePool.java index f5bfd898a4f4..8cf6de68f955 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStoragePool.java @@ -208,12 +208,12 @@ public String getStorageNodeId() { } @Override - public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) { + public Boolean hasHeartBeat(HAStoragePool pool, HostTO host) { return null; } @Override - public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { + public Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { return null; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java index 8dd2116e1235..a8207cec3fa6 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java @@ -33,35 +33,34 @@ public interface KVMStoragePool { - public static final long HeartBeatUpdateTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEARTBEAT_UPDATE_TIMEOUT); - public static final long HeartBeatUpdateFreq = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); - public static final long HeartBeatUpdateMaxTries = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_MAX_TRIES); - public static final long HeartBeatUpdateRetrySleep = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_RETRY_SLEEP); - public static final long HeartBeatCheckerTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_CHECKER_TIMEOUT); + public static final String CLVM_SECURE_ZERO_FILL = "clvmsecurezerofill"; + long HeartBeatUpdateTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEARTBEAT_UPDATE_TIMEOUT); + long HeartBeatUpdateFreqInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); + long HeartBeatCheckerTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_CHECKER_TIMEOUT); - public default KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { + default KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, Long usableSize, byte[] passphrase) { return createPhysicalDisk(volumeUuid, format, provisioningType, size, passphrase); } - public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); + KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); - public KVMPhysicalDisk createPhysicalDisk(String volumeUuid, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); + KVMPhysicalDisk createPhysicalDisk(String volumeUuid, Storage.ProvisioningType provisioningType, long size, byte[] passphrase); - public boolean connectPhysicalDisk(String volumeUuid, Map details); + boolean connectPhysicalDisk(String volumeUuid, Map details); - public KVMPhysicalDisk getPhysicalDisk(String volumeUuid); + KVMPhysicalDisk getPhysicalDisk(String volumeUuid); - public boolean disconnectPhysicalDisk(String volumeUuid); + boolean disconnectPhysicalDisk(String volumeUuid); - public boolean deletePhysicalDisk(String volumeUuid, Storage.ImageFormat format); + boolean deletePhysicalDisk(String volumeUuid, Storage.ImageFormat format); - public List listPhysicalDisks(); + List listPhysicalDisks(); - public String getUuid(); + String getUuid(); - public long getCapacity(); + long getCapacity(); - public long getUsed(); + long getUsed(); default Long getCapacityIops() { return null; @@ -71,51 +70,51 @@ default Long getUsedIops() { return null; } - public long getAvailable(); + long getAvailable(); - public boolean refresh(); + boolean refresh(); - public boolean isExternalSnapshot(); + boolean isExternalSnapshot(); - public String getLocalPath(); + String getLocalPath(); - public String getSourceHost(); + String getSourceHost(); - public String getSourceDir(); + String getSourceDir(); - public int getSourcePort(); + int getSourcePort(); - public String getAuthUserName(); + String getAuthUserName(); - public String getAuthSecret(); + String getAuthSecret(); - public StoragePoolType getType(); + StoragePoolType getType(); - public boolean delete(); + boolean delete(); PhysicalDiskFormat getDefaultFormat(); - public boolean createFolder(String path); + boolean createFolder(String path); - public boolean supportsConfigDriveIso(); + boolean supportsConfigDriveIso(); - public Map getDetails(); + Map getDetails(); default String getLocalPathFor(String relativePath) { return String.format("%s%s%s", getLocalPath(), File.separator, relativePath); } - public boolean isPoolSupportHA(); + boolean isPoolSupportHA(); - public String getHearthBeatPath(); + String getHearthBeatPath(); - public String createHeartBeatCommand(HAStoragePool primaryStoragePool, String hostPrivateIp, boolean hostValidation); + String createHeartBeatCommand(HAStoragePool primaryStoragePool, String hostPrivateIp, boolean hostValidation); - public String getStorageNodeId(); + String getStorageNodeId(); - public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host); + Boolean hasHeartBeat(HAStoragePool pool, HostTO host); - public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration); + Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration); default LibvirtVMDef.DiskDef.BlockIOSize getSupportedLogicalBlockSize() { return null; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java index 6665cf625e2f..d99847fd921a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java @@ -72,7 +72,9 @@ private StorageAdaptor getStorageAdaptor(StoragePoolType type) { private void addStoragePool(String uuid, StoragePoolInformation pool) { synchronized (_storagePools) { - if (!_storagePools.containsKey(uuid)) { + // Insert on first registration; on subsequent calls (e.g. ModifyStoragePoolCommand) + // overwrite when new details are present so config changes are reflected + if (!_storagePools.containsKey(uuid) || MapUtils.isNotEmpty(pool.getDetails())) { _storagePools.put(uuid, pool); } } @@ -81,6 +83,10 @@ private void addStoragePool(String uuid, StoragePoolInformation pool) { public KVMStoragePoolManager(StorageLayer storagelayer, KVMHAMonitor monitor) { this._haMonitor = monitor; this._storageMapper.put("libvirt", new LibvirtStorageAdaptor(storagelayer)); + // Register CLVM/CLVM_NG adaptor explicitly for both types (one shared instance) + ClvmStorageAdaptor clvmAdaptor = new ClvmStorageAdaptor(storagelayer); + this._storageMapper.put(StoragePoolType.CLVM.toString(), clvmAdaptor); + this._storageMapper.put(StoragePoolType.CLVM_NG.toString(), clvmAdaptor); // add other storage adaptors manually here // add any adaptors that wish to register themselves via call to adaptor.getStoragePoolType() @@ -92,8 +98,8 @@ public KVMStoragePoolManager(StorageLayer storagelayer, KVMHAMonitor monitor) { logger.debug("Skipping registration of abstract class / interface " + storageAdaptorClass.getName()); continue; } - if (storageAdaptorClass.isAssignableFrom(LibvirtStorageAdaptor.class)) { - logger.debug("Skipping re-registration of LibvirtStorageAdaptor"); + if (storageAdaptorClass == LibvirtStorageAdaptor.class || storageAdaptorClass == ClvmStorageAdaptor.class) { + logger.debug("Skipping re-registration of explicitly registered adaptor: {}", storageAdaptorClass.getSimpleName()); continue; } try { @@ -269,10 +275,10 @@ public boolean disconnectPhysicalDisksViaVmSpec(VirtualMachineTO vmSpec) { } public KVMStoragePool getStoragePool(StoragePoolType type, String uuid) { - return this.getStoragePool(type, uuid, false); + return this.getStoragePool(type, uuid, false, true); } - public KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean refreshInfo) { + public synchronized KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean refreshInfo, boolean addDetails) { StorageAdaptor adaptor = getStorageAdaptor(type); KVMStoragePool pool = null; @@ -287,19 +293,45 @@ public KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean } } - if (pool instanceof LibvirtStoragePool) { - addPoolDetails(uuid, (LibvirtStoragePool) pool); + if (pool instanceof LibvirtStoragePool && addDetails) { + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + addPoolDetails(uuid, libvirtPool); + ((LibvirtStoragePool) pool).setType(type); + updatePoolTypeIfApplicable(libvirtPool, pool, type, uuid); } return pool; } + private void updatePoolTypeIfApplicable(LibvirtStoragePool libvirtPool, KVMStoragePool pool, + StoragePoolType type, String uuid) { + StoragePoolType correctType = type; + if (correctType == null || correctType == StoragePoolType.CLVM) { + StoragePoolInformation info = _storagePools.get(uuid); + if (info != null && info.getPoolType() != null) { + correctType = info.getPoolType(); + } + } + + if (correctType != null && correctType != pool.getType() && + (correctType == StoragePoolType.CLVM || correctType == StoragePoolType.CLVM_NG) && + (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG)) { + logger.debug("Correcting pool type from {} to {} for pool {} based on caller/cached information", + pool.getType(), correctType, uuid); + libvirtPool.setType(correctType); + } + } + /** * As the class {@link LibvirtStoragePool} is constrained to the {@link org.libvirt.StoragePool} class, there is no way of saving a generic parameter such as the details, hence, * this method was created to always make available the details of libvirt primary storages for when they are needed. */ private void addPoolDetails(String uuid, LibvirtStoragePool pool) { StoragePoolInformation storagePoolInformation = _storagePools.get(uuid); + if (storagePoolInformation == null) { + logger.warn("No cached StoragePoolInformation found for pool UUID {}, pool details will not be set.", uuid); + return; + } Map details = storagePoolInformation.getDetails(); if (MapUtils.isNotEmpty(details)) { @@ -390,6 +422,9 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri private synchronized KVMStoragePool createStoragePool(String name, String host, int port, String path, String userInfo, StoragePoolType type, Map details, boolean primaryStorage) { StorageAdaptor adaptor = getStorageAdaptor(type); KVMStoragePool pool = adaptor.createStoragePool(name, host, port, path, userInfo, type, details, primaryStorage); + if (pool instanceof LibvirtStoragePool) { + ((LibvirtStoragePool) pool).setType(type); + } // LibvirtStorageAdaptor-specific statement if (pool.isPoolSupportHA() && primaryStorage) { @@ -408,7 +443,7 @@ public boolean disconnectPhysicalDisk(StoragePoolType type, String poolUuid, Str return adaptor.disconnectPhysicalDisk(volPath, pool); } - public boolean deleteStoragePool(StoragePoolType type, String uuid) { + public synchronized boolean deleteStoragePool(StoragePoolType type, String uuid) { StorageAdaptor adaptor = getStorageAdaptor(type); if (type == StoragePoolType.NetworkFilesystem) { _haMonitor.removeStoragePool(uuid); @@ -450,6 +485,10 @@ public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String n return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.RAW, provisioningType, size, destPool, timeout, passphrase); + } else if (destPool.getType() == StoragePoolType.CLVM_NG) { + return adaptor.createDiskFromTemplate(template, name, + PhysicalDiskFormat.QCOW2, provisioningType, + size, destPool, timeout, passphrase); } else if (template.getFormat() == PhysicalDiskFormat.DIR) { return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.DIR, provisioningType, @@ -491,6 +530,11 @@ public KVMPhysicalDisk createPhysicalDiskFromDirectDownloadTemplate(String templ return adaptor.createTemplateFromDirectDownloadFile(templateFilePath, destTemplatePath, destPool, format, timeout); } + public void createTemplateOnClvmNg(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + StorageAdaptor adaptor = getStorageAdaptor(pool.getType()); + adaptor.createTemplate(templatePath, templateUuid, timeout, pool); + } + public Ternary, String> prepareStorageClient(StoragePoolType type, String uuid, Map details) { StorageAdaptor adaptor = getStorageAdaptor(type); return adaptor.prepareStorageClient(uuid, details); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 030d9747d6cd..11acb9546b53 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -21,6 +21,8 @@ import static com.cloud.utils.NumbersUtil.toHumanReadableSize; import static com.cloud.utils.storage.S3.S3Utils.putFile; +import com.cloud.template.TemplateManager; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -55,6 +57,7 @@ import com.cloud.agent.api.Command; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.storage.clvm.ClvmPoolManager; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -81,6 +84,7 @@ import org.apache.cloudstack.storage.command.SnapshotAndCopyCommand; import org.apache.cloudstack.storage.command.SyncVolumePathCommand; import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; +import org.apache.cloudstack.storage.to.BackupDeltaTO; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; @@ -185,6 +189,8 @@ public class KVMStorageProcessor implements StorageProcessor { private int incrementalSnapshotTimeout; + private int incrementalSnapshotRetryRebaseWait; + private static final String CHECKPOINT_XML_TEMP_DIR = "/tmp/cloudstack/checkpointXMLs"; private static final String BACKUP_XML_TEMP_DIR = "/tmp/cloudstack/backupXMLs"; @@ -223,6 +229,27 @@ public class KVMStorageProcessor implements StorageProcessor { " \n" + ""; + private static final String DUMMY_VM_XML_BLOCK = "\n" + + " %s\n" + + " 256\n" + + " 256\n" + + " 1\n" + + " \n" + + " hvm\n" + + " \n" + + " \n" + + " \n" + + " %s\n" + + " \n" + + " \n"+ + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + public static final List poolTypesToDeleteChainInfo = Arrays.asList(StoragePoolType.Filesystem, StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint); public KVMStorageProcessor(final KVMStoragePoolManager storagePoolMgr, final LibvirtComputingResource resource) { this.storagePoolMgr = storagePoolMgr; @@ -252,6 +279,7 @@ public boolean configure(final String name, final Map params) th _cmdsTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.CMDS_TIMEOUT) * 1000; incrementalSnapshotTimeout = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.INCREMENTAL_SNAPSHOT_TIMEOUT) * 1000; + incrementalSnapshotRetryRebaseWait = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.INCREMENTAL_SNAPSHOT_RETRY_REBASE_WAIT) * 1000; return true; } @@ -344,15 +372,28 @@ public Answer copyTemplateToPrimaryStorage(final CopyCommand cmd) { path = destTempl.getUuid(); } - if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { - logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); - return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); - } + if (primaryPool.getType() == StoragePoolType.CLVM_NG) { + logger.info("Copying template {} to CLVM_NG pool {}", + destTempl.getUuid(), primaryPool.getUuid()); - primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + try { + storagePoolMgr.createTemplateOnClvmNg(tmplVol.getPath(), path, cmd.getWaitInMillSeconds(), primaryPool); + primaryVol = primaryPool.getPhysicalDisk("template-" + path); + } catch (Exception e) { + logger.error("Failed to create CLVM_NG template: {}", e.getMessage(), e); + return new PrimaryStorageDownloadAnswer("Failed to create CLVM_NG template: " + e.getMessage()); + } + } else { + if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { + logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); + } - if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { - logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + + if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { + logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + } } } else { primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, UUID.randomUUID().toString(), primaryPool, cmd.getWaitInMillSeconds()); @@ -373,7 +414,8 @@ public Answer copyTemplateToPrimaryStorage(final CopyCommand cmd) { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(primaryPool.getType())) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(primaryPool.getType())) { newTemplate.setFormat(ImageFormat.RAW); } else { newTemplate.setFormat(ImageFormat.QCOW2); @@ -530,7 +572,11 @@ public Answer cloneVolumeFromBaseTemplate(final CopyCommand cmd) { final VolumeObjectTO newVol = new VolumeObjectTO(); newVol.setPath(vol.getName()); - newVol.setSize(volume.getSize()); + if (StoragePoolType.CLVM_NG.equals(primaryStore.getPoolType()) && vol != null && vol.getVirtualSize() > 0) { + newVol.setSize(vol.getVirtualSize()); + } else { + newVol.setSize(volume.getSize()); + } if (vol.getQemuEncryptFormat() != null) { newVol.setEncryptFormat(vol.getQemuEncryptFormat().toString()); } @@ -586,7 +632,9 @@ public Answer copyVolumeFromImageCacheToPrimary(final CopyCommand cmd) { String path = details != null ? details.get(DiskTO.IQN) : null; - storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + if (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + } final String volumeName = UUID.randomUUID().toString(); @@ -615,7 +663,9 @@ public Answer copyVolumeFromImageCacheToPrimary(final CopyCommand cmd) { final KVMPhysicalDisk newDisk = storagePoolMgr.copyPhysicalDisk(volume, path != null ? path : volumeName, primaryPool, cmd.getWaitInMillSeconds()); resource.createOrUpdateLogFileForCommand(cmd, Command.State.COMPLETED); - storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path); + if (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path); + } final VolumeObjectTO newVol = new VolumeObjectTO(); @@ -1115,7 +1165,14 @@ public Answer backupSnapshot(final CopyCommand cmd) { } } else { final Script command = new Script(_manageSnapshotPath, cmd.getWaitInMillSeconds(), logger); - command.add("-b", isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath()); + String backupPath; + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + backupPath = snapshotDisk.getPath(); + logger.debug("Using snapshotDisk path for CLVM/CLVM_NG backup: " + backupPath); + } else { + backupPath = isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath(); + } + command.add("-b", backupPath); command.add(NAME_OPTION, snapshotName); command.add("-p", snapshotDestPath); @@ -1160,6 +1217,90 @@ public Answer backupSnapshot(final CopyCommand cmd) { } } + /** + * Parse CLVM/CLVM_NG snapshot path and compute MD5 hash. + * Snapshot path format: /dev/vgname/volumeuuid/snapshotuuid + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (for logging) + * @return Array of [vgName, volumeUuid, snapshotUuid, md5Hash] or null if invalid + */ + private String[] parseClvmSnapshotPath(String snapshotPath, StoragePoolType poolType) { + String[] pathParts = snapshotPath.split("/"); + if (pathParts.length < 5) { + logger.warn("Invalid {} snapshot path format: {}, expected format: /dev/vgname/volume-uuid/snapshot-uuid", + poolType, snapshotPath); + return null; + } + + String vgName = pathParts[2]; + String volumeUuid = pathParts[3]; + String snapshotUuid = pathParts[4]; + + logger.info("Parsed {} snapshot path - VG: {}, Volume: {}, Snapshot: {}", + poolType, vgName, volumeUuid, snapshotUuid); + + String md5Hash = computeMd5Hash(snapshotUuid); + logger.debug("Computed MD5 hash for snapshot UUID {}: {}", snapshotUuid, md5Hash); + + return new String[]{vgName, volumeUuid, snapshotUuid, md5Hash}; + } + + /** + * Delete a CLVM or CLVM_NG snapshot using managesnapshot.sh script. + * For both CLVM and CLVM_NG, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid + * The script handles MD5 transformation and pool-specific deletion commands internally: + * - CLVM: Uses lvremove to delete LVM snapshot + * - CLVM_NG: Uses qemu-img snapshot -d to delete QCOW2 internal snapshot + * This approach is consistent with snapshot creation and backup which also use the script. + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (CLVM or CLVM_NG) + * @param checkExistence If true, checks if snapshot exists before cleanup (for explicit deletion) + * If false, always performs cleanup (for post-backup cleanup) + * @return true if cleanup was performed, false if snapshot didn't exist (when checkExistence=true) + */ + private boolean deleteClvmSnapshot(String snapshotPath, StoragePoolType poolType, boolean checkExistence) { + logger.info("Starting {} snapshot deletion for path: {}, checkExistence: {}", poolType, snapshotPath, checkExistence); + + try { + String[] parsed = parseClvmSnapshotPath(snapshotPath, poolType); + if (parsed == null) { + return false; + } + + String vgName = parsed[0]; + String volumeUuid = parsed[1]; + String snapshotUuid = parsed[2]; + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + // Use managesnapshot.sh script for deletion (consistent with create/backup) + // Script handles MD5 transformation and pool-specific commands internally + Script deleteCommand = new Script(_manageSnapshotPath, 30000, logger); + deleteCommand.add("-d", volumePath); + deleteCommand.add("-n", snapshotUuid); + + logger.info("Executing: managesnapshot.sh -d {} -n {}", volumePath, snapshotUuid); + String result = deleteCommand.execute(); + + if (result == null) { + logger.info("Successfully deleted {} snapshot: {}", poolType, snapshotPath); + return true; + } else { + if (checkExistence && result.contains("does not exist")) { + logger.info("{} snapshot {} already deleted, no cleanup needed", poolType, snapshotPath); + return true; + } + logger.warn("Failed to delete {} snapshot {}: {}", poolType, snapshotPath, result); + return false; + } + + } catch (Exception ex) { + logger.error("Exception while deleting {} snapshot {}", poolType, snapshotPath, ex); + return false; + } + } + private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObjectTO snapshot, KVMStoragePool primaryPool) { String snapshotPath = snapshot.getPath(); @@ -1172,7 +1313,19 @@ private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObject if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) { try { - Files.deleteIfExists(Paths.get(snapshotPath)); + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // Both CLVM and CLVM_NG use the same deletion method via managesnapshot.sh script + boolean cleanedUp = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), false); + if (!cleanedUp) { + String[] parsedPath = parseClvmSnapshotPath(snapshotPath, primaryPool.getType()); + String snapMd5 = (parsedPath != null) ? computeMd5Hash(parsedPath[2]) : computeMd5Hash(snapshotPath); + logger.warn("Deletion of Snapshot: {} on primary store may have failed as it doesn't exist: {} " + + "(MD5 of snapshot UUID: {} - admins can use this to manually locate and delete the LV via managesnapshot.sh or lvremove)", + primaryPool.getType(), snapshotPath, snapMd5); + } + } else { + Files.deleteIfExists(Paths.get(snapshotPath)); + } } catch (IOException ex) { logger.error("Failed to delete snapshot [{}] on primary storage [{}].", snapshot.getId(), snapshot.getName(), ex); } @@ -1181,10 +1334,31 @@ private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObject } } - protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store) throws + + /** + * Compute MD5 hash of a string, matching what managesnapshot.sh does: + * echo "${snapshot}" | md5sum -t | awk '{ print $1 }' + */ + private String computeMd5Hash(String input) { + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] array = md.digest((input + "\n").getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : array) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + logger.error("Failed to compute MD5 hash for: {}", input, e); + return input; + } + } + + protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store, Integer deviceSeq) throws LibvirtException, InternalErrorException { DiskDef iso = new DiskDef(); boolean isUefiEnabled = MapUtils.isNotEmpty(params) && params.containsKey("UEFI"); + Integer devId = (deviceSeq != null) ? deviceSeq : TemplateManager.CDROM_PRIMARY_DEVICE_SEQ; if (isoPath != null && isAttach) { final int index = isoPath.lastIndexOf("/"); final String path = isoPath.substring(0, index); @@ -1200,9 +1374,9 @@ protected synchronized void attachOrDetachISO(final Connect conn, final String v final DiskDef.DiskType isoDiskType = LibvirtComputingResource.getDiskType(isoVol); isoPath = isoVol.getPath(); - iso.defISODisk(isoPath, isUefiEnabled, isoDiskType); + iso.defISODisk(isoPath, devId, isUefiEnabled, isoDiskType); } else { - iso.defISODisk(null, isUefiEnabled, DiskDef.DiskType.FILE); + iso.defISODisk(null, devId, isUefiEnabled, DiskDef.DiskType.FILE); } final List disks = resource.getDisks(conn, vmName); @@ -1222,11 +1396,12 @@ public Answer attachIso(final AttachCommand cmd) { final DiskTO disk = cmd.getDisk(); final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData(); final DataStoreTO store = isoTO.getDataStore(); + final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null; try { String dataStoreUrl = getDataStoreUrlFromStore(store); final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName()); - attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store); + attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store, deviceSeq); } catch (final LibvirtException e) { return new Answer(cmd, false, e.toString()); } catch (final InternalErrorException e) { @@ -1243,11 +1418,12 @@ public Answer dettachIso(final DettachCommand cmd) { final DiskTO disk = cmd.getDisk(); final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData(); final DataStoreTO store = isoTO.getDataStore(); + final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null; try { String dataStoreUrl = getDataStoreUrlFromStore(store); final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName()); - attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store); + attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store, deviceSeq); } catch (final LibvirtException e) { return new Answer(cmd, false, e.toString()); } catch (final InternalErrorException e) { @@ -1520,6 +1696,10 @@ protected synchronized void attachOrDetachDisk(final Connect conn, final boolean if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } + } else if (attachingPool.getType() == StoragePoolType.CLVM_NG) { + // CLVM_NG uses QCOW2 format on block devices + diskdef.defBlockBasedDisk(attachingDisk.getPath(), devId, busT); + diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.defFileBasedDisk(attachingDisk.getPath(), devId, busT, DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.RAW) { @@ -1735,13 +1915,22 @@ public Answer createVolume(final CreateObjectCommand cmd) { primaryPool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); disksize = volume.getSize(); PhysicalDiskFormat format; - if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType())) { + + MigrationOptions migrationOptions = volume.getMigrationOptions(); + boolean useDstPoolFormat = useDestPoolFormat(migrationOptions, primaryStore); + + if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType()) || useDstPoolFormat) { format = primaryPool.getDefaultFormat(); + if (useDstPoolFormat) { + logger.debug("Using destination pool default format {} for volume {} due to CLVM migration (src: {}, dst: {})", + format, volume.getUuid(), + migrationOptions != null ? migrationOptions.getSrcPoolType() : "unknown", + primaryStore.getPoolType()); + } } else { format = PhysicalDiskFormat.valueOf(volume.getFormat().toString().toUpperCase()); } - MigrationOptions migrationOptions = volume.getMigrationOptions(); if (migrationOptions != null) { int timeout = migrationOptions.getTimeout(); @@ -1766,7 +1955,11 @@ public Answer createVolume(final CreateObjectCommand cmd) { format = vol.getFormat(); } } - newVol.setSize(volume.getSize()); + if (StoragePoolType.CLVM_NG.equals(primaryStore.getPoolType()) && vol != null && vol.getVirtualSize() > 0) { + newVol.setSize(vol.getVirtualSize()); + } else { + newVol.setSize(volume.getSize()); + } newVol.setFormat(ImageFormat.valueOf(format.toString().toUpperCase())); return new CreateObjectAnswer(newVol); @@ -1778,6 +1971,29 @@ public Answer createVolume(final CreateObjectCommand cmd) { } } + /** + * For migration involving CLVM (RAW format), use destination pool's default format + * CLVM uses RAW format which may not match destination pool's format (e.g., NFS uses QCOW2) + * This specifically handles: + * - CLVM (RAW) -> NFS/Local/CLVM_NG (QCOW2) + * - NFS/Local/CLVM_NG (QCOW2) -> CLVM (RAW) + * @param migrationOptions + * @param primaryStore + * @return + */ + private boolean useDestPoolFormat(MigrationOptions migrationOptions, PrimaryDataStoreTO primaryStore) { + boolean useDstPoolFormat = false; + if (migrationOptions != null && migrationOptions.getSrcPoolType() != null) { + StoragePoolType srcPoolType = migrationOptions.getSrcPoolType(); + StoragePoolType dstPoolType = primaryStore.getPoolType(); + + if (srcPoolType != dstPoolType) { + useDstPoolFormat = (srcPoolType == StoragePoolType.CLVM || dstPoolType == StoragePoolType.CLVM); + } + } + return useDstPoolFormat; + } + /** * XML to take disk-only snapshot of the VM.

    * 1st parameter: snapshot's name;
    @@ -1867,10 +2083,22 @@ public Answer createSnapshot(final CreateObjectCommand cmd) { if (snapshotSize != null) { newSnapshot.setPhysicalSize(snapshotSize); } - } else if (primaryPool.getType() == StoragePoolType.CLVM) { - CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); - if (result != null) return result; - newSnapshot.setPath(snapshotPath); + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + if (primaryPool.getType() == StoragePoolType.CLVM_NG && snapshotTO.isKvmIncrementalSnapshot()) { + if (secondaryPool == null) { + String errorMsg = String.format("Incremental snapshots for CLVM_NG require secondary storage. " + + "Please configure secondary storage or disable incremental snapshots for volume [%s].", volume.getName()); + logger.error(errorMsg); + return new CreateObjectAnswer(errorMsg); + } + logger.info("Taking incremental snapshot of CLVM_NG volume [{}] using QCOW2 backup to secondary storage.", volume.getName()); + newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, + imageStoreTo.getUrl(), snapshotName, volume, conn, cmd.getWait()); + } else { + CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); + if (result != null) return result; + newSnapshot.setPath(snapshotPath); + } } else { if (snapshotTO.isKvmIncrementalSnapshot()) { newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, imageStoreTo != null ? imageStoreTo.getUrl() : null, snapshotName, volume, conn, cmd.getWait()); @@ -1943,7 +2171,11 @@ private String getVmXml(KVMStoragePool primaryPool, VolumeObjectTO volumeObjectT String machine = resource.isGuestAarch64() ? LibvirtComputingResource.VIRT : LibvirtComputingResource.PC; String cpuArch = resource.getGuestCpuArch() != null ? resource.getGuestCpuArch() : "x86_64"; - return String.format(DUMMY_VM_XML, vmName, cpuArch, machine, resource.getHypervisorPath(), primaryPool.getLocalPathFor(volumeObjectTo.getPath())); + String volumePath = primaryPool.getLocalPathFor(volumeObjectTo.getPath()); + boolean isClvmNg = StoragePoolType.CLVM_NG == primaryPool.getType(); + + String xmlTemplate = isClvmNg ? DUMMY_VM_XML_BLOCK : DUMMY_VM_XML; + return String.format(xmlTemplate, vmName, cpuArch, machine, resource.getHypervisorPath(), volumePath); } private SnapshotObjectTO takeIncrementalVolumeSnapshotOfRunningVm(SnapshotObjectTO snapshotObjectTO, KVMStoragePool primaryPool, KVMStoragePool secondaryPool, @@ -2044,7 +2276,7 @@ private void waitForBackup(String vmName) throws CloudRuntimeException { try { Thread.sleep(10000); } catch (InterruptedException e) { - throw new CloudRuntimeException(e); + logger.trace("Thread that was tracking the progress for backup of VM [{}] was interrupted. Ignoring.", vmName); } } @@ -2089,12 +2321,30 @@ protected void rebaseSnapshot(SnapshotObjectTO snapshotObjectTO, KVMStoragePool logger.debug("Rebasing snapshot [{}] with parent [{}].", snapshotName, parentSnapshotPath); + long snapshotTimeoutInMillis = wait * 1000L; try { - QemuImg qemuImg = new QemuImg(wait); + QemuImg qemuImg = new QemuImg(snapshotTimeoutInMillis); qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); } catch (LibvirtException | QemuImgException e) { - logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e); - throw new CloudRuntimeException(e); + if (!StringUtils.contains(e.getMessage(), "Is another process using the image")) { + logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e); + throw new CloudRuntimeException(e); + } + retryRebase(snapshotName, snapshotTimeoutInMillis, e, snapshotFile, parentSnapshotFile); + } + } + + private void retryRebase(String snapshotName, long waitInMilliseconds, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) { + logger.warn("Libvirt still has not released the lock, will wait [{}] milliseconds and try again later.", incrementalSnapshotRetryRebaseWait); + try { + Thread.sleep(incrementalSnapshotRetryRebaseWait); + QemuImg qemuImg = new QemuImg(waitInMilliseconds); + qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); + } catch (LibvirtException | QemuImgException | InterruptedException ex) { + logger.error("Unable to rebase snapshot [{}].", snapshotName, ex); + CloudRuntimeException cre = new CloudRuntimeException(ex); + cre.addSuppressed(e); + throw cre; } } @@ -2245,7 +2495,7 @@ private SnapshotObjectTO takeFullVolumeSnapshotOfRunningVm(CreateObjectCommand c String convertResult = convertBaseFileToSnapshotFileInStorageDir(ObjectUtils.defaultIfNull(secondaryPool, primaryPool), disk, snapshotPath, directoryPath, volume, cmd.getWait()); - resource.mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, null, true, snapshotName, volume, conn); + resource.mergeDeltaIntoBaseFile(vm, diskLabel, diskPath, null, true, snapshotName, volume, conn); validateConvertResult(convertResult, snapshotPath); } catch (LibvirtException e) { @@ -2516,7 +2766,7 @@ private void convertTheBaseFileToSnapshot(KVMPhysicalDisk baseFile, String snaps QemuImgFile destFile = new QemuImgFile(snapshotPath); destFile.setFormat(PhysicalDiskFormat.QCOW2); - QemuImg q = new QemuImg(wait); + QemuImg q = new QemuImg(wait * 1000L); q.convert(srcFile, destFile, options, qemuObjects, qemuImageOpts, null, true); } @@ -2647,13 +2897,19 @@ public Answer deleteVolume(final DeleteCommand cmd) { final PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)vol.getDataStore(); try { final KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); - try { - pool.getPhysicalDisk(vol.getPath()); - } catch (final Exception e) { - logger.debug(String.format("can't find volume: %s, return true", vol)); - return new Answer(null); + if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG) { + try { + pool.getPhysicalDisk(vol.getPath()); + } catch (final Exception e) { + logger.debug(String.format("can't find volume: %s, return true", vol)); + return new Answer(null); + } } pool.deletePhysicalDisk(vol.getPath(), vol.getFormat()); + if (vol.getChainInfo() != null && poolTypesToDeleteChainInfo.contains(pool.getType()) && vol.getFormat() == ImageFormat.QCOW2 && cmd.isDeleteChain()) { + logger.debug("Deleting leftover backup delta at [{}].", vol.getChainInfo()); + pool.deletePhysicalDisk(vol.getChainInfo(), vol.getFormat()); + } return new Answer(null); } catch (final CloudRuntimeException e) { logger.debug("Failed to delete volume: ", e); @@ -2880,6 +3136,25 @@ public Answer deleteSnapshot(final DeleteCommand cmd) { if (snapshotTO.isKvmIncrementalSnapshot()) { deleteCheckpoint(snapshotTO); } + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // For CLVM/CLVM_NG, snapshots are typically already deleted from primary storage during backup + // via deleteSnapshotOnPrimary in the backupSnapshot finally block. + // This is called when the user explicitly deletes the snapshot via UI/API. + // We check if the snapshot still exists and clean it up if needed. + logger.info("Processing CLVM/CLVM_NG snapshot deletion (id={}, name={}, path={}) on primary storage", + snapshotTO.getId(), snapshotTO.getName(), snapshotTO.getPath()); + + String snapshotPath = snapshotTO.getPath(); + if (snapshotPath != null && !snapshotPath.isEmpty()) { + boolean wasDeleted = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), true); + if (wasDeleted) { + logger.info("Successfully cleaned up {} snapshot {} from primary storage", primaryPool.getType(), snapshotName); + } else { + logger.info("{} snapshot {} was already deleted from primary storage during backup, no cleanup needed", primaryPool.getType(), snapshotName); + } + } else { + logger.debug("{} snapshot path is null or empty, assuming already cleaned up", primaryPool.getType()); + } } else { logger.warn("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); throw new InternalErrorException("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); @@ -3155,7 +3430,8 @@ private Storage.ImageFormat getFormat(StoragePoolType poolType) { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(poolType)) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(poolType)) { return ImageFormat.RAW; } else { return ImageFormat.QCOW2; @@ -3204,6 +3480,20 @@ public Answer syncVolumePath(SyncVolumePathCommand cmd) { return new Answer(cmd, false, "Not currently applicable for KVMStorageProcessor"); } + @Override + public Answer deleteBackup(DeleteCommand cmd) { + BackupDeltaTO delta = (BackupDeltaTO)cmd.getData(); + logger.debug("Deleting backup delta [{}].", delta); + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)delta.getDataStore(); + KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); + try { + pool.deletePhysicalDisk(delta.getPath(), delta.getFormat()); + } catch (CloudRuntimeException e) { + return new Answer(cmd, e); + } + return new Answer(cmd); + } + /** * Determine if migration is using host-local source pool. If so, return this host's storage as the template source, * rather than remote host's diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index a03daeb197bf..4bfac31b68f9 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -97,8 +96,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { public static final int RBD_FEATURES = RBD_FEATURE_LAYERING + RBD_FEATURE_EXCLUSIVE_LOCK + RBD_FEATURE_OBJECT_MAP + RBD_FEATURE_FAST_DIFF + RBD_FEATURE_DEEP_FLATTEN; private int rbdOrder = 0; /* Order 0 means 4MB blocks (the default) */ - private static final Set poolTypesThatEnableCreateDiskFromTemplateBacking = new HashSet<>(Arrays.asList(StoragePoolType.NetworkFilesystem, - StoragePoolType.Filesystem)); + private static final Set QEMU_IMG_MANAGED_POOL_TYPES = Set.of(StoragePoolType.NetworkFilesystem, StoragePoolType.Filesystem, StoragePoolType.SharedMountPoint); public LibvirtStorageAdaptor(StorageLayer storage) { _storageLayer = storage; @@ -134,8 +132,8 @@ public KVMPhysicalDisk createDiskFromTemplateBacking(KVMPhysicalDisk template, S String volumeDesc = String.format("volume [%s], with template backing [%s], in pool [%s] (%s), with size [%s] and encryption is %s", name, template.getName(), destPool.getUuid(), destPool.getType(), size, passphrase != null && passphrase.length > 0); - if (!poolTypesThatEnableCreateDiskFromTemplateBacking.contains(destPool.getType())) { - logger.info(String.format("Skipping creation of %s due to pool type is none of the following types %s.", volumeDesc, poolTypesThatEnableCreateDiskFromTemplateBacking.stream() + if (!QEMU_IMG_MANAGED_POOL_TYPES.contains(destPool.getType())) { + logger.info(String.format("Skipping creation of %s due to pool type is none of the following types %s.", volumeDesc, QEMU_IMG_MANAGED_POOL_TYPES.stream() .map(type -> type.toString()).collect(Collectors.joining(", ")))); return null; @@ -231,6 +229,11 @@ private void createTemplateOnRBDFromDirectDownloadFile(String srcTemplateFilePat } public StorageVol getVolume(StoragePool pool, String volName) { + if (pool == null) { + logger.debug("LibVirt StoragePool is null (likely CLVM/CLVM_NG virtual pool), cannot lookup volume {} via libvirt", volName); + return null; + } + StorageVol vol = null; try { @@ -254,9 +257,12 @@ public StorageVol getVolume(StoragePool pool, String volName) { try { vol = pool.storageVolLookupByName(volName); - logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + if (vol != null) { + logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + } } catch (LibvirtException e) { - throw new CloudRuntimeException("Could not find volume " + volName + ": " + e.getMessage()); + logger.debug("Volume " + volName + " still not found after pool refresh: " + e.getMessage()); + return null; } } @@ -349,38 +355,6 @@ private StoragePool createSharedStoragePool(Connect conn, String uuid, String ho } } - private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { - - String volgroupPath = "/dev/" + path; - String volgroupName = path; - volgroupName = volgroupName.replaceFirst("/", ""); - - LibvirtStoragePoolDef spd = new LibvirtStoragePoolDef(PoolType.LOGICAL, volgroupName, uuid, host, volgroupPath, volgroupPath); - StoragePool sp = null; - try { - logger.debug(spd.toString()); - sp = conn.storagePoolCreateXML(spd.toString(), 0); - return sp; - } catch (LibvirtException e) { - logger.error(e.toString()); - if (sp != null) { - try { - if (sp.isPersistent() == 1) { - sp.destroy(); - sp.undefine(); - } else { - sp.destroy(); - } - sp.free(); - } catch (LibvirtException l) { - logger.debug("Failed to define clvm storage pool with: " + l.toString()); - } - } - return null; - } - - } - private List getNFSMountOptsFromDetails(StoragePoolType type, Map details) { List nfsMountOpts = null; if (!type.equals(StoragePoolType.NetworkFilesystem) || details == null) { @@ -580,14 +554,11 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { Connect conn = LibvirtConnection.getConnection(); storage = conn.storagePoolLookupByUUIDString(uuid); - if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { - logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); - storage.create(0); - } LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); if (spd == null) { throw new CloudRuntimeException("Unable to parse the storage pool definition for storage pool " + uuid); } + StoragePoolType type = null; if (spd.getPoolType() == LibvirtStoragePoolDef.PoolType.NETFS) { type = StoragePoolType.NetworkFilesystem; @@ -603,6 +574,12 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { type = StoragePoolType.PowerFlex; } + // Activate pool if not running + if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { + logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); + storage.create(0); + } + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); if (pool.getType() != StoragePoolType.RBD && pool.getType() != StoragePoolType.PowerFlex) @@ -640,15 +617,17 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { logger.info("Asking libvirt to refresh storage pool " + uuid); pool.refresh(); } + pool.setCapacity(storage.getInfo().capacity); pool.setUsed(storage.getInfo().allocation); - updateLocalPoolIops(pool); pool.setAvailable(storage.getInfo().available); - logger.debug("Successfully refreshed pool " + uuid + - " Capacity: " + toHumanReadableSize(storage.getInfo().capacity) + - " Used: " + toHumanReadableSize(storage.getInfo().allocation) + - " Available: " + toHumanReadableSize(storage.getInfo().available)); + logger.debug("Successfully refreshed pool {} Capacity: {} Used: {} Available: {}", + uuid, toHumanReadableSize(storage.getInfo().capacity), + toHumanReadableSize(storage.getInfo().allocation), + toHumanReadableSize(storage.getInfo().available)); + + updateLocalPoolIops(pool); return pool; } catch (LibvirtException e) { @@ -663,6 +642,10 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { try { StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + throw new CloudRuntimeException("Volume " + volumeUuid + " not found in libvirt pool"); + } + KVMPhysicalDisk disk; LibvirtStorageVolumeDef voldef = getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol); disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); @@ -693,7 +676,7 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { } return disk; } catch (LibvirtException e) { - logger.debug("Failed to get physical disk:", e); + logger.debug("Failed to get volume from libvirt: " + e.getMessage()); throw new CloudRuntimeException(e.toString()); } } @@ -722,7 +705,7 @@ private int adjustStoragePoolRefCount(String uuid, int adjustment) { * Thread-safe increment storage pool usage refcount * @param uuid UUID of the storage pool to increment the count */ - private void incStoragePoolRefCount(String uuid) { + protected void incStoragePoolRefCount(String uuid) { adjustStoragePoolRefCount(uuid, 1); } /** @@ -730,7 +713,7 @@ private void incStoragePoolRefCount(String uuid) { * @param uuid UUID of the storage pool to decrement the count * @return true if the storage pool is still used, else false. */ - private boolean decStoragePoolRefCount(String uuid) { + protected boolean decStoragePoolRefCount(String uuid) { return adjustStoragePoolRefCount(uuid, -1) > 0; } @@ -814,7 +797,7 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri try { sp = createNetfsStoragePool(PoolType.NETFS, conn, name, host, path, nfsMountOpts); } catch (LibvirtException e) { - logger.error("Failed to create netfs mount: " + host + ":" + path , e); + logger.error("Failed to create netfs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -822,7 +805,7 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri try { sp = createNetfsStoragePool(PoolType.GLUSTERFS, conn, name, host, path, null); } catch (LibvirtException e) { - logger.error("Failed to create glusterfs mount: " + host + ":" + path , e); + logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -830,8 +813,6 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri sp = createSharedStoragePool(conn, name, host, path); } else if (type == StoragePoolType.RBD) { sp = createRBDStoragePool(conn, name, host, port, userInfo, path); - } else if (type == StoragePoolType.CLVM) { - sp = createCLVMStoragePool(conn, name, host, path); } } @@ -845,7 +826,6 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri // to be always mounted, as long the primary storage isn't fully deleted. incStoragePoolRefCount(name); } - if (sp.isActive() == 0) { logger.debug("Attempting to activate pool " + name); sp.create(0); @@ -979,7 +959,7 @@ public boolean deleteStoragePool(String uuid) { *
* *
  • - * {@link StoragePoolType#NetworkFilesystem} and {@link StoragePoolType#Filesystem} + * {@link StoragePoolType#NetworkFilesystem}, {@link StoragePoolType#Filesystem} and {@link StoragePoolType#SharedMountPoint} *
      *
    • * If the format is {@link PhysicalDiskFormat#QCOW2} or {@link PhysicalDiskFormat#RAW}, utilizes QemuImg to create the physical disk through the method @@ -1010,7 +990,7 @@ public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, return (dataPool == null) ? createPhysicalDiskByLibVirt(name, pool, PhysicalDiskFormat.RAW, provisioningType, size) : createPhysicalDiskByQemuImg(name, pool, PhysicalDiskFormat.RAW, provisioningType, size, passphrase); - } else if (StoragePoolType.NetworkFilesystem.equals(poolType) || StoragePoolType.Filesystem.equals(poolType)) { + } else if (QEMU_IMG_MANAGED_POOL_TYPES.contains(poolType)) { switch (format) { case QCOW2: case RAW: @@ -1080,7 +1060,7 @@ private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool destFile.setFormat(format); destFile.setSize(size); Map options = new HashMap(); - if (List.of(StoragePoolType.NetworkFilesystem, StoragePoolType.Filesystem).contains(pool.getType())) { + if (QEMU_IMG_MANAGED_POOL_TYPES.contains(pool.getType())) { options.put(QemuImg.PREALLOCATION, QemuImg.PreallocationType.getPreallocationType(provisioningType).toString()); } @@ -1116,6 +1096,7 @@ private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool @Override public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { // this is for managed storage that needs to prep disks prior to use + return true; } @@ -1227,7 +1208,11 @@ public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.Imag LibvirtStoragePool libvirtPool = (LibvirtStoragePool)pool; try { StorageVol vol = getVolume(libvirtPool.getPool(), uuid); - logger.debug("Instructing libvirt to remove volume " + uuid + " from pool " + pool.getUuid()); + if (vol == null) { + logger.warn("Volume {} not found in libvirt pool {}, it may have been already deleted", uuid, pool.getUuid()); + return true; + } + logger.debug("Instructing libvirt to remove volume {} from pool {}", uuid, pool.getUuid()); if(Storage.ImageFormat.DIR.equals(format)){ deleteDirVol(libvirtPool, vol); } else { @@ -1420,9 +1405,7 @@ private KVMPhysicalDisk createDiskFromTemplateOnRBD(KVMPhysicalDisk template, rbd.close(destImage); } else { logger.debug("The source image " + srcPool.getSourceDir() + "/" + template.getName() - + " is RBD format 2. We will perform a RBD clone using snapshot " - + rbdTemplateSnapName); - /* The source image is format 2, we can do a RBD snapshot+clone (layering) */ + + " is RBD format 2. We will perform a RBD snapshot+clone (layering)"); logger.debug("Checking if RBD snapshot " + srcPool.getSourceDir() + "/" + template.getName() @@ -1618,9 +1601,13 @@ to support snapshots(backuped) as qcow2 files. */ } else { destFile = new QemuImgFile(destPath, destFormat); try { - boolean isQCOW2 = PhysicalDiskFormat.QCOW2.equals(sourceFormat); - qemu.convert(srcFile, destFile, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null), - null, false, isQCOW2); + boolean keepBitmaps = PhysicalDiskFormat.QCOW2.equals(sourceFormat); + if (destPool.getType() == StoragePoolType.CLVM) { + keepBitmaps = false; + } + qemu.convert(srcFile, destFile, null, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null), + null, false, keepBitmaps, false, + false, null, null); Map destInfo = qemu.info(destFile); Long virtualSize = Long.parseLong(destInfo.get(QemuImg.VIRTUAL_SIZE)); newDisk.setVirtualSize(virtualSize); @@ -1684,8 +1671,8 @@ to support snapshots(backuped) as qcow2 files. */ } } else { /** - We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning - it doesn't benefit us. It's better to keep the current code in place which works + We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning + it doesn't benefit us. It's better to keep the current code in place which works */ srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool, sourcePath)); srcFile.setFormat(sourceFormat); @@ -1737,6 +1724,7 @@ private void deleteVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtEx vol.delete(0); } + private void deleteDirVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtException { Script.runSimpleBashScript("rm -r --interactive=never " + vol.getPath()); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java index ab39f7bc6ffd..a8c32baa6ef3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java @@ -31,6 +31,7 @@ import com.cloud.agent.api.to.HostTO; import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; +import com.cloud.ha.HighAvailabilityManager; import com.cloud.hypervisor.kvm.resource.KVMHABase.HAStoragePool; import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; @@ -212,7 +213,7 @@ public boolean refresh() { @Override public boolean isExternalSnapshot() { - if (this.type == StoragePoolType.CLVM || type == StoragePoolType.RBD) { + if (this.type == StoragePoolType.CLVM || this.type == StoragePoolType.CLVM_NG || type == StoragePoolType.RBD) { return true; } return false; @@ -277,6 +278,10 @@ public StoragePoolType getType() { return this.type; } + public void setType(StoragePoolType type) { + this.type = type; + } + public StoragePool getPool() { return this._pool; } @@ -320,29 +325,38 @@ public void setDetails(Map details) { @Override public boolean isPoolSupportHA() { - return type == StoragePoolType.NetworkFilesystem; + return HighAvailabilityManager.LIBVIRT_STORAGE_POOL_TYPES_WITH_HA_SUPPORT.contains(type); } public String getHearthBeatPath() { - if (type == StoragePoolType.NetworkFilesystem) { + if (StoragePoolType.NetworkFilesystem.equals(type)) { String kvmScriptsDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_SCRIPTS_DIR); - return Script.findScript(kvmScriptsDir, "kvmheartbeat.sh"); + String scriptPath = Script.findScript(kvmScriptsDir, "kvmheartbeat.sh"); + if (scriptPath == null) { + throw new CloudRuntimeException("Unable to find heartbeat script 'kvmheartbeat.sh' in directory: " + kvmScriptsDir); + } + return scriptPath; + } else if (StoragePoolType.SharedMountPoint.equals(type)) { + String kvmScriptsDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_SCRIPTS_DIR); + String scriptPath = Script.findScript(kvmScriptsDir, "kvmsmpheartbeat.sh"); + if (scriptPath == null) { + throw new CloudRuntimeException("Unable to find heartbeat script 'kvmsmpheartbeat.sh' in directory: " + kvmScriptsDir); + } + return scriptPath; } return null; } public String createHeartBeatCommand(HAStoragePool primaryStoragePool, String hostPrivateIp, boolean hostValidation) { - Script cmd = new Script(primaryStoragePool.getPool().getHearthBeatPath(), HeartBeatUpdateTimeout, logger); + Script cmd = new Script(primaryStoragePool.getPool().getHearthBeatPath(), HeartBeatUpdateTimeoutInMs, logger); cmd.add("-i", primaryStoragePool.getPoolIp()); cmd.add("-p", primaryStoragePool.getPoolMountSourcePath()); cmd.add("-m", primaryStoragePool.getMountDestPath()); if (hostValidation) { cmd.add("-h", hostPrivateIp); - } - - if (!hostValidation) { + } else { cmd.add("-c"); } @@ -360,53 +374,53 @@ public String getStorageNodeId() { } @Override - public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) { - boolean validResult = false; + public Boolean hasHeartBeat(HAStoragePool pool, HostTO host) { String hostIp = host.getPrivateNetwork().getIp(); - Script cmd = new Script(getHearthBeatPath(), HeartBeatCheckerTimeout, logger); + Script cmd = new Script(getHearthBeatPath(), HeartBeatCheckerTimeoutInMs, logger); cmd.add("-i", pool.getPoolIp()); cmd.add("-p", pool.getPoolMountSourcePath()); cmd.add("-m", pool.getMountDestPath()); cmd.add("-h", hostIp); cmd.add("-r"); - cmd.add("-t", String.valueOf(HeartBeatUpdateFreq / 1000)); + cmd.add("-t", String.valueOf(HeartBeatUpdateFreqInMs / 1000)); OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); String result = cmd.execute(parser); String parsedLine = parser.getLine(); - logger.debug(String.format("Checking heart beat with KVMHAChecker [{command=\"%s\", result: \"%s\", log: \"%s\", pool: \"%s\"}].", cmd.toString(), result, parsedLine, - pool.getPoolIp())); + logger.debug("Checking heart beat for host IP {} with KVMHAChecker [{command=\"{}\", result: \"{}\", log: \"{}\", pool: \"{}\"}].", hostIp, cmd.toString(), result, parsedLine, pool.getPoolIp()); if (result == null && parsedLine.contains("DEAD")) { - logger.warn(String.format("Checking heart beat with KVMHAChecker command [%s] returned [%s]. [%s]. It may cause a shutdown of host IP [%s].", cmd.toString(), - result, parsedLine, hostIp)); + logger.warn("Checking heart beat for host IP {} with KVMHAChecker command [{}] returned [{}]. It may cause a shutdown of the host.", hostIp, cmd.toString(), parsedLine); + return false; } else { - validResult = true; + logger.debug("Checking heart beat for host IP {} with KVMHAChecker command [{}] succeeded.", hostIp, cmd.toString()); + return true; } - return validResult; } @Override - public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { + public Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { + String hostIp = host.getPrivateNetwork().getIp(); Script cmd = new Script(vmActivityCheckPath, activityScriptTimeout.getStandardSeconds(), logger); cmd.add("-i", pool.getPoolIp()); cmd.add("-p", pool.getPoolMountSourcePath()); cmd.add("-m", pool.getMountDestPath()); - cmd.add("-h", host.getPrivateNetwork().getIp()); + cmd.add("-h", hostIp); cmd.add("-u", volumeUUIDListString); - cmd.add("-t", String.valueOf(String.valueOf(System.currentTimeMillis() / 1000))); + cmd.add("-t", String.valueOf(System.currentTimeMillis() / 1000)); cmd.add("-d", String.valueOf(duration)); OutputInterpreter.OneLineParser parser = new OutputInterpreter.OneLineParser(); String result = cmd.execute(parser); String parsedLine = parser.getLine(); - logger.debug(String.format("Checking heart beat with KVMHAVMActivityChecker [{command=\"%s\", result: \"%s\", log: \"%s\", pool: \"%s\"}].", cmd.toString(), result, parsedLine, pool.getPoolIp())); + logger.debug("Checking VM activity for host IP {} with KVMHAVMActivityChecker [{command=\"{}\", result: \"{}\", log: \"{}\", pool: \"{}\"}].", hostIp, cmd.toString(), result, parsedLine, pool.getPoolIp()); if (result == null && parsedLine.contains("DEAD")) { - logger.warn(String.format("Checking heart beat with KVMHAVMActivityChecker command [%s] returned [%s]. It is [%s]. It may cause a shutdown of host IP [%s].", cmd.toString(), result, parsedLine, host.getPrivateNetwork().getIp())); + logger.warn("Checking VM activity for host IP {} with KVMHAVMActivityChecker command [{}] returned [{}]. It may cause a shutdown of the host.", hostIp, cmd.toString(), parsedLine); return false; } else { + logger.debug("Checking VM activity for host IP {} with KVMHAVMActivityChecker command [{}] succeeded.", hostIp, cmd.toString()); return true; } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathSCSIPool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathSCSIPool.java index 229481b1f795..df9986c99194 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathSCSIPool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/MultipathSCSIPool.java @@ -225,13 +225,13 @@ public String getStorageNodeId() { } @Override - public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) { + public Boolean hasHeartBeat(HAStoragePool pool, HostTO host) { return null; } @Override - public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, - String volumeUUIDListString, String vmActivityCheckPath, long duration) { + public Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, + String volumeUUIDListString, String vmActivityCheckPath, long duration) { return null; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java index e336e0e5a54d..82bc35f009ee 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStorageAdaptor.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Predicate; import com.cloud.agent.api.PrepareStorageClientCommand; import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClient; @@ -644,18 +645,30 @@ public Ternary, String> prepareStorageClient(String // Assuming SDC service is started, add mdms String mdms = details.get(ScaleIOGatewayClient.STORAGE_POOL_MDMS); String[] mdmAddresses = mdms.split(","); - if (mdmAddresses.length > 0) { - if (ScaleIOUtil.isMdmPresent(mdmAddresses[0])) { - return new Ternary<>(true, getSDCDetails(details), "MDM added, no need to prepare the SDC client"); - } - - ScaleIOUtil.addMdms(mdmAddresses); - if (!ScaleIOUtil.isMdmPresent(mdmAddresses[0])) { - return new Ternary<>(false, null, "Failed to add MDMs"); + // remove MDMs already present in the config and added to the SDC + String[] mdmAddressesToAdd = Arrays.stream(mdmAddresses) + .filter(Predicate.not(ScaleIOUtil::isMdmPresent)) + .toArray(String[]::new); + // if all MDMs are already in the config and added to the SDC + if (mdmAddressesToAdd.length < 1 && mdmAddresses.length > 0) { + String msg = String.format("MDMs %s of the storage pool %s are already added", mdms, uuid); + logger.debug(msg); + return new Ternary<>(true, getSDCDetails(details), msg); + } else if (mdmAddressesToAdd.length > 0) { + ScaleIOUtil.addMdms(mdmAddressesToAdd); + String[] missingMdmAddresses = Arrays.stream(mdmAddressesToAdd) + .filter(Predicate.not(ScaleIOUtil::isMdmPresent)) + .toArray(String[]::new); + if (missingMdmAddresses.length > 0) { + String msg = String.format("Failed to add MDMs %s of the storage pool %s", String.join(", ", missingMdmAddresses), uuid); + logger.debug(msg); + return new Ternary<>(false, null, msg); } else { - logger.debug(String.format("MDMs %s added to storage pool %s", mdms, uuid)); + logger.debug("MDMs {} of the storage pool {} are added", mdmAddressesToAdd, uuid); applyMdmsChangeWaitTime(details); } + } else { + return new Ternary<>(false, getSDCDetails(details), "No MDM addresses provided"); } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java index e8243c3f7cfe..fc512ff94a94 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ScaleIOStoragePool.java @@ -236,12 +236,12 @@ public String getStorageNodeId() { } @Override - public Boolean checkingHeartBeat(HAStoragePool pool, HostTO host) { + public Boolean hasHeartBeat(HAStoragePool pool, HostTO host) { return null; } @Override - public Boolean vmActivityCheck(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { + public Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityScriptTimeout, String volumeUUIDListString, String vmActivityCheckPath, long duration) { return null; } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java index 76b5a413e70f..fb474a6bc7d3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java @@ -148,4 +148,8 @@ default Ternary, String> prepareStorageClient(Strin default Pair unprepareStorageClient(String uuid, Map details) { return new Pair<>(true, ""); } + + default void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + // no-op + } } diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAConfig.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAConfig.java index 59ea720328f5..3fbb5340fcce 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAConfig.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAConfig.java @@ -19,38 +19,37 @@ import org.apache.cloudstack.framework.config.ConfigKey; -public class KVMHAConfig { +public interface KVMHAConfig { - public static final ConfigKey KvmHAHealthCheckTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.health.check.timeout", "10", + ConfigKey KvmHAHealthCheckTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.health.check.timeout", "10", "The maximum length of time, in seconds, expected for an health check to complete.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHAActivityCheckTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.timeout", "60", + ConfigKey KvmHAActivityCheckTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.timeout", "60", "The maximum length of time, in seconds, expected for an activity check to complete.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHAActivityCheckInterval = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.interval", "60", + ConfigKey KvmHAActivityCheckInterval = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.interval", "60", "The interval, in seconds, between activity checks.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHAActivityCheckMaxAttempts = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.max.attempts", "10", + ConfigKey KvmHAActivityCheckMaxAttempts = new ConfigKey<>("Advanced", Long.class, "kvm.ha.activity.check.max.attempts", "10", "The maximum number of activity check attempts to perform before deciding to recover or degrade a resource.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHAActivityCheckFailureThreshold = new ConfigKey<>("Advanced", Double.class, "kvm.ha.activity.check.failure.ratio", "0.7", + ConfigKey KvmHAActivityCheckFailureThreshold = new ConfigKey<>("Advanced", Double.class, "kvm.ha.activity.check.failure.ratio", "0.7", "The activity check failure threshold ratio. This is used with the activity check maximum attempts for deciding to recover or degrade a resource. For most environments, please keep this value above 0.5.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHADegradedMaxPeriod = new ConfigKey<>("Advanced", Long.class, "kvm.ha.degraded.max.period", "300", + ConfigKey KvmHADegradedMaxPeriod = new ConfigKey<>("Advanced", Long.class, "kvm.ha.degraded.max.period", "300", "The maximum length of time, in seconds, a resource can be in degraded state where only health checks are performed.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHARecoverTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.timeout", "60", + ConfigKey KvmHARecoverTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.timeout", "60", "The maximum length of time, in seconds, expected for a recovery operation to complete.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHARecoverWaitPeriod = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.wait.period", "600", + ConfigKey KvmHARecoverWaitPeriod = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.wait.period", "600", "The maximum length of time, in seconds, to wait for a resource to recover.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHARecoverAttemptThreshold = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.failure.threshold", "1", + ConfigKey KvmHARecoverAttemptThreshold = new ConfigKey<>("Advanced", Long.class, "kvm.ha.recover.failure.threshold", "1", "The maximum recovery attempts to be made for a resource, after which the resource is fenced. The recovery counter resets when a health check passes for a resource.", true, ConfigKey.Scope.Cluster); - public static final ConfigKey KvmHAFenceTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.fence.timeout", "60", + ConfigKey KvmHAFenceTimeout = new ConfigKey<>("Advanced", Long.class, "kvm.ha.fence.timeout", "60", "The maximum length of time, in seconds, expected for a fence operation to complete.", true, ConfigKey.Scope.Cluster); - } diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAProvider.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAProvider.java index b937be5265b7..f0b5cfc337de 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAProvider.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHAProvider.java @@ -68,17 +68,18 @@ public boolean hasActivity(final Host r, final DateTime suspectTime) throws HACh @Override public boolean recover(Host r) throws HARecoveryException { + logger.debug("Recover the host {}", r); try { - if (outOfBandManagementService.isOutOfBandManagementEnabled(r)){ + if (outOfBandManagementService.isOutOfBandManagementEnabled(r)) { final OutOfBandManagementResponse resp = outOfBandManagementService.executePowerOperation(r, PowerOperation.RESET, null); return resp.getSuccess(); } else { logger.warn("OOBM recover operation failed for the host {}", r); return false; } - } catch (Exception e){ + } catch (Exception e) { logger.warn("OOBM service is not configured or enabled for this host {} error is {}", r, e.getMessage()); - throw new HARecoveryException(String.format(" OOBM service is not configured or enabled for this host %s", r), e); + throw new HARecoveryException(String.format("OOBM service is not configured or enabled for this host %s", r), e); } } diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHostActivityChecker.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHostActivityChecker.java index 31f87d7e0442..96fd42d4d544 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHostActivityChecker.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/kvm/ha/KVMHostActivityChecker.java @@ -19,6 +19,7 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckOnHostAnswer; import com.cloud.agent.api.CheckOnHostCommand; import com.cloud.agent.api.CheckVMActivityOnStoragePoolCommand; import com.cloud.dc.dao.ClusterDao; @@ -61,7 +62,7 @@ public class KVMHostActivityChecker extends AdapterBase implements ActivityCheck @Inject private AgentManager agentMgr; @Inject - private PrimaryDataStoreDao storagePool; + private PrimaryDataStoreDao storagePoolDao; @Inject private StorageManager storageManager; @Inject @@ -70,11 +71,11 @@ public class KVMHostActivityChecker extends AdapterBase implements ActivityCheck @Override public boolean isActive(Host r, DateTime suspectTime) throws HACheckerException { try { - return isVMActivityOnHost(r, suspectTime); + return hasVMActivityOnHost(r, suspectTime); } catch (HACheckerException e) { - //Re-throwing the exception to avoid poluting the 'HACheckerException' already thrown + //Re-throwing the exception to avoid polluting the 'HACheckerException' already thrown throw e; - } catch (Exception e){ + } catch (Exception e) { String message = String.format("Operation timed out, probably the %s is not reachable.", r.toString()); logger.warn(message, e); throw new HACheckerException(message, e); @@ -83,82 +84,116 @@ public boolean isActive(Host r, DateTime suspectTime) throws HACheckerException @Override public boolean isHealthy(Host r) { - return isAgentActive(r); + return isHostAgentUp(r); } - private boolean isAgentActive(Host agent) { - if (agent.getHypervisorType() != Hypervisor.HypervisorType.KVM && agent.getHypervisorType() != Hypervisor.HypervisorType.LXC) { - throw new IllegalStateException(String.format("Calling KVM investigator for non KVM Host of type [%s].", agent.getHypervisorType())); + private boolean isHostAgentUp(Host host) { + if (host.getHypervisorType() != Hypervisor.HypervisorType.KVM && host.getHypervisorType() != Hypervisor.HypervisorType.LXC) { + throw new IllegalStateException(String.format("Calling KVM investigator for non KVM Host of type [%s].", host.getHypervisorType())); + } + + Status hostStatus = getHostAgentStatus(host); + + logger.debug("{} has the status [{}].", host.toString(), hostStatus); + return hostStatus == Status.Up; + } + + public Status getHostAgentStatus(Host host) { + if (host.getHypervisorType() != Hypervisor.HypervisorType.KVM && host.getHypervisorType() != Hypervisor.HypervisorType.LXC) { + return null; + } + + Status hostStatusFromItself = checkHostStatusWithSameHost(host); + if (hostStatusFromItself == Status.Up) { + return Status.Up; } - Status hostStatus = Status.Unknown; - Status neighbourStatus = Status.Unknown; - final CheckOnHostCommand cmd = new CheckOnHostCommand(agent, HighAvailabilityManager.KvmHAFenceHostIfHeartbeatFailsOnStorage.value()); + + Status hostStatusFromNeighbour = checkHostStatusWithNeighbourHosts(host); + logger.debug("{} status reported from itself: {} and neighbor: {}", host.toString(), hostStatusFromItself, hostStatusFromNeighbour); + Status hostStatus = hostStatusFromItself; + if (hostStatusFromNeighbour == Status.Up && (hostStatusFromItself == Status.Disconnected || hostStatusFromItself == Status.Down)) { + hostStatus = Status.Disconnected; + } + if (hostStatusFromNeighbour == Status.Down && (hostStatusFromItself == Status.Disconnected || hostStatusFromItself == Status.Down)) { + hostStatus = Status.Down; + } + + logger.debug("HA: HOST is ineligible legacy state {} for host {}", hostStatus, host); + return hostStatus; + } + + private Status checkHostStatusWithSameHost(Host host) { + Status hostStatus; + boolean reportFailureIfOneStorageIsDown = HighAvailabilityManager.KvmHAFenceHostIfHeartbeatFailsOnStorage.value(); + final CheckOnHostCommand cmd = new CheckOnHostCommand(host, reportFailureIfOneStorageIsDown); try { - logger.debug(String.format("Checking %s status...", agent.toString())); - Answer answer = agentMgr.easySend(agent.getId(), cmd); + logger.debug("Checking {} status...", host.toString()); + Answer answer = agentMgr.easySend(host.getId(), cmd); if (answer != null) { - hostStatus = answer.getResult() ? Status.Down : Status.Up; - logger.debug(String.format("%s has the status [%s].", agent.toString(), hostStatus)); - - if ( hostStatus == Status.Up ){ - return true; + if (answer.getResult()) { + hostStatus = ((CheckOnHostAnswer)answer).isAlive() ? Status.Up : Status.Down; + } else { + logger.debug("{} is not active according to itself, details: {}.", host.toString(), answer.getDetails()); + hostStatus = Status.Down; } - } - else { - logger.debug(String.format("Setting %s to \"Disconnected\" status.", agent.toString())); + logger.debug("{} has the status [{}].", host.toString(), hostStatus); + } else { + logger.debug("Setting {} to \"Disconnected\" status.", host.toString()); hostStatus = Status.Disconnected; } } catch (Exception e) { - logger.warn(String.format("Failed to send command CheckOnHostCommand to %s.", agent.toString()), e); + logger.warn("Failed to send command CheckOnHostCommand to {}.", host.toString(), e); + hostStatus = Status.Disconnected; } - List neighbors = resourceManager.listHostsInClusterByStatus(agent.getClusterId(), Status.Up); + return hostStatus; + } + + private Status checkHostStatusWithNeighbourHosts(Host host) { + Status hostStatusFromNeighbour = Status.Unknown; + boolean reportFailureIfOneStorageIsDown = HighAvailabilityManager.KvmHAFenceHostIfHeartbeatFailsOnStorage.value(); + final CheckOnHostCommand cmd = new CheckOnHostCommand(host, reportFailureIfOneStorageIsDown); + List neighbors = resourceManager.listHostsInClusterByStatus(host.getClusterId(), Status.Up); for (HostVO neighbor : neighbors) { - if (neighbor.getId() == agent.getId() || (neighbor.getHypervisorType() != Hypervisor.HypervisorType.KVM && neighbor.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { + if (neighbor.getId() == host.getId() + || (neighbor.getHypervisorType() != Hypervisor.HypervisorType.KVM && neighbor.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { continue; } try { - logger.debug(String.format("Investigating %s via neighbouring %s.", agent.toString(), neighbor.toString())); - + logger.debug("Investigating {} via neighboring {}.", host.toString(), neighbor.toString()); Answer answer = agentMgr.easySend(neighbor.getId(), cmd); if (answer != null) { - neighbourStatus = answer.getResult() ? Status.Down : Status.Up; - - logger.debug(String.format("Neighbouring %s returned status [%s] for the investigated %s.", neighbor.toString(), neighbourStatus, agent.toString())); - - if (neighbourStatus == Status.Up) { - break; + if (answer.getResult()) { + hostStatusFromNeighbour = ((CheckOnHostAnswer)answer).isAlive() ? Status.Up : Status.Down; + logger.debug("Neighboring {} returned status [{}] for the investigated {}.", neighbor.toString(), hostStatusFromNeighbour, host.toString()); + if (hostStatusFromNeighbour == Status.Up) { + return hostStatusFromNeighbour; + } + } else { + logger.debug("{} is not active according to neighbor {}, details: {}.", host.toString(), neighbor.toString(), answer.getDetails()); } } else { - logger.debug(String.format("Neighbouring %s is Disconnected.", neighbor.toString())); + logger.debug("Neighboring {} is Disconnected.", neighbor.toString()); } } catch (Exception e) { - logger.warn(String.format("Failed to send command CheckOnHostCommand to %s.", neighbor.toString()), e); + logger.warn("Failed to send command CheckOnHostCommand to neighbor {}.", neighbor.toString(), e); } } - if (neighbourStatus == Status.Up && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { - hostStatus = Status.Disconnected; - } - if (neighbourStatus == Status.Down && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { - hostStatus = Status.Down; - } - logger.debug(String.format("%s has the status [%s].", agent.toString(), hostStatus)); - - return hostStatus == Status.Up; + return hostStatusFromNeighbour; } - private boolean isVMActivityOnHost(Host agent, DateTime suspectTime) throws HACheckerException { - if (agent.getHypervisorType() != Hypervisor.HypervisorType.KVM && agent.getHypervisorType() != Hypervisor.HypervisorType.LXC) { - throw new IllegalStateException(String.format("Calling KVM investigator for non KVM Host of type [%s].", agent.getHypervisorType())); + private boolean hasVMActivityOnHost(Host host, DateTime suspectTime) throws HACheckerException { + if (host.getHypervisorType() != Hypervisor.HypervisorType.KVM && host.getHypervisorType() != Hypervisor.HypervisorType.LXC) { + throw new IllegalStateException(String.format("Calling KVM investigator for non KVM Host of type [%s].", host.getHypervisorType())); } boolean activityStatus = true; - HashMap> poolVolMap = getVolumeUuidOnHost(agent); - for (StoragePool pool : poolVolMap.keySet()) { - activityStatus = verifyActivityOfStorageOnHost(poolVolMap, pool, agent, suspectTime, activityStatus); + HashMap> poolVolumeMap = getStoragePoolAndVolumeInfoOnHost(host); + for (StoragePool pool : poolVolumeMap.keySet()) { + activityStatus = verifyActivityOfStorageOnHost(poolVolumeMap, pool, host, suspectTime, activityStatus); if (!activityStatus) { - logger.warn("It seems that the storage pool [{}] does not have activity on {}.", pool, agent); + logger.warn("It seems that the storage pool [{}] does not have activity on {}.", pool, host); break; } } @@ -166,66 +201,64 @@ private boolean isVMActivityOnHost(Host agent, DateTime suspectTime) throws HACh return activityStatus; } - protected boolean verifyActivityOfStorageOnHost(HashMap> poolVolMap, StoragePool pool, Host agent, DateTime suspectTime, boolean activityStatus) throws HACheckerException, IllegalStateException { + protected boolean verifyActivityOfStorageOnHost(HashMap> poolVolMap, StoragePool pool, Host host, DateTime suspectTime, boolean activityStatus) throws HACheckerException, IllegalStateException { List volume_list = poolVolMap.get(pool); - final CheckVMActivityOnStoragePoolCommand cmd = new CheckVMActivityOnStoragePoolCommand(agent, pool, volume_list, suspectTime); + final CheckVMActivityOnStoragePoolCommand cmd = new CheckVMActivityOnStoragePoolCommand(host, pool, volume_list, suspectTime); - logger.debug("Checking VM activity for {} on storage pool [{}].", agent.toString(), pool); + logger.debug("Checking VM activity for {} on storage pool [{}].", host.toString(), pool); try { - Answer answer = storageManager.sendToPool(pool, getNeighbors(agent), cmd); - + Answer answer = storageManager.sendToPool(pool, getNeighbors(host), cmd); if (answer != null) { activityStatus = !answer.getResult(); - logger.debug("{} {} activity on storage pool [{}]", agent.toString(), activityStatus ? "has" : "does not have", pool); + logger.debug("{} {} activity on storage pool [{}]", host.toString(), activityStatus ? "has" : "does not have", pool); } else { - String message = String.format("Did not get a valid response for VM activity check for %s on storage pool [%s].", agent.toString(), pool); + String message = String.format("Did not get a valid response for VM activity check for %s on storage pool [%s].", host.toString(), pool); logger.debug(message); throw new IllegalStateException(message); } - } catch (StorageUnavailableException e){ - String message = String.format("Storage [%s] is unavailable to do the check, probably the %s is not reachable.", pool, agent); + } catch (StorageUnavailableException e) { + String message = String.format("Storage [%s] is unavailable to do the check, probably the %s is not reachable.", pool, host); logger.warn(message, e); throw new HACheckerException(message, e); } return activityStatus; } - private HashMap> getVolumeUuidOnHost(Host agent) { - List vm_list = vmInstanceDao.listByHostId(agent.getId()); - List volume_list = new ArrayList(); - for (VirtualMachine vm : vm_list) { + private HashMap> getStoragePoolAndVolumeInfoOnHost(Host host) { + List vmListOnHost = vmInstanceDao.listByHostId(host.getId()); + List volumeListOnHost = new ArrayList<>(); + for (VirtualMachine vm : vmListOnHost) { logger.debug("Retrieving volumes of VM [{}]...", vm); - List vm_volume_list = volumeDao.findByInstance(vm.getId()); - volume_list.addAll(vm_volume_list); + List volumeListOfVM = volumeDao.findByInstance(vm.getId()); + volumeListOnHost.addAll(volumeListOfVM); } - HashMap> poolVolMap = new HashMap>(); - for (Volume vol : volume_list) { - StoragePool sp = storagePool.findById(vol.getPoolId()); - logger.debug("Retrieving storage pool [{}] of volume [{}]...", sp, vol); - if (!poolVolMap.containsKey(sp)) { - List list = new ArrayList(); - list.add(vol); + HashMap> poolVolumeMap = new HashMap<>(); + for (Volume volume : volumeListOnHost) { + StoragePool pool = storagePoolDao.findById(volume.getPoolId()); + logger.debug("Retrieving storage pool [{}] of volume [{}]...", pool, volume); + if (!poolVolumeMap.containsKey(pool)) { + List volList = new ArrayList<>(); + volList.add(volume); - poolVolMap.put(sp, list); + poolVolumeMap.put(pool, volList); } else { - poolVolMap.get(sp).add(vol); + poolVolumeMap.get(pool).add(volume); } } - return poolVolMap; + return poolVolumeMap; } - public long[] getNeighbors(Host agent) { - List neighbors = new ArrayList(); - List cluster_hosts = resourceManager.listHostsInClusterByStatus(agent.getClusterId(), Status.Up); - logger.debug("Retrieving all \"Up\" hosts from cluster [{}]...", clusterDao.findById(agent.getClusterId())); - for (HostVO host : cluster_hosts) { - if (host.getId() == agent.getId() || (host.getHypervisorType() != Hypervisor.HypervisorType.KVM && host.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { + public long[] getNeighbors(Host host) { + List neighbors = new ArrayList<>(); + List clusterHosts = resourceManager.listHostsInClusterByStatus(host.getClusterId(), Status.Up); + logger.debug("Retrieving all \"Up\" hosts from cluster [{}]...", clusterDao.findById(host.getClusterId())); + for (HostVO clusterHost : clusterHosts) { + if (clusterHost.getId() == host.getId() || (clusterHost.getHypervisorType() != Hypervisor.HypervisorType.KVM && clusterHost.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { continue; } - neighbors.add(host.getId()); + neighbors.add(clusterHost.getId()); } return ArrayUtils.toPrimitive(neighbors.toArray(new Long[neighbors.size()])); } - } diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java index 1fec561dc890..cae6832999eb 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java @@ -54,14 +54,15 @@ public class QemuImg { public static final String TARGET_ZERO_FLAG = "--target-is-zero"; public static final String PREALLOCATION = "preallocation"; public static final long QEMU_2_10 = 2010000; - public static final long QEMU_5_10 = 5010000; + public static final long QEMU_5_1 = 5001000; + public static final long QEMU_5_2 = 5002000; public static final int MIN_BITMAP_VERSION = 3; /* The qemu-img binary. We expect this to be in $PATH */ public String _qemuImgPath = "qemu-img"; private String cloudQemuImgPath = "cloud-qemu-img"; - private int timeout; + private long timeout; private boolean skipZero = false; private boolean skipTargetVolumeCreation = false; private boolean noCache = false; @@ -129,7 +130,7 @@ public enum BitmapOperation { * @param skipZeroIfSupported Don't write zeroes to target device during convert, if supported by qemu-img * @param noCache Ensure we flush writes to target disk (useful for block device targets) */ - public QemuImg(final int timeout, final boolean skipZeroIfSupported, final boolean noCache) throws LibvirtException { + public QemuImg(final long timeout, final boolean skipZeroIfSupported, final boolean noCache) throws LibvirtException { if (skipZeroIfSupported) { final Script s = new Script(_qemuImgPath, timeout); s.add("--help"); @@ -159,7 +160,7 @@ public QemuImg(final int timeout, final boolean skipZeroIfSupported, final boole * @param timeout * The timeout of scripts executed by this QemuImg object. */ - public QemuImg(final int timeout) throws LibvirtException, QemuImgException { + public QemuImg(final long timeout) throws LibvirtException, QemuImgException { this(timeout, false, false); } @@ -186,6 +187,12 @@ public QemuImg(final String qemuImgPath) throws LibvirtException { _qemuImgPath = qemuImgPath; } + /** + * Created for testing purposes + * */ + protected QemuImg() { + } + /* These are all methods supported by the qemu-img tool. */ /** @@ -392,7 +399,7 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, */ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, final Map options, final List qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat) throws QemuImgException { - convert(srcFile, destFile, options, qemuObjects, srcImageOpts, snapshotName, forceSourceFormat, false); + convert(srcFile, destFile, null, options, qemuObjects, srcImageOpts, snapshotName, forceSourceFormat, false, false, false, null, null); } protected Map getResizeOptionsFromConvertOptions(final Map options) { @@ -408,31 +415,41 @@ protected Map getResizeOptionsFromConvertOptions(final Map * This method is a facade for 'qemu-img convert' and converts a disk image or snapshot into a disk image with the specified filename and format. * * @param srcFile - * The source file. + * The source file. * @param destFile - * The destination file. + * The destination file. + * @param backingFile + * The destination's backing file. * @param options - * Options for the conversion. Takes a Map with key value - * pairs which are passed on to qemu-img without validation. + * Options for the conversion. Takes a Map with key value + * pairs which are passed on to qemu-img without validation. * @param qemuObjects - * Pass qemu Objects to create - see objects in the qemu main page. + * Pass qemu Objects to create - see objects in the qemu main page. * @param srcImageOpts - * pass qemu --image-opts to convert. + * pass qemu --image-opts to convert. * @param snapshotName - * If it is provided, conversion uses it as parameter. + * If it is provided, conversion uses it as parameter. * @param forceSourceFormat - * If true, specifies the source format in the conversion command. + * If true, specifies the source format in the conversion command. * @param keepBitmaps - * If true, copies the bitmaps to the destination image. + * If true, copies the bitmaps to the destination image. + * @param outOfOrderWrites + * If true, inform -W to convert + * @param compress + * If true, inform -c to convert + * @param coroutines + * If not null, inform -m and number of coroutines. By default, qemu uses 8 coroutines. + * @param rateLimit + * If not null, inform -r and rate limit in MB/s. By default, qemu does not limit the convert rate. * @return void */ - public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, - final Map options, final List qemuObjects, final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat, - boolean keepBitmaps) throws QemuImgException { + public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, QemuImgFile backingFile, final Map options, final List qemuObjects, + final QemuImageOptions srcImageOpts, final String snapshotName, final boolean forceSourceFormat, boolean keepBitmaps, boolean outOfOrderWrites, boolean compress, + Integer coroutines, Integer rateLimit) throws QemuImgException { Script script = new Script(_qemuImgPath, timeout); if (StringUtils.isNotBlank(snapshotName)) { String qemuPath = Script.runSimpleBashScript(getQemuImgPathScript); @@ -455,9 +472,28 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, script.add("-O"); script.add(destFile.getFormat().toString()); + addBackingFileToConvertCommand(script, backingFile); addScriptOptionsFromMap(options, script); addSnapshotToConvertCommand(srcFile.getFormat().toString(), snapshotName, forceSourceFormat, script, version); + if (outOfOrderWrites) { + script.add("-W"); + } + + if (rateLimit != null) { + script.add("-r"); + script.add(rateLimit + "M"); + } + + if (coroutines != null) { + script.add("-m"); + script.add(String.valueOf(coroutines)); + } + + if (compress) { + script.add("-c"); + } + if (noCache) { script.add("-t"); script.add("none"); @@ -484,7 +520,7 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, script.add(srcFile.getFileName()); } - if (this.version >= QEMU_5_10 && keepBitmaps && Qcow2Inspector.validateQcow2Version(srcFile.getFileName(), MIN_BITMAP_VERSION)) { + if (this.version >= QEMU_5_1 && keepBitmaps && Qcow2Inspector.validateQcow2Version(srcFile.getFileName(), MIN_BITMAP_VERSION)) { script.add("--bitmaps"); } @@ -500,6 +536,23 @@ public void convert(final QemuImgFile srcFile, final QemuImgFile destFile, } } + + protected void addBackingFileToConvertCommand(Script script, QemuImgFile backingFile) { + if (backingFile == null) { + return; + } + + script.add("-o"); + + String opts; + if (backingFile.getFormat() == null) { + opts = String.format("backing_file=%s", backingFile.getFileName()); + } else { + opts = String.format("backing_file=%s,backing_fmt=%s", backingFile.getFileName(), backingFile.getFormat().toString()); + } + script.add(opts); + } + /** * Qemu version 2.0.0 added (via commit ef80654d0dc1edf2dd2a51feff8cc3e1102a6583) the * flag "-l" to inform the snapshot name or ID @@ -871,11 +924,8 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles throw new QemuImgException("File should not be null"); } - final Script s = new Script(_qemuImgPath, timeout); + final Script s = createScript(_qemuImgPath, timeout); s.add("commit"); - if (skipEmptyingFiles) { - s.add("-d"); - } if (file.getFormat() != null) { s.add("-f"); @@ -885,6 +935,8 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles if (base != null) { s.add("-b"); s.add(base.getFileName()); + } else if (skipEmptyingFiles) { + s.add("-d"); } s.add(file.getFileName()); @@ -894,6 +946,13 @@ public void commit(QemuImgFile file, QemuImgFile base, boolean skipEmptyingFiles } } + /** + * This was created to facilitate testing + * */ + protected Script createScript(String path, long timeout) { + return new Script(path, timeout); + } + /** * Does qemu-img support --target-is-zero * @return boolean @@ -927,7 +986,10 @@ public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) { } protected static boolean helpSupportsImageFormat(String text, QemuImg.PhysicalDiskFormat format) { - Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); + // QEMU >= 10.1.0 changed the qemu-img --help header from + // "Supported formats:" to "Supported image formats:", so the word + // "image" must be treated as optional here. + Pattern pattern = Pattern.compile("Supported\\s(image\\s)?formats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE); return pattern.matcher(text).find(); } @@ -1006,4 +1068,9 @@ private void removeBitmap(QemuImgFile srcFile, String bitmapName) throws QemuImg throw new QemuImgException(String.format("Exception while removing bitmap [%s] from file [%s]. Result is [%s].", srcFile.getFileName(), bitmapName, result)); } } + + public long getVersion() { + return this.version; + } + } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index cde87fd93842..639f1dc2894c 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -52,7 +52,6 @@ import java.util.Random; import java.util.UUID; import java.util.Vector; -import java.util.concurrent.Semaphore; import javax.naming.ConfigurationException; import javax.xml.parsers.DocumentBuilderFactory; @@ -61,6 +60,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import com.cloud.agent.api.CheckOnHostAnswer; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -2406,7 +2406,7 @@ public void testGetStorageStatsCommand() { final KVMStoragePool secondaryPool = Mockito.mock(KVMStoragePool.class); when(libvirtComputingResourceMock.getStoragePoolMgr()).thenReturn(storagePoolMgr); - when(storagePoolMgr.getStoragePool(command.getPooltype(), command.getStorageId(), true)).thenReturn(secondaryPool); + when(storagePoolMgr.getStoragePool(command.getPooltype(), command.getStorageId(), true, true)).thenReturn(secondaryPool); final LibvirtRequestWrapper wrapper = LibvirtRequestWrapper.getInstance(); assertNotNull(wrapper); @@ -2415,7 +2415,7 @@ public void testGetStorageStatsCommand() { assertTrue(answer.getResult()); verify(libvirtComputingResourceMock, times(1)).getStoragePoolMgr(); - verify(storagePoolMgr, times(1)).getStoragePool(command.getPooltype(), command.getStorageId(), true); + verify(storagePoolMgr, times(1)).getStoragePool(command.getPooltype(), command.getStorageId(), true, true); } @SuppressWarnings("unchecked") @@ -2728,8 +2728,11 @@ public void testCreateStoragePoolCommand() { @Test public void testModifyStoragePoolCommand() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); final KVMStoragePool kvmStoragePool = Mockito.mock(KVMStoragePool.class); @@ -2753,8 +2756,11 @@ public void testModifyStoragePoolCommand() { @Test public void testModifyStoragePoolCommandFailure() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); @@ -3134,6 +3140,8 @@ public void testCheckOnHostCommand() { final Answer answer = wrapper.execute(command, libvirtComputingResourceMock); assertTrue(answer.getResult()); + assertTrue(answer instanceof CheckOnHostAnswer); + assertFalse(((CheckOnHostAnswer)answer).isAlive()); verify(libvirtComputingResourceMock, times(1)).getMonitor(); } @@ -5642,35 +5650,45 @@ public void testAddExtraConfigComponentNotEmptyExtraConfig() { Mockito.verify(vmDef, times(1)).addComp(any()); } - public void validateGetCurrentMemAccordingToMemBallooningWithoutMemBalooning(){ + @Test + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryWithoutMemBallooning(){ VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); - Mockito.when(vmTo.getType()).thenReturn(Type.User); LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); libvirtComputingResource.noMemBalloon = true; - long maxMemory = 2048; + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; - long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, maxMemory); - Assert.assertEquals(maxMemory, currentMemory); - Mockito.verify(vmTo, Mockito.times(0)).getMinRam(); + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(requestedMemory, currentMemory); } @Test - public void validateGetCurrentMemAccordingToMemBallooningWithtMemBalooning(){ + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryWithMemoryBallooning(){ LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); libvirtComputingResource.noMemBalloon = false; - long maxMemory = 2048; - long minMemory = ByteScaleUtils.mebibytesToBytes(64); - VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); Mockito.when(vmTo.getType()).thenReturn(Type.User); - Mockito.when(vmTo.getMinRam()).thenReturn(minMemory); + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; - long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, maxMemory); - Assert.assertEquals(ByteScaleUtils.bytesToKibibytes(minMemory), currentMemory); - Mockito.verify(vmTo).getMinRam(); + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(minMemory, currentMemory); } + @Test + public void getCurrentMemAccordingToMemBallooningTestValidateCurrentMemoryForSystemVms() { + LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); + libvirtComputingResource.noMemBalloon = false; + + VirtualMachineTO vmTo = Mockito.mock(VirtualMachineTO.class); + Mockito.when(vmTo.getType()).thenReturn(Type.SecondaryStorageVm); + long requestedMemory = 1024 * 1024; + long minMemory = 512 * 1024; + + long currentMemory = libvirtComputingResource.getCurrentMemAccordingToMemBallooning(vmTo, requestedMemory, minMemory); + Assert.assertEquals(requestedMemory, currentMemory); + } @Test public void validateCreateGuestResourceDefWithVcpuMaxLimit(){ LibvirtComputingResource libvirtComputingResource = new LibvirtComputingResource(); @@ -6669,10 +6687,12 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> true); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); @@ -6684,7 +6704,7 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE | Domain.BlockCommitFlags.DELETE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName); @@ -6694,10 +6714,12 @@ public void mergeSnapshotIntoBaseFileTestActiveAndDeleteFlags() throws Exception @Test public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenAnswer(invocation -> false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); @@ -6709,7 +6731,7 @@ public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, true, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.ACTIVE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName); @@ -6719,10 +6741,12 @@ public void mergeSnapshotIntoBaseFileTestActiveFlag() throws Exception { @Test public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(true); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); @@ -6733,7 +6757,7 @@ public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, Domain.BlockCommitFlags.DELETE); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(true, "/" + snapshotName); @@ -6743,10 +6767,12 @@ public void mergeSnapshotIntoBaseFileTestDeleteFlag() throws Exception { @Test public void mergeSnapshotIntoBaseFileTestNoFlags() throws Exception { try (MockedStatic libvirtUtilitiesHelperMockedStatic = Mockito.mockStatic(LibvirtUtilitiesHelper.class); - MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { + MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class); + MockedStatic agentPropertiesFileHandlerMockedStatic = Mockito.mockStatic(AgentPropertiesFileHandler.class)) { + + agentPropertiesFileHandlerMockedStatic.when(() -> AgentPropertiesFileHandler.getPropertyValue(Mockito.any())).thenAnswer(invocation -> true); libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); @@ -6757,7 +6783,7 @@ public void mergeSnapshotIntoBaseFileTestNoFlags() throws Exception { String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); Mockito.verify(domainMock, Mockito.times(1)).blockCommit(diskLabel, baseFilePath, null, 0, 0); Mockito.verify(libvirtComputingResourceSpy, Mockito.times(1)).manuallyDeleteUnusedSnapshotFile(false, "/" + snapshotName); @@ -6770,20 +6796,20 @@ public void mergeSnapshotIntoBaseFileTestMergeFailsThrowException() throws Excep MockedStatic threadContextMockedStatic = Mockito.mockStatic(ThreadContext.class)) { libvirtComputingResourceSpy.qcow2DeltaMergeTimeout = 10; libvirtUtilitiesHelperMockedStatic.when(() -> LibvirtUtilitiesHelper.isLibvirtSupportingFlagDeleteOnCommandVirshBlockcommit(Mockito.any())).thenReturn(false); - Mockito.doReturn(new Semaphore(1)).when(libvirtComputingResourceSpy).getSemaphoreToWaitForMerge(); threadContextMockedStatic.when(() -> ThreadContext.get(Mockito.anyString())).thenReturn("logid"); + Mockito.doReturn(Boolean.TRUE).when(libvirtComputingResourceSpy).isLibvirtEventsEnabled(); Mockito.doNothing().when(domainMock).addBlockJobListener(Mockito.any()); Mockito.doReturn(null).when(domainMock).getBlockJobInfo(Mockito.anyString(), Mockito.anyInt()); Mockito.doNothing().when(domainMock).removeBlockJobListener(Mockito.any()); - Mockito.doReturn(blockCommitListenerMock).when(libvirtComputingResourceSpy).getBlockCommitListener(Mockito.any(), Mockito.any()); + Mockito.doReturn(blockCommitListenerMock).when(libvirtComputingResourceSpy).getBlockCommitListener(Mockito.any()); Mockito.doReturn("Failed").when(blockCommitListenerMock).getResult(); String diskLabel = "vda"; String baseFilePath = "/file"; String snapshotName = "snap"; - libvirtComputingResourceSpy.mergeSnapshotIntoBaseFileWithEventsAndConfigurableTimeout(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); + libvirtComputingResourceSpy.mergeDeltaIntoBaseFile(domainMock, diskLabel, baseFilePath, null, false, snapshotName, volumeObjectToMock, connMock); } } @@ -7203,4 +7229,414 @@ public void defineDiskForDefaultPoolTypeHandlesNullDetails() { libvirtComputingResourceSpy.defineDiskForDefaultPoolType(diskDef, volume, false, false, false, physicalDisk, DEV_ID, DISK_BUS_TYPE, DISK_BUS_TYPE_DATA, null); Mockito.verify(diskDef).defFileBasedDisk(PHYSICAL_DISK_PATH, DEV_ID, DISK_BUS_TYPE_DATA, DiskDef.DiskFmtType.QCOW2); } + + @Test + public void getInterfaceTestValidMacAddressReturnInterface() { + String macAddress = "a0:90:27:a9:9e:62"; + final String vmName = "Test"; + final InterfaceDef interfaceDef = Mockito.mock(InterfaceDef.class); + final List interfaces = new ArrayList<>(); + interfaces.add(interfaceDef); + + Mockito.doReturn(macAddress).when(interfaceDef).getMacAddress(); + Mockito.doReturn(interfaces).when(libvirtComputingResourceSpy).getInterfaces(Mockito.any(), Mockito.anyString()); + + InterfaceDef result = libvirtComputingResourceSpy.getInterface(connMock, vmName, macAddress); + + Assert.assertNotNull(result); + } + + @Test(expected = CloudRuntimeException.class) + public void getInterfaceTestInvalidMacAddressThrowCloudRuntimeException() { + String invalidMacAddress = "ea:57:5d:f1:64:05"; + String macAddress = "a0:90:27:a9:9e:62"; + final String vmName = "Test"; + final InterfaceDef interfaceDef = Mockito.mock(InterfaceDef.class); + final List interfaces = new ArrayList<>(); + interfaces.add(interfaceDef); + + Mockito.doReturn(macAddress).when(interfaceDef).getMacAddress(); + Mockito.doReturn(interfaces).when(libvirtComputingResourceSpy).getInterfaces(Mockito.any(), Mockito.anyString()); + + libvirtComputingResourceSpy.getInterface(connMock, vmName, invalidMacAddress); + } + + @Test + public void testExtractVolumeGroupFromPath_ValidPath() { + String devicePath = "/dev/vg1/volume-123"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_ComplexVGName() { + String devicePath = "/dev/cloudstack-vg-primary/volume-456"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("cloudstack-vg-primary", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MultiLevelPath() { + String devicePath = "/dev/vg-cluster-01/lv-data-001"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-cluster-01", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NullPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(null); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_EmptyPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(""); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NonDevPath() { + String devicePath = "/var/lib/libvirt/images/disk.qcow2"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_InvalidFormat() { + String devicePath = "/dev/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_OnlyVG() { + String devicePath = "/dev/vg1"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + // Implementation extracts parts[2] regardless of whether there's an LV name + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MapperPath() { + String devicePath = "/dev/mapper/vg1-volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("mapper", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithDashes() { + String devicePath = "/dev/vg-name-with-dashes/lv-name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-name-with-dashes", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithUnderscores() { + String devicePath = "/dev/vg_name_with_underscores/lv_name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg_name_with_underscores", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NullVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(null); + assertFalse(result); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_EmptyVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(""); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ValidPath() { + try { + String volumePath = "/dev/test-vg/test-lv"; + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_NullPath() { + LibvirtComputingResource.deactivateClvmVolume(null); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_NullPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(null); + assertTrue(true); // Passes if no exception + } + + @Test + public void testDeactivateClvmVolume_EmptyPath() { + LibvirtComputingResource.deactivateClvmVolume(""); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_EmptyPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(""); + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_InvalidPath() { + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.deactivateClvmVolume(invalidPath); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_InvalidPath() { + // Should handle invalid path gracefully without throwing + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.setClvmVolumeToSharedMode(invalidPath); + assertTrue(true); // Passes if no exception + } + + @Test + public void testExtractVolumeGroupFromPath_RealWorldPaths() { + assertEquals("acsvg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/acsvg/volume-123")); + assertEquals("cloudstack-primary", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/cloudstack-primary/vm-disk-1")); + assertEquals("ceph-vg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/ceph-vg/snapshot-456")); + assertEquals("vg01", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg01/data")); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NonExistentVG() { + String nonExistentVG = "non-existent-vg-" + System.currentTimeMillis(); + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(nonExistentVG); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ComplexPath() { + try { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.activateClvmVolumeExclusive(complexPath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ComplexPath() { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.deactivateClvmVolume(complexPath); + assertTrue(true); + } + + @Test + public void testExtractVolumeGroupFromPath_SpecialCharacters() { + assertEquals("vg.name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg.name/lv")); + assertEquals("vg_name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg_name/lv")); + assertEquals("vg-name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg-name/lv")); + assertEquals("vg123", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg123/lv456")); + } + + @Test + public void testExtractVolumeGroupFromPath_TrailingSlash() { + String devicePath = "/dev/vg1/volume-123/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_WhitespaceVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(" "); + assertFalse(result); + } + + @Test + public void testExtractVolumeGroupFromPath_DevMapperExcluded() { + String mapperPath1 = "/dev/mapper/vg1-lv1"; + String mapperPath2 = "/dev/mapper/cloudstack--vg-volume--1"; + + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath1)); + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath2)); + } + + @Test + public void testExtractVolumeGroupFromPath_EdgeCases() { + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev/")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("dev/vg/lv")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("//dev//vg//lv")); + } + + @Test + public void testClvmVolumeActivationSequence() { + // Test a typical sequence: deactivate -> activate exclusive -> deactivate -> shared + String volumePath = "/dev/test-vg/test-volume"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + try { + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + // Expected in test environment + } + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); // Test passes if sequence completes + } + + @Test + public void testExtractVolumeGroupFromPath_LongVGName() { + String longVGName = "a".repeat(100); + String devicePath = "/dev/" + longVGName + "/volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals(longVGName, vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_LongLVName() { + String longLVName = "volume-" + "b".repeat(100); + String devicePath = "/dev/vg1/" + longLVName; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_SpecialCharactersInName() { + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg.test.name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg_test_name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg-test-name")); + } + + @Test + public void testClvmMethodsWithMultiplePaths() { + String[] paths = { + "/dev/vg1/vol1", + "/dev/vg2/vol2", + "/dev/cloudstack-primary/vol3", + "/dev/test-vg/test-vol" + }; + + for (String path : paths) { + LibvirtComputingResource.deactivateClvmVolume(path); + LibvirtComputingResource.setClvmVolumeToSharedMode(path); + + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(path); + assertNotNull("Should extract VG from: " + path, vgName); + + boolean clustered = LibvirtComputingResource.checkIfVolumeGroupIsClustered(vgName); + } + + assertTrue(true); // Passes if all paths processed + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenLibvirtVersionIsLessThanTheMinimum() throws LibvirtException { + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 8999; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, null, false); + Mockito.verify(domainMock, Mockito.never()).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenThereIsNoCapCapChangeAndNoCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(false); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, false); + Mockito.verify(domainMock, Mockito.never()).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertQuotaIsRemovedWhenThereIsCpuCapChangeAndNoCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(false); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, true); + Mockito.verify(domainMock, Mockito.times(1)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreUpdatedWhenThereIsNotCpuCapChangeAndCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(true); + double cpuQuotaPercentage = 0.03; + Mockito.when(vmTO.getCpuQuotaPercentage()).thenReturn(cpuQuotaPercentage); + Mockito.doReturn(new Pair<>(1000, 300L)).when(libvirtComputingResourceSpy).getPeriodAndQuota(cpuQuotaPercentage); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, false); + Mockito.verify(domainMock, Mockito.times(2)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreUpdatedWhenThereIsCpuCapChangeAndCpuLimitationIsApplied() throws LibvirtException { + Mockito.when(vmTO.isLimitCpuUse()).thenReturn(true); + double cpuQuotaPercentage = 0.03; + Mockito.when(vmTO.getCpuQuotaPercentage()).thenReturn(cpuQuotaPercentage); + Mockito.doReturn(new Pair<>(1000, 300L)).when(libvirtComputingResourceSpy).getPeriodAndQuota(cpuQuotaPercentage); + Mockito.when(domainMock.getName()).thenReturn("i-2-10-VM"); + libvirtComputingResourceSpy.hypervisorLibvirtVersion = 9000; + libvirtComputingResourceSpy.updateCpuQuotaAndPeriod(domainMock, vmTO, true); + Mockito.verify(domainMock, Mockito.times(2)).setSchedulerParameters(Mockito.any()); + } + + @Test + public void getPeriodAndQuotaTestAssertQuotaIsEqualToPeriodMultipliedByQuotaPercentage() { + double cpuQuotaPercentage = 0.3; + int expectedPeriod = CpuTuneDef.DEFAULT_PERIOD; + long expectedQuota = (long) (expectedPeriod * cpuQuotaPercentage); + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } + + @Test + public void getPeriodAndQuotaTestQuotaIsEqualToMinimumWhenRequired() { + double cpuQuotaPercentage = 0.03; + long expectedQuota = CpuTuneDef.MIN_QUOTA; + int expectedPeriod = (int) ((double) expectedQuota / cpuQuotaPercentage); + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } + + @Test + public void getPeriodAndQuotaTestPeriodIsEqualToMaximumWhenRequired() { + double cpuQuotaPercentage = 0.0003; + long expectedQuota = CpuTuneDef.MIN_QUOTA; + int expectedPeriod = CpuTuneDef.MAX_PERIOD; + Pair expectedResult = new Pair<>(expectedPeriod, expectedQuota); + Pair result = libvirtComputingResourceSpy.getPeriodAndQuota(cpuQuotaPercentage); + Assert.assertEquals(expectedResult, result); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDefTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDefTest.java index 0060e1d7ed4d..e6f63e852f85 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDefTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtGpuDefTest.java @@ -115,6 +115,145 @@ public void testGpuDef_withComplexPciAddress() { assertTrue(gpuXml.contains("")); } + @Test + public void testGpuDef_withFullPciAddressDomainZero() { + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "passthrough", + "passthrough", + "0000:00:02.0", + "10de", + "NVIDIA Corporation", + "1b38", + "Tesla T4" + ); + gpuDef.defGpu(pciGpuInfo); + + String gpuXml = gpuDef.toString(); + + assertTrue(gpuXml.contains("
      ")); + } + + @Test + public void testGpuDef_withFullPciAddressNonZeroDomain() { + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "passthrough", + "passthrough", + "0001:65:00.0", + "10de", + "NVIDIA Corporation", + "1b38", + "Tesla T4" + ); + gpuDef.defGpu(pciGpuInfo); + + String gpuXml = gpuDef.toString(); + + assertTrue(gpuXml.contains("
      ")); + } + + @Test + public void testGpuDef_withNvidiaStyleEightDigitDomain() { + // nvidia-smi reports PCI addresses with an 8-digit domain (e.g. "00000001:af:00.1"). + // generatePciXml must normalize it to the canonical 4-digit form "0x0001". + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "passthrough", + "passthrough", + "00000001:af:00.1", + "10de", + "NVIDIA Corporation", + "1b38", + "Tesla T4" + ); + gpuDef.defGpu(pciGpuInfo); + + String gpuXml = gpuDef.toString(); + + assertTrue(gpuXml.contains("
      ")); + } + + @Test + public void testGpuDef_withFullPciAddressVfNonZeroDomain() { + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo vfGpuInfo = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "VF-Profile", + "VF-Profile", + "0002:81:00.3", + "10de", + "NVIDIA Corporation", + "1eb8", + "Tesla T4" + ); + gpuDef.defGpu(vfGpuInfo); + + String gpuXml = gpuDef.toString(); + + // Non-passthrough NVIDIA VFs should be unmanaged + assertTrue(gpuXml.contains("")); + assertTrue(gpuXml.contains("
      ")); + } + + @Test + public void testGpuDef_withLegacyShortBdfDefaultsDomainToZero() { + // Backward compatibility: short BDF with no domain segment must still + // produce a valid libvirt address with domain 0x0000. + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo pciGpuInfo = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "passthrough", + "passthrough", + "af:00.0", + "10de", + "NVIDIA Corporation", + "1b38", + "Tesla T4" + ); + gpuDef.defGpu(pciGpuInfo); + + String gpuXml = gpuDef.toString(); + + assertTrue(gpuXml.contains("
      ")); + } + + @Test + public void testGpuDef_withInvalidBusAddressThrows() { + String[] invalidAddresses = { + "notahex:00.0", // non-hex bus + "gg:00:02.0", // non-hex domain + "00:02:03:04", // too many colon-separated parts + "00", // missing slot/function + "00:02", // missing function (no dot) + "00:02.0.1", // extra dot in ss.f + }; + for (String addr : invalidAddresses) { + LibvirtGpuDef gpuDef = new LibvirtGpuDef(); + VgpuTypesInfo info = new VgpuTypesInfo( + GpuDevice.DeviceType.PCI, + "passthrough", + "passthrough", + addr, + "10de", + "NVIDIA Corporation", + "1b38", + "Tesla T4" + ); + gpuDef.defGpu(info); + try { + String ignored = gpuDef.toString(); + fail("Expected IllegalArgumentException for address: " + addr + " but got: " + ignored); + } catch (IllegalArgumentException e) { + assertTrue("Exception message should contain the bad address", + e.getMessage().contains(addr)); + } + } + } + @Test public void testGpuDef_withNullDeviceType() { LibvirtGpuDef gpuDef = new LibvirtGpuDef(); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java new file mode 100644 index 000000000000..8354993e61a1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirTakeKbossBackupCommandWrapperTest.java @@ -0,0 +1,377 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +// +package com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.backup.TakeKbossBackupAnswer; +import org.apache.cloudstack.backup.TakeKbossBackupCommand; +import org.apache.cloudstack.storage.to.BackupDeltaTO; +import org.apache.cloudstack.storage.to.DeltaMergeTreeTO; +import org.apache.cloudstack.storage.to.KbossTO; +import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.LibvirtException; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.BackupException; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirTakeKbossBackupCommandWrapperTest { + + @Mock + private TakeKbossBackupCommand takeKbossBackupCommandMock; + + @Mock + private LibvirtComputingResource libvirtComputingResourceMock; + + @Mock + private KVMStoragePoolManager kvmStoragePoolManagerMock; + + @Mock + private KVMStoragePool kvmStoragePool1; + + @Mock + private KVMStoragePool kvmStoragePool2; + + @Mock + private KVMStoragePool kvmStoragePool3; + + @Mock + private KbossTO kbossTO1; + + @Mock + private KbossTO kbossTO2; + + @Mock + private VolumeObjectTO volumeObjectToMock1; + + @Mock + private VolumeObjectTO volumeObjectToMock2; + + @Mock + private DeltaMergeTreeTO deltaMergeTreeToMock; + + @Mock + private BackupDeltaTO backupDeltaTOMock; + + @Mock + private PrimaryDataStoreTO primaryDataStoreToMock; + + @Spy + @InjectMocks + private LibvirtTakeKbossBackupCommandWrapper libvirtTakeKbossBackupCommandWrapperSpy; + + private String volUuid1 = "uuid1"; + + private String volUuid2 = "uuid2"; + + private String deltaPath1 = "deltapath1"; + + private String deltaPath2 = "deltapath2"; + + private String secondaryUrl = "nfs://1.1.1.2:/mnt"; + + private String secondaryUrl2 = "nfs://2.2.2.2:/mnt2"; + + @Test + public void executeTestBackupException() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doThrow(new BackupException("tst", false)).when(libvirtComputingResourceMock).createDiskOnlyVMSnapshotOfStoppedVm(any(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + assertFalse(answer.getResult()); + assertFalse(answer.isVmConsistent()); + } + + @Test + public void executeTestSuccessStoppedVm() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + verify(libvirtComputingResourceMock).createDiskOnlyVMSnapshotOfStoppedVm(any(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + assertTrue(answer.getResult()); + } + + @Test + public void executeTestSuccessRunningVm() { + doReturn(List.of()).when(takeKbossBackupCommandMock).getKbossTOs(); + doReturn(true).when(takeKbossBackupCommandMock).isRunningVM(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + + TakeKbossBackupAnswer answer = (TakeKbossBackupAnswer)libvirtTakeKbossBackupCommandWrapperSpy.execute(takeKbossBackupCommandMock, libvirtComputingResourceMock); + + verify(libvirtComputingResourceMock).createDiskOnlyVmSnapshotForRunningVm(any(), any(), any(), anyBoolean()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).backupVolumes(any(), any(), any(), any(), any(), any(), anyBoolean(), any()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).cleanupVm(any(), any(), any(), any(), anyBoolean(), any()); + assertTrue(answer.getResult()); + } + + @Test (expected = BackupException.class) + public void backupVolumesTestRecoverIfExceptionThrown() { + List> volumeTosAndNewPaths = new ArrayList<>(); + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + String secondaryUrl = "nfs://1.1.1.2:/mnt"; + + doReturn(secondaryUrl).when(takeKbossBackupCommandMock).getImageStoreUrl(); + doThrow(new RuntimeException("odasij")).when(kbossTO1).getVolumeObjectTO(); + + libvirtTakeKbossBackupCommandWrapperSpy.backupVolumes(takeKbossBackupCommandMock, libvirtComputingResourceMock, kvmStoragePoolManagerMock, List.of(kbossTO1), + volumeTosAndNewPaths, "tst", false, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + + verify(libvirtTakeKbossBackupCommandWrapperSpy).recoverPreviousVmStateAndDeletePartialBackup(libvirtComputingResourceMock, volumeTosAndNewPaths, "tst", false, + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, kvmStoragePoolManagerMock, secondaryUrl); + } + + @Test + public void backupVolumesTestHappyPath() { + setupKbossTos(); + List> volumeTosAndNewPaths = new ArrayList<>(); + Map> mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize = new HashMap<>(); + + doReturn(100).when(takeKbossBackupCommandMock).getWait(); + doReturn(secondaryUrl).when(takeKbossBackupCommandMock).getImageStoreUrl(); + Pair pair1 = new Pair<>("p1", 10L); + doReturn(pair1).when(libvirtTakeKbossBackupCommandWrapperSpy).copyBackupDeltaToSecondary(eq(kvmStoragePoolManagerMock), eq(kbossTO1), anyList(), + eq(secondaryUrl), anyInt()); + Pair pair2 = new Pair<>("p2", 13L); + doReturn(pair2).when(libvirtTakeKbossBackupCommandWrapperSpy).copyBackupDeltaToSecondary(eq(kvmStoragePoolManagerMock), eq(kbossTO2), anyList(), + eq(secondaryUrl), anyInt()); + + libvirtTakeKbossBackupCommandWrapperSpy.backupVolumes(takeKbossBackupCommandMock, libvirtComputingResourceMock, kvmStoragePoolManagerMock, List.of(kbossTO1, kbossTO2), + volumeTosAndNewPaths, "tst", false, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize); + + verify(libvirtTakeKbossBackupCommandWrapperSpy, never()).recoverPreviousVmStateAndDeletePartialBackup(libvirtComputingResourceMock, volumeTosAndNewPaths, "tst", false, + mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize, kvmStoragePoolManagerMock, secondaryUrl); + assertEquals(pair1, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.get(volUuid1)); + assertEquals(pair2, mapVolumeUuidToDeltaPathOnSecondaryAndDeltaSize.get(volUuid2)); + } + + @Test + public void cleanupVmTestEndOfChain() { + setupKbossTos(); + Map mapVolumeUUidToNewVolumePath = new HashMap<>(); + String path1 = "path1"; + String path2 = "path2"; + String path3 = "path3"; + String vmName = "ttt"; + doReturn(path1).when(volumeObjectToMock1).getPath(); + doReturn(path2).when(volumeObjectToMock2).getPath(); + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnPrimary(); + doReturn(deltaPath2).when(kbossTO2).getDeltaPathOnPrimary(); + doReturn(deltaMergeTreeToMock).when(kbossTO1).getDeltaMergeTreeTO(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).mergeBackupDelta(any(), any(), any(), any(), anyBoolean(), any(), anyBoolean()); + doReturn(true).when(takeKbossBackupCommandMock).isEndChain(); + doReturn(volumeObjectToMock1).when(deltaMergeTreeToMock).getChild(); + doReturn(backupDeltaTOMock).when(deltaMergeTreeToMock).getParent(); + doReturn(path3).when(backupDeltaTOMock).getPath(); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).endChainForVolume(libvirtComputingResourceMock, volumeObjectToMock1, vmName, true, volUuid1, path3); + + libvirtTakeKbossBackupCommandWrapperSpy.cleanupVm(takeKbossBackupCommandMock, libvirtComputingResourceMock, List.of(kbossTO1, kbossTO2), vmName, true, + mapVolumeUUidToNewVolumePath); + + verify(volumeObjectToMock1).setPath(deltaPath1); + verify(volumeObjectToMock2).setPath(deltaPath2); + verify(libvirtTakeKbossBackupCommandWrapperSpy).mergeBackupDelta(eq(libvirtComputingResourceMock), eq(deltaMergeTreeToMock), eq(volumeObjectToMock1), eq(vmName), eq(true), + eq(volUuid1), anyBoolean()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).endChainForVolume(libvirtComputingResourceMock, volumeObjectToMock1, vmName, true, volUuid1, path3); + assertEquals(path3, mapVolumeUUidToNewVolumePath.get(volUuid1)); + assertEquals(path2, mapVolumeUUidToNewVolumePath.get(volUuid2)); + } + + @Test + public void cleanupVmTestNotEndOfChainAndNotIsolated() { + setupKbossTos(); + Map mapVolumeUUidToNewVolumePath = new HashMap<>(); + String vmName = "ttt"; + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnPrimary(); + doReturn(deltaPath2).when(kbossTO2).getDeltaPathOnPrimary(); + + libvirtTakeKbossBackupCommandWrapperSpy.cleanupVm(takeKbossBackupCommandMock, libvirtComputingResourceMock, List.of(kbossTO1, kbossTO2), vmName, true, + mapVolumeUUidToNewVolumePath); + + verify(volumeObjectToMock1).setPath(deltaPath1); + verify(volumeObjectToMock2).setPath(deltaPath2); + assertEquals(deltaPath1, mapVolumeUUidToNewVolumePath.get(volUuid1)); + assertEquals(deltaPath2, mapVolumeUUidToNewVolumePath.get(volUuid2)); + } + + @Test + public void copyBackupDeltaToSecondaryTest() throws LibvirtException, QemuImgException { + String parentPath = "parentPath"; + String volumePath = "volPath"; + String parentBackupFullPath = "parentBackupFullPath"; + String backupDeltaFullPathOnPrimary1 = "backupDeltaFullPathOnPrimary1"; + String backupDeltaFullPathOnSecondary1 = "backupDeltaFullPathOnSecondary1"; + String randomPath1 = "random"; + String backupDeltaFullPathOnPrimary2 = "backupDeltaFullPathOnPrimary2"; + + doReturn(volumeObjectToMock1).when(kbossTO1).getVolumeObjectTO(); + doReturn(volumePath).when(volumeObjectToMock1).getPath(); + doReturn(volUuid1).when(volumeObjectToMock1).getUuid(); + doReturn(parentPath).when(kbossTO1).getPathBackupParentOnSecondary(); + doReturn(new ArrayList<>(List.of(deltaPath2))).when(kbossTO1).getVmSnapshotDeltaPaths(); + doReturn(deltaPath1).when(kbossTO1).getDeltaPathOnSecondary(); + doReturn(kvmStoragePool1).when(kvmStoragePoolManagerMock).getStoragePoolByURI(secondaryUrl); + doReturn(kvmStoragePool2).when(kvmStoragePoolManagerMock).getStoragePoolByURI(secondaryUrl2); + doReturn(primaryDataStoreToMock).when(volumeObjectToMock1).getDataStore(); + doReturn(kvmStoragePool3).when(kvmStoragePoolManagerMock).getStoragePool(any(), any()); + + doReturn(parentBackupFullPath).when(kvmStoragePool2).getLocalPathFor(parentPath); + doReturn(backupDeltaFullPathOnPrimary1).when(kvmStoragePool3).getLocalPathFor(deltaPath2); + doReturn(backupDeltaFullPathOnSecondary1).when(kvmStoragePool1).getLocalPathFor(deltaPath1); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(backupDeltaFullPathOnPrimary1, backupDeltaFullPathOnSecondary1, parentBackupFullPath, volUuid1, 100000); + + doReturn(randomPath1).when(libvirtTakeKbossBackupCommandWrapperSpy).getRelativePathOnSecondaryForBackup(anyLong(), anyLong(), any()); + doReturn("random2").when(kvmStoragePool1).getLocalPathFor(randomPath1); + doReturn(backupDeltaFullPathOnPrimary2).when(kvmStoragePool3).getLocalPathFor(volumePath); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(eq(backupDeltaFullPathOnPrimary2), eq("random2"), eq(backupDeltaFullPathOnSecondary1), + any(), anyInt()); + + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(randomPath1, deltaPath1, kvmStoragePool1, + backupDeltaFullPathOnSecondary1, 100000); + doNothing().when(libvirtTakeKbossBackupCommandWrapperSpy).removeTemporaryDeltas(any(), anyBoolean()); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + filesMockedStatic.when(() -> Files.size(any())).thenReturn(1000L); + Pair result = libvirtTakeKbossBackupCommandWrapperSpy.copyBackupDeltaToSecondary(kvmStoragePoolManagerMock, kbossTO1, List.of(secondaryUrl2), + secondaryUrl, 100000); + + assertEquals(deltaPath1, result.first()); + assertEquals(Long.valueOf(1000L), result.second()); + } + + verify(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(backupDeltaFullPathOnPrimary1, backupDeltaFullPathOnSecondary1, parentBackupFullPath, volUuid1, 100000); + verify(libvirtTakeKbossBackupCommandWrapperSpy).convertDeltaToSecondary(eq(backupDeltaFullPathOnPrimary2), eq("random2"), eq(backupDeltaFullPathOnSecondary1), + any(), anyInt()); + verify(libvirtTakeKbossBackupCommandWrapperSpy).commitTopDeltaOnBaseBackupOnSecondaryIfNeeded(randomPath1, deltaPath1, kvmStoragePool1, + backupDeltaFullPathOnSecondary1, 100000); + verify(libvirtTakeKbossBackupCommandWrapperSpy).removeTemporaryDeltas(any(), anyBoolean()); + } + + @Test + public void removeTemporaryDeltasTestResultTrue() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, true); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(1)); + } + } + + @Test + public void removeTemporaryDeltasTestResultFalse() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, false); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(2)); + } + } + + @Test + public void removeTemporaryDeltasTestExceptionIsIgnored() { + ArrayList input = new ArrayList<>(List.of("a", "b")); + + try(MockedStatic filesMockedStatic = Mockito.mockStatic(Files.class)) { + filesMockedStatic.when(() -> Files.deleteIfExists(any())).thenThrow(new IOException("das")); + libvirtTakeKbossBackupCommandWrapperSpy.removeTemporaryDeltas(input, false); + + filesMockedStatic.verify(() -> Files.deleteIfExists(any()), Mockito.times(2)); + } + } + + @Test (expected = BackupException.class) + public void mergeBackupDeltaTestThrowsException() throws LibvirtException, QemuImgException { + doThrow(new QemuImgException("a")).when(libvirtComputingResourceMock).mergeDeltaForRunningVm(any(), any(), any()); + + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", true, volUuid1, false); + } + + @Test + public void mergeBackupDeltaTestRunningVm() throws LibvirtException, QemuImgException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", true, volUuid1, false); + + verify(libvirtComputingResourceMock).mergeDeltaForRunningVm(deltaMergeTreeToMock, "ttt", volumeObjectToMock1); + } + + @Test + public void mergeBackupDeltaTestStoppedVm() throws LibvirtException, QemuImgException, IOException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", false, volUuid1, false); + + verify(libvirtComputingResourceMock).mergeDeltaForStoppedVm(deltaMergeTreeToMock); + } + + @Test + public void mergeBackupDeltaTestStoppedVmCountNewestDeltaAsGrandChild() throws LibvirtException, QemuImgException, IOException { + libvirtTakeKbossBackupCommandWrapperSpy.mergeBackupDelta(libvirtComputingResourceMock, deltaMergeTreeToMock, volumeObjectToMock1, "ttt", false, volUuid1, true); + + verify(deltaMergeTreeToMock).addGrandChild(volumeObjectToMock1); + verify(libvirtComputingResourceMock).mergeDeltaForStoppedVm(deltaMergeTreeToMock); + } + + private void setupKbossTos() { + doReturn(volumeObjectToMock1).when(kbossTO1).getVolumeObjectTO(); + doReturn(volUuid1).when(volumeObjectToMock1).getUuid(); + doReturn(volumeObjectToMock2).when(kbossTO2).getVolumeObjectTO(); + doReturn(volUuid2).when(volumeObjectToMock2).getUuid(); + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java index 3cad9c27a687..74090723331a 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckConvertInstanceCommandWrapperTest.java @@ -52,6 +52,7 @@ public void setUp() { @Test public void testCheckInstanceCommand_success() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(false); Mockito.when(libvirtComputingResourceMock.hostSupportsInstanceConversion()).thenReturn(true); Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); assertTrue(answer.getResult()); @@ -59,9 +60,33 @@ public void testCheckInstanceCommand_success() { @Test public void testCheckInstanceCommand_failure() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(false); Mockito.when(libvirtComputingResourceMock.hostSupportsInstanceConversion()).thenReturn(false); Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); assertFalse(answer.getResult()); assertTrue(StringUtils.isNotBlank(answer.getDetails())); } + + @Test + public void testCheckInstanceCommand_vddkSuccess() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.getVddkLibDir()).thenReturn("/opt/vmware-vddk/vmware-vix-disklib-distrib"); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddk("/opt/vmware-vddk/vmware-vix-disklib-distrib")).thenReturn(true); + + Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); + + assertTrue(answer.getResult()); + } + + @Test + public void testCheckInstanceCommand_vddkFailure() { + Mockito.when(checkConvertInstanceCommandMock.isUseVddk()).thenReturn(true); + Mockito.when(checkConvertInstanceCommandMock.getVddkLibDir()).thenReturn("/opt/vmware-vddk/vmware-vix-disklib-distrib"); + Mockito.when(libvirtComputingResourceMock.hostSupportsVddk("/opt/vmware-vddk/vmware-vix-disklib-distrib")).thenReturn(false); + + Answer answer = checkConvertInstanceCommandWrapper.execute(checkConvertInstanceCommandMock, libvirtComputingResourceMock); + + assertFalse(answer.getResult()); + assertTrue(StringUtils.isNotBlank(answer.getDetails())); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapperTest.java new file mode 100644 index 000000000000..a06a9c50bc1c --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVirtualMachineCommandWrapperTest.java @@ -0,0 +1,191 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainInfo; +import org.libvirt.DomainInfo.DomainState; +import org.libvirt.LibvirtException; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.CheckVirtualMachineAnswer; +import com.cloud.agent.api.CheckVirtualMachineCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.vm.VirtualMachine.PowerState; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtCheckVirtualMachineCommandWrapperTest { + + private static final String VM_NAME = "i-2-3-VM"; + + @Mock + private LibvirtComputingResource libvirtComputingResource; + @Mock + private LibvirtUtilitiesHelper libvirtUtilitiesHelper; + @Mock + private Connect conn; + @Mock + private Domain domain; + + private LibvirtCheckVirtualMachineCommandWrapper wrapper; + private CheckVirtualMachineCommand command; + + @Before + public void setUp() throws LibvirtException { + wrapper = new LibvirtCheckVirtualMachineCommandWrapper(); + command = new CheckVirtualMachineCommand(VM_NAME); + + when(libvirtComputingResource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper); + when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenReturn(conn); + } + + @Test + public void testExecuteVmPoweredOnReturnsStateAndVncPort() throws LibvirtException { + DomainInfo domainInfo = new DomainInfo(); + domainInfo.state = DomainState.VIR_DOMAIN_RUNNING; + + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOn); + when(libvirtComputingResource.getVncPort(conn, VM_NAME)).thenReturn(5900); + when(conn.domainLookupByName(VM_NAME)).thenReturn(domain); + when(domain.getInfo()).thenReturn(domainInfo); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + assertEquals(PowerState.PowerOn, answer.getState()); + assertEquals(Integer.valueOf(5900), answer.getVncPort()); + } + + @Test + public void testExecuteVmPausedReturnsPowerUnknown() throws LibvirtException { + DomainInfo domainInfo = new DomainInfo(); + domainInfo.state = DomainState.VIR_DOMAIN_PAUSED; + + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOn); + when(libvirtComputingResource.getVncPort(conn, VM_NAME)).thenReturn(5901); + when(conn.domainLookupByName(VM_NAME)).thenReturn(domain); + when(domain.getInfo()).thenReturn(domainInfo); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + assertEquals(PowerState.PowerUnknown, answer.getState()); + assertEquals(Integer.valueOf(5901), answer.getVncPort()); + } + + @Test + public void testExecuteVmPoweredOffReturnsStateWithNullVncPort() throws LibvirtException { + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOff); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + assertEquals(PowerState.PowerOff, answer.getState()); + assertNull(answer.getVncPort()); + } + + @Test + public void testExecuteVmStateUnknownReturnsStateWithNullVncPort() throws LibvirtException { + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerUnknown); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + assertEquals(PowerState.PowerUnknown, answer.getState()); + assertNull(answer.getVncPort()); + } + + @Test + public void testExecuteVmPoweredOnWithNullVncPort() throws LibvirtException { + DomainInfo domainInfo = new DomainInfo(); + domainInfo.state = DomainState.VIR_DOMAIN_RUNNING; + + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOn); + when(libvirtComputingResource.getVncPort(conn, VM_NAME)).thenReturn(null); + when(conn.domainLookupByName(VM_NAME)).thenReturn(domain); + when(domain.getInfo()).thenReturn(domainInfo); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + assertEquals(PowerState.PowerOn, answer.getState()); + assertNull(answer.getVncPort()); + } + + @Test + public void testExecuteLibvirtExceptionOnGetConnectionReturnsFailure() throws LibvirtException { + LibvirtException libvirtException = mock(LibvirtException.class); + when(libvirtException.getMessage()).thenReturn("Connection refused"); + when(libvirtUtilitiesHelper.getConnectionByVmName(VM_NAME)).thenThrow(libvirtException); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertEquals("Connection refused", answer.getDetails()); + } + + @Test + public void testExecuteLibvirtExceptionOnGetVncPortReturnsFailure() throws LibvirtException { + LibvirtException libvirtException = mock(LibvirtException.class); + when(libvirtException.getMessage()).thenReturn("VNC port error"); + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOn); + when(libvirtComputingResource.getVncPort(conn, VM_NAME)).thenThrow(libvirtException); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertEquals("VNC port error", answer.getDetails()); + } + + @Test + public void testExecuteLibvirtExceptionOnDomainLookupReturnsFailure() throws LibvirtException { + LibvirtException libvirtException = mock(LibvirtException.class); + when(libvirtException.getMessage()).thenReturn("Domain not found"); + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOn); + when(libvirtComputingResource.getVncPort(conn, VM_NAME)).thenReturn(5900); + when(conn.domainLookupByName(VM_NAME)).thenThrow(libvirtException); + + CheckVirtualMachineAnswer answer = (CheckVirtualMachineAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertEquals("Domain not found", answer.getDetails()); + } + + @Test + public void testExecuteCallsGetLibvirtUtilitiesHelper() throws LibvirtException { + when(libvirtComputingResource.getVmState(conn, VM_NAME)).thenReturn(PowerState.PowerOff); + + wrapper.execute(command, libvirtComputingResource); + + verify(libvirtComputingResource).getLibvirtUtilitiesHelper(); + verify(libvirtUtilitiesHelper).getConnectionByVmName(VM_NAME); + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java new file mode 100644 index 000000000000..5d11cf209a45 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java @@ -0,0 +1,462 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.utils.script.Script; + +/** + * Tests for LibvirtClvmLockTransferCommandWrapper + */ +@RunWith(MockitoJUnitRunner.class) +public class LibvirtClvmLockTransferCommandWrapperTest { + + @Mock + private LibvirtComputingResource libvirtComputingResource; + + private LibvirtClvmLockTransferCommandWrapper wrapper; + + private static final String TEST_LV_PATH = "/dev/vg1/volume-123"; + private static final String TEST_VOLUME_UUID = "test-volume-uuid-456"; + + @Before + public void setUp() { + wrapper = new LibvirtClvmLockTransferCommandWrapper(); + } + + @Test + public void testExecute_DeactivateSuccess() { + ClvmLockTransferCommand cmd = new ClvmLockTransferCommand( + ClvmLockTransferCommand.Operation.DEACTIVATE, + TEST_LV_PATH, + TEST_VOLUME_UUID + ); + + try (MockedConstruction + + diff --git a/ui/src/components/offering/DiskOfferingForm.vue b/ui/src/components/offering/DiskOfferingForm.vue new file mode 100644 index 000000000000..f3f39647fefa --- /dev/null +++ b/ui/src/components/offering/DiskOfferingForm.vue @@ -0,0 +1,507 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + + + + diff --git a/ui/src/components/view/ApiKeyPairsTab.vue b/ui/src/components/view/ApiKeyPairsTab.vue new file mode 100644 index 000000000000..87feda59d992 --- /dev/null +++ b/ui/src/components/view/ApiKeyPairsTab.vue @@ -0,0 +1,451 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + + + + + diff --git a/ui/src/components/view/DeployVMFromBackup.vue b/ui/src/components/view/DeployVMFromBackup.vue index cbf265b08e4e..ccd019eeb7fc 100644 --- a/ui/src/components/view/DeployVMFromBackup.vue +++ b/ui/src/components/view/DeployVMFromBackup.vue @@ -1454,7 +1454,7 @@ export default { this.initForm() this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {} this.showOverrideDiskOfferingOption = this.dataPreFill.overridediskoffering - + this.selectedArchitecture = this.dataPreFill.backupArch ? this.dataPreFill.backupArch : this.architectureTypes.opts[0].id if (this.dataPreFill.isIso) { this.tabKey = 'isoid' } else { @@ -1543,46 +1543,6 @@ export default { fillValue (field) { this.form[field] = this.dataPreFill[field] }, - fetchZoneByQuery () { - return new Promise(resolve => { - let zones = [] - let apiName = '' - const params = {} - if (this.templateId) { - apiName = 'listTemplates' - params.listall = true - params.templatefilter = this.isNormalAndDomainUser ? 'executable' : 'all' - params.id = this.templateId - } else if (this.isoId) { - apiName = 'listIsos' - params.listall = true - params.isofilter = this.isNormalAndDomainUser ? 'executable' : 'all' - params.id = this.isoId - } else if (this.networkId) { - params.listall = true - params.id = this.networkId - apiName = 'listNetworks' - } - if (!apiName) return resolve(zones) - - getAPI(apiName, params).then(json => { - let objectName - const responseName = [apiName.toLowerCase(), 'response'].join('') - for (const key in json[responseName]) { - if (key === 'count') { - continue - } - objectName = key - break - } - const data = json?.[responseName]?.[objectName] || [] - zones = data.map(item => item.zoneid) - return resolve(zones) - }).catch(() => { - return resolve(zones) - }) - }) - }, async fetchData () { this.fetchZones(null, null) _.each(this.params, (param, name) => { @@ -1721,6 +1681,7 @@ export default { if (template.details['vmware-to-kvm-mac-addresses']) { this.dataPreFill.macAddressArray = JSON.parse(template.details['vmware-to-kvm-mac-addresses']) } + this.selectedArchitecture = template?.arch || 'x86_64' } } else if (name === 'isoid') { this.templateConfigurations = [] @@ -2347,9 +2308,6 @@ export default { this.clusterId = null this.zone = _.find(this.options.zones, (option) => option.id === value) this.isZoneSelectedMultiArch = this.zone.ismultiarch - if (this.isZoneSelectedMultiArch) { - this.selectedArchitecture = this.architectureTypes.opts[0].id - } this.zoneSelected = true this.form.startvm = true this.selectedZone = this.zoneId diff --git a/ui/src/components/view/DetailSettings.vue b/ui/src/components/view/DetailSettings.vue index 987a8ac42136..fc3b4257311f 100644 --- a/ui/src/components/view/DetailSettings.vue +++ b/ui/src/components/view/DetailSettings.vue @@ -100,7 +100,7 @@ @@ -115,7 +115,7 @@ > @@ -213,11 +213,16 @@ export default { this.detailOptions = json.listdetailoptionsresponse.detailoptions.details }) this.disableSettings = (this.$route.meta.name === 'vm' && resource.state !== 'Stopped') - getAPI('listTemplates', { templatefilter: 'all', id: resource.templateid }).then(json => { - this.deployasistemplate = json.listtemplatesresponse.template[0].deployasis - }) + if (this.$route.meta.name === 'vm') { + getAPI('listTemplates', { templatefilter: 'all', id: resource.templateid }).then(json => { + this.deployasistemplate = json.listtemplatesresponse.template[0].deployasis + }) + } }, allowEditOfDetail (name) { + if (this.deployasistemplate) { + return this.resource.alloweddetails && this.resource.alloweddetails.split(',').map(item => item.trim()).includes(name) + } if (this.resource.readonlydetails) { if (this.resource.readonlydetails.split(',').map(item => item.trim()).includes(name)) { return false @@ -320,7 +325,11 @@ export default { return } if (!this.allowEditOfDetail(this.newKey)) { - this.error = this.$t('error.unable.to.proceed') + if (this.deployasistemplate) { + this.error = this.$t('error.unable.to.add.setting') + ' : ' + this.newKey + '. ' + this.$t('message.error.setting.deployasistemplate') + } else { + this.error = this.$t('error.unable.to.add.setting') + ' : ' + this.newKey + } return } this.error = false diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 4145eeb9be6d..3119e4d57545 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -97,9 +97,12 @@ {{ $t(dataResource[item].toLowerCase()) }} {{ dataResource[item] }} -
      +
      {{ $toLocaleDate(dataResource[item]) }}
      + + {{ dataResource[item] }} +
      {{ dataResource[item] }}
      {{ decodeUserData(dataResource.userdata)}}
      @@ -147,6 +150,21 @@
      {{ dataResource[item] }}
      + +
      + {{ $t('label.secretkey') }} + +
      +
      {{ dataResource[item].substring(0, 20) }}...
      +
      +
      {{ $t('label.' + String(item).toLowerCase()) }} @@ -168,7 +186,7 @@
      {{ dataResource[item] }}
      - +
      {{ $t('label.' + item.replace('date', '.date.and.time'))}}
      @@ -189,7 +207,7 @@
      {{ dataResource[item].rbd_default_data_pool }}
      - +
      {{ $t('label.configuration.details') }}
      @@ -207,6 +225,26 @@
      + +
      + {{ $t('label.provider') }} +
      +
      {{ dataResource[item] }}
      +
      +
      +
      + +
      + {{ $t('label.' + String(key).toLowerCase()) }} +
      +
      + {{ value }} +
      +
      +
      +
      @@ -223,7 +261,10 @@ import HostInfo from '@/views/infra/HostInfo' import VmwareData from './VmwareData' import ObjectListTable from '@/components/view/ObjectListTable' import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails' +import TooltipButton from '@/components/widgets/TooltipButton' import { genericCompare } from '@/utils/sort' +import CodeHighlight from 'vue-code-highlight/src/CodeHighlight.vue' +import 'vue-code-highlight/themes/prism-okaidia.css' export default { name: 'DetailsTab', @@ -232,7 +273,9 @@ export default { HostInfo, VmwareData, ObjectListTable, - ExternalConfigurationDetails + ExternalConfigurationDetails, + TooltipButton, + CodeHighlight }, props: { resource: { @@ -270,7 +313,7 @@ export default { }, computed: { customDisplayItems () { - var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters'] + var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters', 'secretkey', 'backupofferingdetails'] if (this.$route.meta.name === 'webhookdeliveries') { items.push('startdate') items.push('enddate') diff --git a/ui/src/components/view/DomainDeleteConfirm.vue b/ui/src/components/view/DomainDeleteConfirm.vue new file mode 100644 index 000000000000..1247a052669f --- /dev/null +++ b/ui/src/components/view/DomainDeleteConfirm.vue @@ -0,0 +1,155 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + + + + + diff --git a/ui/src/components/view/ImageDeployInstanceButton.vue b/ui/src/components/view/ImageDeployInstanceButton.vue index 2cdd5a0af460..a4d47d4464ae 100644 --- a/ui/src/components/view/ImageDeployInstanceButton.vue +++ b/ui/src/components/view/ImageDeployInstanceButton.vue @@ -83,7 +83,8 @@ export default { computed: { allowed () { return (this.$route.meta.name === 'template' || - (this.$route.meta.name === 'iso' && this.resource.bootable)) + (this.$route.meta.name === 'iso' && this.resource?.bootable)) && + !!this.resource?.isready } }, methods: { @@ -91,7 +92,7 @@ export default { this.fetchResourceData() }, fetchResourceData () { - if (!this.resource || !this.resource.id) { + if (!this.resource || !this.resource.id || !this.resource.isready) { return } const params = { diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 996e30ead3b5..45ba348e4edf 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -120,12 +120,18 @@ -
      +
      {{ $t('label.status') }}
      +
      +
      {{ $t('label.apikeyaccess') }}
      +
      + +
      +
      {{ $t('label.allocationstate') }}
      @@ -159,6 +165,42 @@
      +
      +
      + + + {{ $t('label.apikey') }} + + +
      + {{ resource.apikey.substring(0, 20) }}... +
      +

      +
      + + + {{ $t('label.secretkey') }} + + +
      + {{ resource.secretkey.substring(0, 20) }}... +
      +
      +
      {{ $t('label.ostypename') }}
      @@ -413,6 +455,30 @@
      +
      +
      {{ $t('label.kms.key') }}
      +
      + + + {{ resource.kmskey }} + + {{ resource.kmskey }} +
      +
      +
      +
      {{ $t('label.hsm.profile') }}
      +
      + + + {{ resource.hsmprofile }} + + {{ resource.hsmprofile }} +
      +
      {{ $t('label.network') }}
      @@ -482,7 +548,7 @@ {{ resource.project || resource.projectname || resource.projectid }} - {{ resource.projectname }} + {{ resource.projectname || resource.projectid }}
      @@ -609,7 +675,8 @@
      - {{ resource.templatedisplaytext || resource.templatename || resource.templateid }} + {{ resource.templatedisplaytext || resource.templatename || resource.templateid }} + {{ resource.templatedisplaytext || resource.templatename || resource.templateid }}
      @@ -617,7 +684,8 @@
      - {{ resource.isodisplaytext || resource.isoname || resource.isoid }} + {{ resource.isodisplaytext || resource.isoname || resource.isoid }} + {{ resource.isodisplaytext || resource.isoname || resource.isoid }}
      @@ -794,6 +862,14 @@ {{ resource.account }}
      +
      +
      {{ $t('label.user') }}
      +
      + + {{ resource.username }} + {{ resource.username }} +
      +
      {{ $t('label.role') }}
      @@ -811,6 +887,18 @@ {{ resource.domain || resource.domainid }}
      +
      +
      {{ $t('label.currency') }}
      +
      + {{ resource.currency }} +
      +
      +
      +
      {{ $t('label.quota.current.balance') }}
      +
      + {{ resource.balance }} +
      +
      {{ $t('label.payloadurl') }}
      @@ -881,54 +969,6 @@ :osCategoryId="osCategoryId" />
      - - - -