diff --git a/.asf.yaml b/.asf.yaml index cb957e57fd95..1b922d6e9d84 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -57,6 +57,7 @@ github: - jmsperu - GaOrtiga - bhouse-nexthop + - Dogface2k rulesets: - name: "Default Branch Protection" 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/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/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 index bd07aeefd811..bc9716b7b1fa 100644 --- a/.github/workflows/daily-issue-triage.lock.yml +++ b/.github/workflows/daily-issue-triage.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"919fb17c7928e5e96d9c0a2854670a42f9c5f6cfc2059b46009bb3c23640d0ca","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# 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"}]} # ___ _ _ # / _ \ | | (_) @@ -35,12 +35,12 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - 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@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 @@ -90,7 +90,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -106,7 +106,7 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + 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" @@ -135,7 +135,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -194,20 +194,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' - GH_AW_PROMPT_7c51e8f15cc7af75_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_7c51e8f15cc7af75_EOF' + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' Tools: add_comment(max:10), add_labels(max:10), missing_tool, missing_data, noop - GH_AW_PROMPT_7c51e8f15cc7af75_EOF + GH_AW_PROMPT_7885f18f67e7c010_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -236,12 +236,12 @@ jobs: {{/if}} - GH_AW_PROMPT_7c51e8f15cc7af75_EOF + GH_AW_PROMPT_7885f18f67e7c010_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' + cat << 'GH_AW_PROMPT_7885f18f67e7c010_EOF' {{#runtime-import .github/workflows/daily-issue-triage.md}} - GH_AW_PROMPT_7c51e8f15cc7af75_EOF + GH_AW_PROMPT_7885f18f67e7c010_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -319,7 +319,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pick_copilot_token runs-on: ubuntu-latest permissions: read-all concurrency: @@ -349,7 +351,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -371,7 +373,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -447,9 +449,9 @@ jobs: 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_09fd9551c3cd7278_EOF' - {"add_comment":{"max":10,"target":"*"},"add_labels":{"max":10,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_09fd9551c3cd7278_EOF + 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: | @@ -661,7 +663,7 @@ jobs: 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_37cac1d5ee0c175c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_f4f9bbb59e3fefdc_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -705,7 +707,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_37cac1d5ee0c175c_EOF + GH_AW_MCP_CONFIG_f4f9bbb59e3fefdc_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -752,8 +754,8 @@ jobs: 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + 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 @@ -815,7 +817,7 @@ jobs: 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: ${{ secrets.COPILOT_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 }} @@ -939,6 +941,7 @@ jobs: - activation - agent - detection + - pick_copilot_token - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -961,7 +964,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1030,7 +1033,8 @@ jobs: 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: "true" + 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" @@ -1046,7 +1050,8 @@ jobs: 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: "true" + 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" @@ -1083,7 +1088,7 @@ jobs: 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: "true" + 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" @@ -1112,7 +1117,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1141,7 +1146,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1242,7 +1247,7 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + 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 @@ -1299,6 +1304,79 @@ jobs: } } + 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 @@ -1318,7 +1396,7 @@ jobs: 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: ${{ needs.agent.outputs.model }} + 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" @@ -1336,7 +1414,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1381,7 +1459,7 @@ jobs: 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\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + 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: | diff --git a/.github/workflows/daily-issue-triage.md b/.github/workflows/daily-issue-triage.md index 719dca1f3c63..c3bf7b485335 100644 --- a/.github/workflows/daily-issue-triage.md +++ b/.github/workflows/daily-issue-triage.md @@ -14,7 +14,91 @@ 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 diff --git a/.github/workflows/daily-repo-status.md b/.github/workflows/daily-repo-status.md deleted file mode 100644 index 49b553940b8b..000000000000 --- a/.github/workflows/daily-repo-status.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -description: | - This workflow creates daily 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: - schedule: daily - workflow_dispatch: - -permissions: - contents: read - issues: read - pull-requests: read - -network: defaults - -engine: - id: copilot - model: claude-haiku-4.5 - -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: [] - create-issue: - title-prefix: "[repo-status] " - labels: [report, daily-status] - close-older-issues: true -source: githubnext/agentics/workflows/repo-status.md@main ---- - -# Repo Status - -Create an upbeat daily 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/.github/workflows/sonar-check.yml b/.github/workflows/sonar-check.yml index fbb3cb9f540d..7f2a1bdb2935 100644 --- a/.github/workflows/sonar-check.yml +++ b/.github/workflows/sonar-check.yml @@ -80,12 +80,14 @@ jobs: 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 |', @@ -106,10 +108,26 @@ jobs: `> [View full Actions run](${runUrl})`, ].filter(l => l !== undefined).join('\n'); - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: body, }); - console.log('Posted coverage grade comment'); + 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/daily-repo-status.lock.yml b/.github/workflows/weekly-repo-status.lock.yml similarity index 90% rename from .github/workflows/daily-repo-status.lock.yml rename to .github/workflows/weekly-repo-status.lock.yml index 0992d3b67de0..63198d8602dd 100644 --- a/.github/workflows/daily-repo-status.lock.yml +++ b/.github/workflows/weekly-repo-status.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"bcecce6f1d9f8df2b3eca9eb7bb1fdbac13c396c240a2dc802a96546f435b969","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot","agent_model":"claude-haiku-4.5"} +# 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"}]} # ___ _ _ # / _ \ | | (_) @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# This workflow creates daily repo status reports. It gathers recent repository +# 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. @@ -36,12 +36,12 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# - 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@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 +# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 @@ -51,11 +51,10 @@ # - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 -name: "Repo Status" +name: "Weekly Repo Status" on: schedule: - - cron: "11 19 * * *" - # Friendly format: daily (scattered) + - cron: "0 12 * * 0" workflow_dispatch: inputs: aw_context: @@ -69,7 +68,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Repo Status" +run-name: "Weekly Repo Status" jobs: activation: @@ -91,13 +90,13 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + 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: "Repo Status" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} + 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" @@ -107,11 +106,11 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: "claude-haiku-4.5" + 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: "Repo Status" + 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" @@ -136,7 +135,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false sparse-checkout: | @@ -161,7 +160,7 @@ jobs: id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_WORKFLOW_FILE: "daily-repo-status.lock.yml" + GH_AW_WORKFLOW_FILE: "weekly-repo-status.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | @@ -195,20 +194,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' - GH_AW_PROMPT_eeb322738661ed58_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_eeb322738661ed58_EOF' + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_eeb322738661ed58_EOF + GH_AW_PROMPT_4dd73a0378be2614_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -237,12 +236,12 @@ jobs: {{/if}} - GH_AW_PROMPT_eeb322738661ed58_EOF + GH_AW_PROMPT_4dd73a0378be2614_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' + cat << 'GH_AW_PROMPT_4dd73a0378be2614_EOF' - {{#runtime-import .github/workflows/daily-repo-status.md}} - GH_AW_PROMPT_eeb322738661ed58_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 @@ -319,7 +318,9 @@ jobs: retention-days: 1 agent: - needs: activation + needs: + - activation + - pick_copilot_token runs-on: ubuntu-latest permissions: contents: read @@ -333,7 +334,7 @@ jobs: 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: dailyrepostatus + 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' }} @@ -352,15 +353,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + 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: "Repo Status" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} + 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" @@ -374,7 +375,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory @@ -450,15 +451,15 @@ jobs: 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_51571b44da85874d_EOF' - {"create_issue":{"close_older_issues":true,"labels":["report","daily-status"],"max":1,"title_prefix":"[repo-status] "},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_51571b44da85874d_EOF + 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\" \"daily-status\"] will be automatically added." + "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": [] @@ -658,7 +659,7 @@ jobs: 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_4b3a7789a6eea081_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_3ad3d264c5e0c6f9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -702,7 +703,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_4b3a7789a6eea081_EOF + GH_AW_MCP_CONFIG_3ad3d264c5e0c6f9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -749,8 +750,8 @@ jobs: 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-haiku-4.5 + 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 @@ -812,7 +813,7 @@ jobs: 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: ${{ secrets.COPILOT_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 }} @@ -937,6 +938,7 @@ jobs: - activation - agent - detection + - pick_copilot_token - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || @@ -946,7 +948,7 @@ jobs: contents: read issues: write concurrency: - group: "gh-aw-conclusion-daily-repo-status" + group: "gh-aw-conclusion-weekly-repo-status" cancel-in-progress: false queue: max outputs: @@ -957,15 +959,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + 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: "Repo Status" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} + 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" @@ -990,7 +992,7 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "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" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -1008,7 +1010,7 @@ jobs: 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: "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" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -1026,8 +1028,9 @@ jobs: 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: "true" - GH_AW_WORKFLOW_NAME: "Repo Status" + 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: @@ -1042,8 +1045,9 @@ jobs: 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: "true" - GH_AW_WORKFLOW_NAME: "Repo Status" + 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: @@ -1059,12 +1063,12 @@ jobs: 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: "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" 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-repo-status" + 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 }} @@ -1079,7 +1083,7 @@ jobs: 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: "true" + 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" @@ -1108,15 +1112,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + 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: "Repo Status" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} + 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" @@ -1137,7 +1141,7 @@ jobs: 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # --- Threat Detection --- @@ -1185,8 +1189,8 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - WORKFLOW_NAME: "Repo Status" - WORKFLOW_DESCRIPTION: "This workflow creates daily 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." + 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: | @@ -1238,7 +1242,7 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: claude-haiku-4.5 + 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 @@ -1295,6 +1299,79 @@ jobs: } } + 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 @@ -1307,15 +1384,15 @@ jobs: issues: write timeout-minutes: 15 env: - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-repo-status" + 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: "claude-haiku-4.5" + GH_AW_ENGINE_MODEL: "gpt-5.6-luna" GH_AW_ENGINE_VERSION: "1.0.52" - GH_AW_WORKFLOW_ID: "daily-repo-status" - GH_AW_WORKFLOW_NAME: "Repo Status" + 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: @@ -1330,15 +1407,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 + 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: "Repo Status" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-repo-status.lock.yml@${{ github.ref }} + 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" @@ -1375,7 +1452,7 @@ jobs: 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\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + 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: | 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 91537e25267e..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 @@ -172,12 +172,12 @@ repos: name: run codespell description: Check spelling with codespell - 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 @@ -186,7 +186,7 @@ repos: types: [markdown] files: \.md$ - repo: https://github.com/adrienverge/yamllint - rev: v1.37.1 + rev: v1.38.0 hooks: - id: yamllint name: run yamllint diff --git a/README.md b/README.md index 852674ab7b1f..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/) @@ -210,7 +215,7 @@ The following provides more details on the included cryptographic software: ## 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/api/src/main/java/com/cloud/projects/ProjectService.java b/api/src/main/java/com/cloud/projects/ProjectService.java index d11e9ae0446d..17413de320c3 100644 --- a/api/src/main/java/com/cloud/projects/ProjectService.java +++ b/api/src/main/java/com/cloud/projects/ProjectService.java @@ -23,6 +23,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.projects.ProjectAccount.Role; import com.cloud.user.Account; +import com.cloud.user.User; public interface ProjectService { /** @@ -102,4 +103,5 @@ public interface ProjectService { boolean addUserToProject(Long projectId, String username, String email, Long projectRoleId, Role projectRole) throws ResourceAllocationException; + void moveProjectAssociationsToUser(User oldUser, User newUser) throws ResourceAllocationException; } 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 aab20f108f9e..36e1cccca5e9 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 @@ -18,6 +18,7 @@ import javax.inject.Inject; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -112,7 +113,7 @@ public ApiCommandResourceType getApiResourceType() { } @Override - public void execute() { + public void execute() throws ResourceAllocationException { 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!"); 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 ffc32b6c4d08..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,16 +63,6 @@ public interface BackupProvider { */ boolean removeVMFromBackupOffering(VirtualMachine vm); - /** - * Removes the specified backup schedule from a virtual machine. - * - * @param vm the virtual machine from which the schedule will be removed. - * @param backupSchedule the backup schedule to be removed. - * @return {@code true} if the operation was successful; {@code false} otherwise. - */ - default boolean removeVMBackupSchedule(VirtualMachine vm, BackupSchedule backupSchedule) { - return true; - } /** * Whether the provider will delete backups on removal of VM from the offering @@ -91,7 +81,7 @@ default boolean removeVMBackupSchedule(VirtualMachine vm, BackupSchedule backupS * @param isolated * @return the result and {code}Backup{code} {code}Object{code} */ - Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long backupScheduleId); + Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated); /** * Delete an existing backup diff --git a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java index efd28385a3e8..0543fb10af36 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java +++ b/api/src/main/java/org/apache/cloudstack/backup/InternalBackupProvider.java @@ -136,7 +136,7 @@ default Set getSecondaryStorageUrls(UserVm userVm) { default void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { } - default boolean finishBackupChains(VirtualMachine virtualMachine) { + default boolean finishBackupChain(VirtualMachine virtualMachine) { return false; } } diff --git a/api/src/main/java/org/apache/cloudstack/region/RegionService.java b/api/src/main/java/org/apache/cloudstack/region/RegionService.java index b947b61c8f02..47dbb34dd629 100644 --- a/api/src/main/java/org/apache/cloudstack/region/RegionService.java +++ b/api/src/main/java/org/apache/cloudstack/region/RegionService.java @@ -18,6 +18,7 @@ import java.util.List; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.command.admin.account.DeleteAccountCmd; import org.apache.cloudstack.api.command.admin.account.DisableAccountCmd; import org.apache.cloudstack.api.command.admin.account.EnableAccountCmd; @@ -116,7 +117,7 @@ public interface RegionService { * @param moveUserCmd * @return true if delete was successful, false otherwise */ - boolean moveUser(MoveUserCmd moveUserCmd); + boolean moveUser(MoveUserCmd moveUserCmd) throws ResourceAllocationException; /** * update an existing domain diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java index 042047b59358..34b65adc2467 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorAnswer.java @@ -18,26 +18,26 @@ */ package org.apache.cloudstack.backup; -import java.util.Map; +import java.util.List; -import org.apache.commons.collections4.MapUtils; +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; -import com.cloud.utils.Pair; public class CleanupKbossBackupErrorAnswer extends Answer { - private Map> volumeIdToPathAndChainEnded; + private List volumeObjectTos; private boolean vmRunning; - public CleanupKbossBackupErrorAnswer(Command cmd, Map> volumeIdToPathAndChainEnded, boolean vmRunning) { - super(cmd, MapUtils.isNotEmpty(volumeIdToPathAndChainEnded), null); - this.volumeIdToPathAndChainEnded = volumeIdToPathAndChainEnded; + public CleanupKbossBackupErrorAnswer(Command cmd, List volumeObjectTos, boolean vmRunning) { + super(cmd, CollectionUtils.isNotEmpty(volumeObjectTos), null); + this.volumeObjectTos = volumeObjectTos; this.vmRunning = vmRunning; } - public Map> getVolumeIdToPathAndChainEnded() { - return volumeIdToPathAndChainEnded; + public List getVolumeObjectTos() { + return volumeObjectTos; } public boolean isVmRunning() { diff --git a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java index e5cd5a7f8150..3257851d11ba 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/CleanupKbossBackupErrorCommand.java @@ -25,40 +25,19 @@ public class CleanupKbossBackupErrorCommand extends Command { private boolean runningVM; - private boolean errorOnCreate; - - private boolean endOfChain; - - private boolean isTopDelta; - private String vmName; private String imageStoreUrl; private List kbossTOS; - public CleanupKbossBackupErrorCommand(boolean runningVM, boolean errorOnCreate, boolean endOfChain, boolean isTopDelta, String vmName, String imageStoreUrl, List kbossTOS) { - this.errorOnCreate = errorOnCreate; + public CleanupKbossBackupErrorCommand(boolean runningVM, String vmName, String imageStoreUrl, List kbossTOS) { this.runningVM = runningVM; - this.endOfChain = endOfChain; - this.isTopDelta = isTopDelta; this.vmName = vmName; this.imageStoreUrl = imageStoreUrl; this.kbossTOS = kbossTOS; } - public boolean isErrorOnCreate() { - return errorOnCreate; - } - - public boolean isEndOfChain() { - return endOfChain; - } - - public boolean isTopDelta() { - return isTopDelta; - } - public boolean isRunningVM() { return runningVM; } 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 index 4a3f53a6dad4..0280ffa27a1f 100644 --- a/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java +++ b/core/src/main/java/org/apache/cloudstack/storage/to/KbossTO.java @@ -16,9 +16,10 @@ // specific language governing permissions and limitations // under the License. -import java.util.LinkedList; 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; @@ -29,22 +30,20 @@ public class KbossTO { private String deltaPathOnPrimary; private String parentDeltaPathOnPrimary; private String deltaPathOnSecondary; - private String oldVolumePath; private DeltaMergeTreeTO deltaMergeTreeTO; - private List deltaPaths; + private List vmSnapshotDeltaPaths; - public KbossTO(VolumeObjectTO volumeObjectTO, LinkedList deltaPaths) { + public KbossTO(VolumeObjectTO volumeObjectTO, List snapshotDataStoreVOs) { this.volumeObjectTO = volumeObjectTO; - this.deltaPaths = deltaPaths; + this.vmSnapshotDeltaPaths = snapshotDataStoreVOs.stream().map(SnapshotDataStoreVO::getInstallPath).collect(Collectors.toList()); } - public KbossTO(VolumeObjectTO volumeObjectTO, String deltaPathOnPrimary, String deltaPathOnSecondary, LinkedList deltaPaths) { + public KbossTO(VolumeObjectTO volumeObjectTO, String deltaPathOnPrimary, String deltaPathOnSecondary) { this.volumeObjectTO = volumeObjectTO; this.deltaPathOnPrimary = deltaPathOnPrimary; this.deltaPathOnSecondary = deltaPathOnSecondary; - this.deltaPaths = deltaPaths; } public String getPathBackupParentOnSecondary() { @@ -59,8 +58,8 @@ public DeltaMergeTreeTO getDeltaMergeTreeTO() { return deltaMergeTreeTO; } - public List getDeltaPaths() { - return deltaPaths; + public List getVmSnapshotDeltaPaths() { + return vmSnapshotDeltaPaths; } public String getDeltaPathOnPrimary() { @@ -95,14 +94,6 @@ public void setDeltaPathOnSecondary(String deltaPathOnSecondary) { this.deltaPathOnSecondary = deltaPathOnSecondary; } - public String getOldVolumePath() { - return oldVolumePath; - } - - public void setOldVolumePath(String oldVolumePath) { - this.oldVolumePath = oldVolumePath; - } - @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 5b1d4c573b68..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 @@ -81,7 +81,6 @@ public class VolumeObjectTO extends DownloadableObjectTO implements DataTO, Seri private String encryptFormat; private List checkpointPaths; private Set checkpointImageStoreUrls; - private Set deltasToRemove; public VolumeObjectTO() { @@ -426,12 +425,4 @@ public Set getCheckpointImageStoreUrls() { public void setCheckpointImageStoreUrls(Set checkpointImageStoreUrls) { this.checkpointImageStoreUrls = checkpointImageStoreUrls; } - - public Set getDeltasToRemove() { - return deltasToRemove; - } - - public void setDeltasToRemove(Set deltasToRemove) { - this.deltasToRemove = deltasToRemove; - } } 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 f4198819dd16..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 @@ -2025,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); diff --git a/engine/schema/src/main/java/com/cloud/projects/ProjectAccountVO.java b/engine/schema/src/main/java/com/cloud/projects/ProjectAccountVO.java index 4710a815f978..0e2f32d5f8b5 100644 --- a/engine/schema/src/main/java/com/cloud/projects/ProjectAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/projects/ProjectAccountVO.java @@ -110,6 +110,10 @@ public long getProjectAccountId() { return projectAccountId; } + public void setAccountId(long accountId) { + this.accountId = accountId; + } + public void setProjectRoleId(Long projectRoleId) { this.projectRoleId = projectRoleId; } diff --git a/engine/schema/src/main/java/com/cloud/projects/ProjectInvitationVO.java b/engine/schema/src/main/java/com/cloud/projects/ProjectInvitationVO.java index 887939311b24..09f19b046c84 100644 --- a/engine/schema/src/main/java/com/cloud/projects/ProjectInvitationVO.java +++ b/engine/schema/src/main/java/com/cloud/projects/ProjectInvitationVO.java @@ -102,6 +102,10 @@ public Long getForAccountId() { return forAccountId; } + public void setForAccountId(Long forAccountId) { + this.forAccountId = forAccountId; + } + @Override public String getToken() { return token; diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java index f4b2f6460020..cbc300aa56cf 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java @@ -20,6 +20,7 @@ import com.cloud.projects.ProjectAccount; import com.cloud.projects.ProjectAccountVO; +import com.cloud.user.User; import com.cloud.utils.db.GenericDao; public interface ProjectAccountDao extends GenericDao { @@ -47,9 +48,11 @@ public interface ProjectAccountDao extends GenericDao { void removeAccountFromProjects(long accountId); - void removeUserFromProjects(long userId); - boolean canUserModifyProject(long projectId, long accountId, long userId); List listUsersOrAccountsByRole(long id); + + List listBy(Long projectId, Long accountId, Long userId); + + void move(User oldUser, User newUser); } diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java index b6eb6d44cea8..4de64eef4ca5 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java @@ -18,6 +18,7 @@ import java.util.List; +import com.cloud.user.User; import org.springframework.stereotype.Component; import com.cloud.projects.ProjectAccount; @@ -192,17 +193,6 @@ public void removeAccountFromProjects(long accountId) { } } - @Override - public void removeUserFromProjects(long userId) { - SearchCriteria sc = AllFieldsSearch.create(); - sc.setParameters("userId", userId); - - int removedCount = remove(sc); - if (removedCount > 0) { - logger.debug(String.format("Removed user [%s] from %s project(s).", userId, removedCount)); - } - } - @Override public boolean canUserModifyProject(long projectId, long accountId, long userId) { SearchCriteria sc = AllFieldsSearch.create(); @@ -222,4 +212,23 @@ public List listUsersOrAccountsByRole(long id) { sc.setParameters("projectRoleId", id); return listBy(sc); } + + @Override + public List listBy(Long projectId, Long accountId, Long userId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParametersIfNotNull("projectId", projectId); + sc.setParametersIfNotNull("userId", userId); + sc.setParametersIfNotNull("accountId", accountId); + return listBy(sc); + } + + @Override + public void move(User oldUser, User newUser) { + List projectAccounts = listBy(null, oldUser.getAccountId(), oldUser.getId()); + for (ProjectAccountVO projectAccount : projectAccounts) { + projectAccount.setAccountId(newUser.getAccountId()); + projectAccount.setUserId(newUser.getId()); + update(projectAccount.getId(), projectAccount); + } + } } diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDao.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDao.java index 976d53998e2e..aba2a881e967 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDao.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDao.java @@ -20,6 +20,7 @@ import com.cloud.projects.ProjectInvitation.State; import com.cloud.projects.ProjectInvitationVO; +import com.cloud.user.User; import com.cloud.utils.db.GenericDao; public interface ProjectInvitationDao extends GenericDao { @@ -43,4 +44,9 @@ public interface ProjectInvitationDao extends GenericDao listInvitationsToExpire(long timeOut); + int removeBy(Long projectId, Long accountId, Long userId); + + List listBy(Long projectId, Long accountId, Long userId); + + void move(User oldUser, User newUser); } diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDaoImpl.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDaoImpl.java index d30b1c9f1f10..17e841967fb0 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectInvitationDaoImpl.java @@ -19,6 +19,7 @@ import java.sql.Date; import java.util.List; +import com.cloud.user.User; import org.springframework.stereotype.Component; import com.cloud.projects.ProjectInvitation.State; @@ -124,6 +125,40 @@ public List listInvitationsToExpire(long timeOut) { return listBy(sc); } + @Override + public int removeBy(Long projectId, Long accountId, Long userId) { + SearchCriteria sc = prepareAllFieldsSearchCriteria(projectId, accountId, userId); + return remove(sc); + } + + @Override + public List listBy(Long projectId, Long accountId, Long userId) { + SearchCriteria sc = prepareAllFieldsSearchCriteria(projectId, accountId, userId); + return listBy(sc); + } + + @Override + public void move(User oldUser, User newUser) { + List projectInvitations = listBy(null, oldUser.getAccountId(), oldUser.getId()); + for (ProjectInvitationVO projectInvitation : projectInvitations) { + projectInvitation.setForAccountId(newUser.getAccountId()); + projectInvitation.setForUserId(newUser.getId()); + update(projectInvitation.getId(), projectInvitation); + } + } + + private SearchCriteria prepareAllFieldsSearchCriteria(Long projectId, Long accountId, Long userId) { + SearchCriteria sc = AllFieldsSearch.create(); + + sc.setParametersIfNotNull("userId", userId); + sc.setParametersIfNotNull("accountId", accountId); + if (projectId != null && projectId != -1) { + sc.setParameters("projectId", projectId); + } + + return sc; + } + @Override public boolean isActive(long id, long timeout) { SearchCriteria sc = InactiveSearch.create(); 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 index e9e232ef2023..acc54ccd41fa 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/InternalBackupJoinVO.java @@ -101,15 +101,6 @@ public class InternalBackupJoinVO { @Column(name = "isolated") private Boolean isolated; - @Column(name = "storage_pool_delta_path") - private String storagePoolDeltaPath; - - @Column(name = "storage_pool_parent_path") - private String storagePoolParentPath; - - @Column(name = "schedule_id") - private Long scheduleId; - public InternalBackupJoinVO() { } @@ -192,18 +183,6 @@ public Backup.CompressionStatus getCompressionStatus() { return compressionStatus; } - public String getStoragePoolDeltaPath() { - return storagePoolDeltaPath; - } - - public String getStoragePoolParentPath() { - return storagePoolParentPath; - } - - public Long getScheduleId() { - return scheduleId; - } - @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE); 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 index 3cb6ea241041..2f9c3dd1ff8f 100644 --- 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 @@ -24,15 +24,11 @@ public interface InternalBackupJoinDao extends GenericDao { - List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Long scheduleId, Date date, boolean before, boolean ascending); + List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Date date, boolean before, boolean ascending); - List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Long scheduleId, Date beforeDate); + List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Date beforeDate); - InternalBackupJoinVO findCurrent(long vmId, Long scheduleId); - - List listCurrents(long vmId, boolean descending); - - List listCurrentsByVolumeIdDesc(long volumeId); + InternalBackupJoinVO findCurrent(long vmId); InternalBackupJoinVO findByParentId(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 index bbc91db13e83..e8d823bb69c5 100644 --- 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 @@ -39,8 +39,6 @@ public class InternalBackupJoinDaoImpl extends GenericDaoBase backupSearch; private SearchBuilder allBackupsSearch; @@ -54,7 +52,6 @@ protected void init() { 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.and(SCHEDULE_ID, backupSearch.entity().getScheduleId(), SearchCriteria.Op.EQ); backupSearch.groupBy(backupSearch.entity().getId()); backupSearch.done(); @@ -63,14 +60,11 @@ protected void init() { 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.and(VM_ID, allBackupsSearch.entity().getVmId(), SearchCriteria.Op.EQ); - allBackupsSearch.and(VOLUME_ID, allBackupsSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); - allBackupsSearch.and(CURRENT, allBackupsSearch.entity().getCurrent(), SearchCriteria.Op.EQ); allBackupsSearch.done(); } @Override - public List listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(long vmId, Long scheduleId, Date date, boolean before, boolean ascending) { + 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); @@ -80,29 +74,26 @@ public List listByBackedUpAndVmIdAndDateBeforeOrAfterOrder sc.setParameters(CREATED_AFTER, date); } sc.setParameters(ISOLATED, Boolean.FALSE.toString()); - sc.setParameters(SCHEDULE_ID, scheduleId); Filter filter = new Filter(InternalBackupJoinVO.class, "date", ascending); return new ArrayList<>(listBy(sc, filter)); } @Override - public List listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(long vmId, Long scheduleId, Date beforeDate) { + 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()); - sc.setParameters(SCHEDULE_ID, scheduleId); Filter filter = new Filter(InternalBackupJoinVO.class, "date", false); return new ArrayList<>(listIncludingRemovedBy(sc, filter)); } @Override - public InternalBackupJoinVO findCurrent(long vmId, Long scheduleId) { + public InternalBackupJoinVO findCurrent(long vmId) { SearchCriteria sc = backupSearch.create(); sc.setParameters(VM_ID, vmId); sc.setParameters(CURRENT, Boolean.TRUE.toString()); - sc.setParameters(SCHEDULE_ID, scheduleId); return findOneBy(sc); } @@ -136,23 +127,4 @@ public List listByParentId(long parentId) { sc.setParameters(STATUS, Backup.Status.BackedUp); return listBy(sc); } - - @Override - public List listCurrents(long vmId, boolean descending) { - SearchCriteria sc = allBackupsSearch.create(); - sc.setParameters(VM_ID, vmId); - sc.setParameters(CURRENT, Boolean.TRUE.toString()); - Filter filter = new Filter(InternalBackupJoinVO.class, "date", !descending); - - return listBy(sc, filter); - } - - @Override - public List listCurrentsByVolumeIdDesc(long volumeId) { - SearchCriteria sc = allBackupsSearch.create(); - sc.setParameters(VOLUME_ID, volumeId); - sc.setParameters(CURRENT, Boolean.TRUE.toString()); - Filter filter = new Filter(InternalBackupJoinVO.class, "date", false); - return listBy(sc, filter); - } } 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 index e6628f78af8c..7e2ac5a249e4 100644 --- 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 @@ -25,13 +25,9 @@ public interface InternalBackupStoragePoolDao extends GenericDao listByBackupId(long backupId); - List listByVolumeId(long volumeId); - - InternalBackupStoragePoolVO findOneByVolumeIdAndBackupId(long volumeId, long backupId); + InternalBackupStoragePoolVO findOneByVolumeId(long volumeId); void expungeByBackupId(long backupId); void expungeByVolumeId(long volumeId); - - void expungeByVolumeIdAndBackupId(long volumeId, long backupId); } 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 index 41002aeb51d2..443ceed02e7d 100644 --- 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 @@ -48,17 +48,9 @@ public List listByBackupId(long backupId) { } @Override - public List listByVolumeId(long volumeId) { + public InternalBackupStoragePoolVO findOneByVolumeId(long volumeId) { SearchCriteria sc = backupSearch.create(); sc.setParameters(VOLUME_ID, volumeId); - return listBy(sc); - } - - @Override - public InternalBackupStoragePoolVO findOneByVolumeIdAndBackupId(long volumeId, long backupId) { - SearchCriteria sc = backupSearch.create(); - sc.setParameters(VOLUME_ID, volumeId); - sc.setParameters(BACKUP_ID, backupId); return findOneBy(sc); } @@ -75,12 +67,4 @@ public void expungeByVolumeId(long volumeId) { sc.setParameters(VOLUME_ID, volumeId); expunge(sc); } - - @Override - public void expungeByVolumeIdAndBackupId(long volumeId, long backupId) { - SearchCriteria sc = backupSearch.create(); - sc.setParameters(VOLUME_ID, volumeId); - sc.setParameters(BACKUP_ID, backupId); - expunge(sc); - } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300-cleanup.sql index e2b066af7800..13b73fe9648b 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300-cleanup.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300-cleanup.sql @@ -18,3 +18,7 @@ --; -- Schema upgrade cleanup from 4.22.1.0 to 4.23.0.0 --; + +-- Delete stale project association entries for users that were removed +DELETE FROM `cloud`.`project_account` WHERE `user_id` IN (SELECT `id` FROM `cloud`.`user` WHERE `removed` IS NOT NULL); +DELETE FROM `cloud`.`project_invitations` WHERE `user_id` IN (SELECT `id` FROM `cloud`.`user` WHERE `removed` IS NOT NULL); 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 index 9e6be1fc5b7f..a1fbd102630b 100644 --- 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 @@ -37,15 +37,11 @@ SELECT b.id, 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, - nbpr.backup_delta_path storage_pool_delta_path, - nbpr.backup_parent_path storage_pool_parent_path, - nbsr.path image_store_path, - bs.id schedule_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 and nbpr.backup_id = b.id -LEFT JOIN backup_schedule bs ON bs.id = b.backup_schedule_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/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 15f16ae7b4ba..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; @@ -49,40 +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.BackupManagerImpl; -import org.apache.cloudstack.backup.BackupOfferingVO; -import org.apache.cloudstack.backup.InternalBackupJoinVO; -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.InternalBackupJoinDao; -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 javax.inject.Inject; -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; public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStrategy { @@ -106,8 +106,6 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra @Inject private SnapshotDao snapshotDao; - @Inject - private InternalBackupJoinDao internalBackupJoinDao; @Override public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { @@ -153,7 +151,7 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { List volumeSnapshotVos = new ArrayList<>(); if (isCurrent && numberOfChildren == 0) { - volumeSnapshotVos = mergeSucceedingDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs); + volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs); } else if (numberOfChildren == 0) { logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid()); volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId); @@ -278,7 +276,7 @@ private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParen List snapshotVos; if (oldParent.getCurrent()) { - snapshotVos = mergeSucceedingDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); + snapshotVos = mergeCurrentDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); } else { List oldSiblings = vmSnapshotDao.listByParentAndStateIn(oldParent.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); @@ -426,7 +424,7 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO SnapshotObjectTO parentTO = (SnapshotObjectTO) deltaMergeTreeTO.getParent(); if (childTO instanceof BackupDeltaTO) { - InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(parentTO.getVolume().getVolumeId(), childTO.getId()); + 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); @@ -447,27 +445,26 @@ private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO return snapshotVOList; } - private List mergeSucceedingDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { - logger.debug(String.format("Merging VM snapshot [%s] with the succeeding delta.", vmSnapshotVo.getUuid())); + private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { + logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid()); List deltaMergeTreeTOs = new ArrayList<>(); List volumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVo.getId()); - Map volumeIdAndSucceedingBackupMap = getVolumeIdAndSucceedingBackupMap(vmSnapshotVo); for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { - Long volumeId = volumeObjectTO.getId(); - SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeId)) + 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(); - if (volumeIdAndSucceedingBackupMap.containsKey(volumeId)) { - InternalBackupJoinVO succeedingBackup = volumeIdAndSucceedingBackupMap.get(volumeId); - logger.debug("The succeeding delta is also a KNIB backup delta. Will merge the snapshot delta of volume [{}] with the parent backup delta at [{}].", - volumeObjectTO.getUuid(), succeedingBackup.getStoragePoolParentPath()); - BackupDeltaTO childTo = new BackupDeltaTO(succeedingBackup.getId(), volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, succeedingBackup.getStoragePoolParentPath()); + 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, succeedingBackup.getStoragePoolDeltaPath())); + grandChildren.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, backupDelta.getBackupDeltaPath())); } deltaMergeTreeTOs.add(new DeltaMergeTreeTO(volumeObjectTO, parentSnapshot, childTo, grandChildren)); } else { @@ -491,7 +488,7 @@ private List mergeSucceedingDeltaOnSnapshot(VMSnapshotVO vmSnapshotV 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.findOneByVolumeIdAndBackupId(parentTO.getVolume().getVolumeId(), dataTO.getId()); + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(parentTO.getVolume().getVolumeId()); backupDelta.setBackupDeltaParentPath(parentTO.getPath()); internalBackupStoragePoolDao.update(backupDelta.getId(), backupDelta); } else { @@ -655,7 +652,6 @@ private List generateDeltaMergeTrees(VMSnapshotVO parent, VMSn List parentVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(parent.getId()); List childVolumeSnapshots = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(child.getId()); List grandChildrenVolumeSnapshots = new ArrayList<>(); - Map volumeIdAndSucceedingBackupMap = getVolumeIdAndSucceedingBackupMap(parent); for (VMSnapshotVO grandChild : grandChildren) { grandChildrenVolumeSnapshots.addAll(vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(grandChild.getId())); @@ -664,14 +660,14 @@ private List generateDeltaMergeTrees(VMSnapshotVO parent, VMSn for (SnapshotDataStoreVO parentSnapshotDataStoreVO : parentVolumeSnapshots) { SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); VolumeObjectTO volumeObjectTO = parentTO.getVolume(); - InternalBackupJoinVO succeedingBackup = volumeIdAndSucceedingBackupMap.get(volumeObjectTO.getId()); SnapshotDataStoreVO childVO = childVolumeSnapshots.stream() .filter(childSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), childSnapshot.getVolumeId())) .findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Could not find child snapshot of parent [%s].", parentSnapshotDataStoreVO.getSnapshotId()))); + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(childVO.getVolumeId()); List grandChildrenTOList = new ArrayList<>(); - DataTO childTO = getChildAndGrandChildren(child, stoppedVm, parentSnapshotDataStoreVO, succeedingBackup, childVO, volumeObjectTO, grandChildrenTOList, + DataTO childTO = getChildAndGrandChildren(child, stoppedVm, parentSnapshotDataStoreVO, backupDelta, childVO, volumeObjectTO, grandChildrenTOList, grandChildrenVolumeSnapshots); snapshotMergeTrees.add(new DeltaMergeTreeTO(volumeObjectTO, parentTO, childTO, grandChildrenTOList)); @@ -684,16 +680,16 @@ private List generateDeltaMergeTrees(VMSnapshotVO parent, VMSn /** * Gets the correct children and grandchildren, taking KBOSS backups into account. * */ - private DataTO getChildAndGrandChildren(VMSnapshotVO childSnapshot, boolean stoppedVm, SnapshotDataStoreVO parentSnapshotDataStoreVO, InternalBackupJoinVO childBackup, + private DataTO getChildAndGrandChildren(VMSnapshotVO child, boolean stoppedVm, SnapshotDataStoreVO parentSnapshotDataStoreVO, InternalBackupStoragePoolVO backupDelta, SnapshotDataStoreVO childVO, VolumeObjectTO volumeObjectTO, List grandChildrenTOList, List grandChildrenVolumeSnapshots) { DataTO childTO; - if (childBackup != null && childBackup.getDate().before(childSnapshot.getCreated())) { + 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.", parentSnapshotDataStoreVO.getInstallPath(), childBackup.getStoragePoolDeltaPath()); - childTO = new BackupDeltaTO(childBackup.getId(), volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, childBackup.getStoragePoolParentPath()); - if (stoppedVm) { - grandChildrenTOList.add(new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, childBackup.getStoragePoolDeltaPath())); + "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())); } } else { childTO = snapshotDataFactory.getSnapshot(childVO.getSnapshotId(), childVO.getDataStoreId(), DataStoreRole.Primary).getTO(); @@ -703,7 +699,7 @@ private DataTO getChildAndGrandChildren(VMSnapshotVO childSnapshot, boolean stop .collect(Collectors.toList())); } - if (childSnapshot.getCurrent() && stoppedVm && grandChildrenTOList.isEmpty()) { + if (child.getCurrent() && stoppedVm) { grandChildrenTOList.add(volumeObjectTO); } @@ -762,26 +758,4 @@ private void transitStateWithoutThrow(VMSnapshot vmSnapshot, VMSnapshot.Event ev throw new CloudRuntimeException(msg, e); } } - - - private Map getVolumeIdAndSucceedingBackupMap(VMSnapshotVO vmSnapshotVO) { - Map volumeIdAndSucceedingBackupMap = new HashMap<>(); - if (vmSnapshotVO == null) { - return volumeIdAndSucceedingBackupMap; - } - - List currents = internalBackupJoinDao.listCurrents(vmSnapshotVO.getVmId(), false) - .stream().filter(internalBackupJoinVO -> internalBackupJoinVO.getDate().after(vmSnapshotVO.getCreated())).collect(Collectors.toList()); - if (currents.isEmpty()) { - logger.debug("No backups created after the VM snapshot [{}] were found, returning.", vmSnapshotVO.getUuid()); - return volumeIdAndSucceedingBackupMap; - } - - InternalBackupJoinVO succeedingBackup = currents.get(0); - volumeIdAndSucceedingBackupMap = currents.stream().filter(b -> succeedingBackup.getId() == b.getId()) - .collect(Collectors.toMap(InternalBackupJoinVO::getVolumeId, internalBackupJoinVO -> internalBackupJoinVO)); - logger.debug("Found the following backups that succeeds the VM snapshot [{}]: [{}].", vmSnapshotVO.getUuid(), volumeIdAndSucceedingBackupMap.values()); - - return volumeIdAndSucceedingBackupMap; - } } 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 index 9101a591587e..56197e5fc8cd 100644 --- 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 @@ -256,7 +256,7 @@ For all standard network / VPC commands, CloudStack now executes the script as: ``` | 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. | @@ -277,7 +277,7 @@ 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. | @@ -292,7 +292,7 @@ The following names appear repeatedly inside the nested `payload` object. ### 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. | @@ -308,7 +308,7 @@ The following names appear repeatedly inside the nested `payload` object. ### 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. | @@ -319,7 +319,7 @@ The following names appear repeatedly inside the nested `payload` object. ### 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. | @@ -357,7 +357,7 @@ passed back to later `ensure-network-device` calls as `payload.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. | @@ -389,7 +389,7 @@ configure the gateway. **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN tag. | | `gateway` | Guest network gateway. | @@ -411,7 +411,7 @@ 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. | @@ -440,7 +440,7 @@ return. **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN tag. | | `vpc_id` | Present for VPC tier networks. | @@ -471,7 +471,7 @@ 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. | @@ -492,7 +492,7 @@ The `extension.details` blob is removed from CloudStack after a successful retur **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `vpc_id` | VPC ID. | --- @@ -507,7 +507,7 @@ The `extension.details` blob is removed from CloudStack after a successful retur **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `vpc_id` | VPC ID. | | `vpc_cidr` | VPC supernet CIDR. | | `public_ip` | New source-NAT IP. | @@ -531,7 +531,7 @@ network (source NAT, static NAT, PF, LB allocation). **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN. | | `public_ip` | The public IP being assigned or released. | @@ -556,7 +556,7 @@ and a VM private IP. **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN tag. | | `public_ip` | Public IP. | @@ -578,7 +578,7 @@ and a VM private IP. **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN tag. | | `public_ip` | Public IP. | @@ -606,7 +606,7 @@ 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. | @@ -645,7 +645,7 @@ rules for the network, so a full rebuild is always safe. ``` | 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"`. | @@ -670,7 +670,7 @@ 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. | @@ -702,7 +702,7 @@ Rules are applied in ascending `number` order. ``` | Field | Description | -|---|---| +| --- | --- | | `number` | Rule priority (lower number = higher priority). | | `action` | `"allow"` or `"deny"`. | | `trafficType` | `"ingress"` or `"egress"`. | @@ -730,7 +730,7 @@ 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. | @@ -749,7 +749,7 @@ those belonging to DHCP/DNS-enabled offerings. **`release-nic` payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `vlan` | Guest VLAN tag. | | `mac` | VM NIC MAC address. | @@ -779,7 +779,7 @@ network whose DHCP service is provided by this extension. **`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. | @@ -796,7 +796,7 @@ network whose DHCP service is provided by this extension. **`remove-dhcp-entry` payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `mac` | VM NIC MAC address. | | `ip` | VM assigned IP. | @@ -816,7 +816,7 @@ 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. | @@ -830,7 +830,7 @@ without tying it to a specific VM. **`remove-dhcp-subnet` payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `extension_ip` | Extension IP. | | `vpc_id` | Present for VPC tier networks. | @@ -845,7 +845,7 @@ without tying it to a specific VM. **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"}`. | @@ -862,7 +862,7 @@ provided by this extension. **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `ip` | VM IP. | | `hostname` | VM hostname. | @@ -879,7 +879,7 @@ provided by this extension. **`config-dns-subnet` payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `gateway` | Guest network gateway. | | `cidr` | Guest network CIDR. | @@ -893,7 +893,7 @@ provided by this extension. **`remove-dns-subnet` payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `extension_ip` | Extension IP. | | `vpc_id` | Present for VPC tier networks. | @@ -911,7 +911,7 @@ 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. | @@ -947,7 +947,7 @@ Your metadata HTTP server should serve each entry at: **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `ip` | VM IP. | | `gateway` | Gateway of the VM NIC. | @@ -966,7 +966,7 @@ Your metadata HTTP server should serve each entry at: **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `ip` | VM IP. | | `gateway` | Gateway of the VM NIC. | @@ -985,7 +985,7 @@ Your metadata HTTP server should serve each entry at: **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `ip` | VM IP. | | `gateway` | Gateway of the VM NIC. | @@ -1006,7 +1006,7 @@ 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. | @@ -1029,7 +1029,7 @@ 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. | @@ -1057,7 +1057,7 @@ virtual server → backend pool mappings. ``` | 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`. | @@ -1077,7 +1077,7 @@ calls). **Payload fields (`payload` object):** | Field | Description | -|---|---| +| --- | --- | | `network_id` | Network ID. | | `gateway` | Guest network gateway. | | `cidr` | Guest network CIDR. | @@ -1133,7 +1133,7 @@ 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. | @@ -1144,7 +1144,7 @@ including `physical-network-extension-details` and `network-extension-details`. **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. | @@ -1160,7 +1160,7 @@ Hook scripts should parse the payload file directly. ## 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` | @@ -1300,7 +1300,7 @@ 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. | @@ -1308,7 +1308,7 @@ 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. | 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 cf02de1f6c18..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 @@ -154,7 +154,7 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long backupScheduleId) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated) { logger.debug("Starting backup for VM {} on Dummy provider", vm); BackupVO backup = new BackupVO(); 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 index 0569c318935e..596dc62b7206 100644 --- 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 @@ -29,12 +29,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -131,7 +129,6 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; import com.cloud.utils.Predicate; -import com.cloud.utils.Ternary; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.Transaction; @@ -334,23 +331,11 @@ 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); - List currents = internalBackupJoinDao.listCurrents(vm.getId(), true); - - return finishAllChains(vm, currents); - } - - @Override - public boolean removeVMBackupSchedule(VirtualMachine vm, BackupSchedule backupSchedule) { - logger.info("Removing VM [{}] from KBOSS backup schedule.", vm.getUuid()); - - if (endBackupChain(vm, backupSchedule.getId())) { + if (endBackupChain(vm)) { return true; } UserVmVO vmVO = userVmDao.findById(vm.getId()); - logger.error("Failed to merge deltas for VM [{}] during backup schedule removal process. Changing its state to [{}].", vm, VirtualMachine.State.BackupError); - BackupVO backupVO = backupDao.findById(internalBackupJoinDao.findCurrent(vm.getId(), backupSchedule.getId()).getId()); - backupVO.setStatus(Backup.Status.Error); - backupDao.update(backupVO.getId(), backupVO); + 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); @@ -364,9 +349,9 @@ public boolean willDeleteBackupsOnOfferingRemoval() { } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVm, boolean isolated, Long backupScheduleId) { + 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, backupScheduleId); + Outcome outcome = createBackupThroughJobQueue(vm, ObjectUtils.defaultIfNull(quiesceVm, false), isolated); try { outcome.get(); @@ -438,6 +423,13 @@ public Pair orchestrateTakeBackup(Backup backup, boolean quiesceV 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); @@ -445,24 +437,14 @@ public Pair orchestrateTakeBackup(Backup backup, boolean quiesceV DataStore imageStore = getImageStoreForBackup(userVm.getDataCenterId(), backupVO); createBasicBackupDetails(imageStore.getId(), fullBackup ? 0L : parentBackup.getId(), backupVO); - List succeedingBackupList = getSucceedingBackupList(parentBackup); - InternalBackupJoinVO succeedingBackup = succeedingBackupList.isEmpty() ? null : succeedingBackupList.get(0); - List succeedingVmSnapshotList = getSucceedingVmSnapshotList(parentBackup); VMSnapshotVO succeedingVmSnapshot = succeedingVmSnapshotList.isEmpty() ? null : succeedingVmSnapshotList.get(0); - if (!fullBackup) { - parentBackupDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parentBackup.getId()); - parentBackupDeltasOnSecondary = internalBackupDataStoreDao.listByBackupId(parentBackup.getId()); - - chainImageStoreUrls = getChainImageStoreUrls(backupChain); - } - - Map> volumeIdToSnapshotDataStoreAndBackupPathList = mapVolumesToVmSnapshotAndBackupReferences(volumeTOs, succeedingVmSnapshotList, succeedingBackupList); + Map> volumeIdToSnapshotDataStoreList = mapVolumesToVmSnapshotReferences(volumeTOs, succeedingVmSnapshotList); for (VolumeObjectTO volumeObjectTO : volumeTOs) { - KbossTO kbossTO = new KbossTO(volumeObjectTO, volumeIdToSnapshotDataStoreAndBackupPathList.getOrDefault(volumeObjectTO.getId(), new LinkedList<>())); + KbossTO kbossTO = new KbossTO(volumeObjectTO, volumeIdToSnapshotDataStoreList.getOrDefault(volumeObjectTO.getId(), new ArrayList<>())); kbossTOs.add(kbossTO); - createDeltaReferences(fullBackup, runningVm, backup, parentBackupDeltasOnSecondary, + createDeltaReferences(fullBackup, !succeedingVmSnapshotList.isEmpty(), runningVm, backup, parentBackupDeltasOnSecondary, parentBackupDeltasOnPrimary, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, succeedingVmSnapshot, kbossTO); } @@ -477,7 +459,7 @@ public Pair orchestrateTakeBackup(Backup backup, boolean quiesceV } processBackupSuccess(runningVm, volumeTOs, volumeUuidToDeltaPrimaryRef, volumeUuidToDeltaSecondaryRef, (TakeKbossBackupAnswer)answer, parentBackupDeltasOnPrimary, - succeedingVmSnapshot, backupVO, fullBackup, userVm, hostId, newBackupJoin.getEndOfChain(), isolated, succeedingBackup); + succeedingVmSnapshotList, backupVO, fullBackup, userVm, hostId, newBackupJoin.getEndOfChain(), isolated); if (!isolated) { updateCurrentBackup(newBackupJoin); @@ -577,7 +559,7 @@ public Boolean orchestrateDeleteBackup(Backup backup, boolean forced) { List removedBackupIds = backupParentsToBeRemovedAndLastAliveBackup.first().stream().map(InternalBackupJoinVO::getId).collect(Collectors.toList()); removedBackupIds.add(backup.getId()); - boolean isFailedSetEmpty = processRemoveBackupFailures(forced, deleteAnswers, removedBackupIds, backupJoinVO, virtualMachine); + boolean isFailedSetEmpty = processRemoveBackupFailures(forced, deleteAnswers, removedBackupIds, backupJoinVO); processRemovedBackups(removedBackupIds); @@ -625,10 +607,10 @@ public Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, } InternalBackupJoinVO backupJoinVO = internalBackupJoinDao.findById(backupId); - List currentBackups = sameVmAsBackup ? internalBackupJoinDao.listCurrents(vm.getId(), false) : List.of(); + InternalBackupJoinVO currentBackup = sameVmAsBackup ? internalBackupJoinDao.findCurrent(vm.getId()) : null; List deltasOnPrimary = new ArrayList<>(); - for (InternalBackupJoinVO currentBackup : currentBackups) { - deltasOnPrimary.addAll(0, internalBackupStoragePoolDao.listByBackupId(currentBackup.getId())); + if (currentBackup != null) { + deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(currentBackup.getId()); } List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(backupId); List volumeTOs = vmSnapshotHelper.getVolumeTOList(vm.getId()); @@ -685,7 +667,7 @@ public Boolean orchestrateRestoreVMFromBackup(Backup backup, VirtualMachine vm, updateVolumePathsAndSizeIfNeeded(vm, volumeTOs, volumeInfos, deltasToBeMerged, sameVmAsBackup); - for (InternalBackupJoinVO currentBackup : currentBackups) { + if (currentBackup != null) { internalBackupStoragePoolDao.expungeByBackupId(currentBackup.getId()); setEndOfChainAndRemoveCurrentForBackup(currentBackup); } @@ -901,17 +883,17 @@ public Pair restoreBackupToVM(VirtualMachine vm, Backup backup, } @Override - public boolean finishBackupChains(VirtualMachine virtualMachine) { - UserVmVO vm = userVmDao.findById(virtualMachine.getId()); - List currents = internalBackupJoinDao.listCurrents(vm.getId(), true); - if (allowedVmStates.contains(vm.getState())) { - return finishAllChains(vm, currents); - } - if (vm.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].", vm.getUuid()); + 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(vm); + return normalizeBackupErrorAndFinishChain(userVmVO); } @Override @@ -945,14 +927,14 @@ public boolean supportsMemoryVmSnapshot() { @Override public void prepareVolumeForDetach(Volume volume, VirtualMachine virtualMachine) { logger.info("Preparing volume [{}] for detach.", volume.getUuid()); - mergeCurrentDeltasIntoVolume(volume, virtualMachine, "detach", virtualMachine.getState().equals(VirtualMachine.State.Running)); + 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()); - mergeCurrentDeltasIntoVolume(volume, vm, "live migration", true); + mergeCurrentDeltaIntoVolume(volume, vm, "live migration", true); } } @@ -963,37 +945,31 @@ public void updateVolumeId(VirtualMachine virtualMachine, long oldVolumeId, long @Override public void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine virtualMachine) { - List currentBackups = internalBackupJoinDao.listCurrents(virtualMachine.getId(), true); + InternalBackupJoinVO currentBackup = internalBackupJoinDao.findCurrent(virtualMachine.getId()); - if (currentBackups.isEmpty()) { + if (currentBackup == null) { logger.debug("There is no current backup delta, the VM [{}] is already prepared for VM snapshot revert.", virtualMachine.getUuid()); return; } - currentBackups = currentBackups.stream().filter(backup -> backup.getDate().after(vmSnapshot.getCreated())).collect(Collectors.toList()); - if (currentBackups.isEmpty()) { - logger.debug("Existing backup deltas [{}] were created before the target VM snapshot [{}]. No preparation needed for VM [{}].", - currentBackups, vmSnapshot.getCreated(), virtualMachine.getUuid()); + 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<>(); - Map backupVmSnapshotMap = new HashMap<>(); - - for (InternalBackupJoinVO currentBackup : currentBackups) { - VMSnapshotVO vmSnapshotSucceedingCurrentBackup = getSucceedingVmSnapshot(currentBackup); - - createDeleteCommandsAndMergeTrees(volumeObjectTOs, commands, deletedDeltas, vmSnapshotSucceedingCurrentBackup, deltaMergeTreeTOList, currentBackup); - backupVmSnapshotMap.put(currentBackup, vmSnapshotSucceedingCurrentBackup); - } + createDeleteCommandsAndMergeTrees(volumeObjectTOs, commands, deletedDeltas, vmSnapshotSucceedingCurrentBackup, deltaMergeTreeTOList); - if (CollectionUtils.isNotEmpty(deltaMergeTreeTOList)) { + if (!deltaMergeTreeTOList.isEmpty()) { commands.addCommand(new MergeDiskOnlyVmSnapshotCommand(deltaMergeTreeTOList, false, virtualMachine.getInstanceName())); } @@ -1012,18 +988,12 @@ public void prepareVmForSnapshotRevert(VMSnapshot vmSnapshot, VirtualMachine vir throw new CloudRuntimeException(String.format("Unable to prepare VM [%s] for VM snapshot reversion.", virtualMachine.getUuid())); } - for (Map.Entry backupAndVmSnapshot : backupVmSnapshotMap.entrySet()) { - InternalBackupJoinVO backup = backupAndVmSnapshot.getKey(); - VMSnapshotVO vmSnapshotSucceedingBackup = backupAndVmSnapshot.getValue(); - - List snapRefsSucceedingCurrentBackup = new ArrayList<>(); - - if (vmSnapshotSucceedingBackup != null) { - snapRefsSucceedingCurrentBackup = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotSucceedingBackup.getId()); - } - - updateReferencesAfterPrepareForSnapshotRevert(deltaMergeTreeTOList, snapRefsSucceedingCurrentBackup, deletedDeltas, backup); + List snapRefsSucceedingCurrentBackup = new ArrayList<>(); + if (vmSnapshotSucceedingCurrentBackup != null) { + snapRefsSucceedingCurrentBackup = vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotSucceedingCurrentBackup.getId()); } + + updateReferencesAfterPrepareForSnapshotRevert(deltaMergeTreeTOList, snapRefsSucceedingCurrentBackup, deletedDeltas, currentBackup); } /** @@ -1064,14 +1034,14 @@ public ConfigKey[] getConfigKeys() { backupCompressionCoroutines}; } - protected Outcome createBackupThroughJobQueue(VirtualMachine vm, boolean quiesceVm, boolean isolated, Long backupScheduleId) { + 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, backupScheduleId, + 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, @@ -1269,14 +1239,16 @@ 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 of schedule [{}] for VM [{}]. The next backup will be a full backup.", - backupVO.getBackupScheduleId(), BackupValidationServiceJobController.backupValidationEndChainOnFail.toString()); - endBackupChain(userVmDao.findById(backupVO.getVmId()), backupVO.getBackupScheduleId()); + 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); } } @@ -1292,20 +1264,12 @@ protected boolean normalizeBackupErrorAndFinishChain(UserVmVO userVmVO) { boolean runningVM = detail == null || VirtualMachine.State.valueOf(detail.getValue()) == VirtualMachine.State.Running; BackupVO backupVO = backupDao.findLatestByStatusAndVmId(Backup.Status.Error, userVmVO.getId()); - InternalBackupJoinVO currentOnThisChain = internalBackupJoinDao.findCurrent(userVmVO.getId(), backupVO.getBackupScheduleId()); - InternalBackupJoinVO errorBackup = internalBackupJoinDao.findById(backupVO.getId()); - - boolean errorOnBackupCreation = currentOnThisChain == null || currentOnThisChain.getId() != errorBackup.getId(); - - List succeedingBackupList = getSucceedingBackupList(currentOnThisChain); - List succeedingVmSnapshotList = getSucceedingVmSnapshotList(currentOnThisChain); - List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVmVO.getId()); - - Map> volumeToDeltasAfterCurrent = mapVolumesToVmSnapshotAndBackupReferences(volumeTOs, succeedingVmSnapshotList, succeedingBackupList); + InternalBackupJoinVO internalBackupJoinVO = internalBackupJoinDao.findById(backupVO.getId()); + ImageStoreVO imageStoreVO = imageStoreDao.findById(internalBackupJoinVO.getImageStoreId()); List kbossTOS = new ArrayList<>(); - List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(errorBackup.getId()); - InternalBackupJoinVO parent = internalBackupJoinDao.findById(errorBackup.getParentId()); + 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<>(); @@ -1313,11 +1277,9 @@ protected boolean normalizeBackupErrorAndFinishChain(UserVmVO userVmVO) { parentDeltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(parent.getId()); } - List deltasOnSecondary = internalBackupDataStoreDao.listByBackupId(errorBackup.getId()); - ImageStoreVO imageStoreVO = imageStoreDao.findById(errorBackup.getImageStoreId()); - configureKbossTosForCleanup(userVmVO, deltasOnPrimary, volumeToDeltasAfterCurrent, deltasOnSecondary, parentDeltasOnPrimary, kbossTOS, errorOnBackupCreation); - CleanupKbossBackupErrorCommand command = new CleanupKbossBackupErrorCommand(runningVM, errorOnBackupCreation, errorBackup.getEndOfChain(), succeedingBackupList.isEmpty(), - userVmVO.getInstanceName(), imageStoreVO.getUrl(), kbossTOS); + 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); @@ -1326,40 +1288,32 @@ protected boolean normalizeBackupErrorAndFinishChain(UserVmVO userVmVO) { return false; } - boolean chainAlreadyEnded = processCleanupBackupErrorAnswer(userVmVO, answer, errorBackup, currentOnThisChain, succeedingBackupList); + boolean chainAlreadyEnded = processCleanupBackupErrorAnswer(userVmVO, answer); if (!chainAlreadyEnded) { - mergeCurrentBackupDeltas(errorBackup); - } - - if (currentOnThisChain != null) { - internalBackupStoragePoolDao.expungeByBackupId(currentOnThisChain.getId()); - setEndOfChainAndRemoveCurrentForBackup(currentOnThisChain); + return endBackupChain(userVmVO); } + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(userVmVO.getId()); + internalBackupStoragePoolDao.expungeByBackupId(current.getId()); + setEndOfChainAndRemoveCurrentForBackup(current); - return finishBackupChains(userVmVO); + return true; } - protected boolean processCleanupBackupErrorAnswer(UserVmVO userVmVO, Answer answer, InternalBackupJoinVO errorBackup, InternalBackupJoinVO currentBackup, - List succeedingBackups) { + 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 = true; - for (Map.Entry> entry : cleanAnswer.getVolumeIdToPathAndChainEnded().entrySet()) { - VolumeVO volumeVO = volumeDao.findByUuid(entry.getKey()); - if (!entry.getValue().first().equals(volumeVO.getPath())) { - volumeVO.setPath(entry.getValue().first()); + 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); - if (!entry.getValue().second()) { - chainAlreadyEnded = false; - continue; - } - internalBackupStoragePoolDao.expungeByVolumeIdAndBackupId(volumeVO.getId(), errorBackup.getId()); + chainAlreadyEnded = true; } } - updateSucceedingBackupIfNeeded(currentBackup, succeedingBackups); runningVM = cleanAnswer.isVmRunning(); userVmVO.setState(runningVM ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); @@ -1368,20 +1322,6 @@ protected boolean processCleanupBackupErrorAnswer(UserVmVO userVmVO, Answer answ return chainAlreadyEnded; } - private void updateSucceedingBackupIfNeeded(InternalBackupJoinVO currentBackup, List succeedingBackups) { - if (currentBackup == null || succeedingBackups.isEmpty()) { - return; - } - InternalBackupJoinVO succeedingBackup = succeedingBackups.get(0); - for (InternalBackupStoragePoolVO deltaRef : internalBackupStoragePoolDao.listByBackupId(currentBackup.getId())) { - InternalBackupStoragePoolVO succeedingDelta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(deltaRef.getVolumeId(), succeedingBackup.getId()); - if (succeedingDelta != null) { - succeedingDelta.setBackupDeltaParentPath(deltaRef.getBackupDeltaParentPath()); - internalBackupStoragePoolDao.update(succeedingDelta.getId(), succeedingDelta); - } - } - } - 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); @@ -1585,10 +1525,51 @@ protected boolean deleteFailedBackup(BackupVO backupVO) { 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 runningVm, Backup backup, + protected void createDeltaReferences(boolean fullBackup, boolean hasVmSnapshotSucceedingLastBackup, boolean runningVm, Backup backup, List parentBackupDeltasOnSecondary, List parentBackupDeltasOnPrimary, HashMap volumeUuidToDeltaPrimaryRef, HashMap volumeUuidToDeltaSecondaryRef, VMSnapshotVO succeedingVmSnapshot, KbossTO kbossTO) { @@ -1603,8 +1584,7 @@ protected void createDeltaReferences(boolean fullBackup, boolean runningVm, Back InternalBackupDataStoreVO deltaSecondaryRef = new InternalBackupDataStoreVO(backup.getId(), volumeObjectTO.getVolumeId(), volumeObjectTO.getDeviceId(), relativePathOnSecondary); if (!fullBackup) { - InternalBackupStoragePoolVO parentDeltaOnPrimary = createDeltaMergeTreeForVolume(false, runningVm, parentBackupDeltasOnPrimary, succeedingVmSnapshot, kbossTO, - new ArrayList<>()); + InternalBackupStoragePoolVO parentDeltaOnPrimary = createDeltaMergeTreeForVolume(false, runningVm, parentBackupDeltasOnPrimary, succeedingVmSnapshot, kbossTO); findAndSetParentBackupPath(parentBackupDeltasOnSecondary, parentDeltaOnPrimary, kbossTO); } @@ -1615,8 +1595,10 @@ protected void createDeltaReferences(boolean fullBackup, boolean runningVm, Back InternalBackupStoragePoolVO deltaPrimaryRef = new InternalBackupStoragePoolVO(backup.getId(), volumeObjectTO.getPoolId(), volumeObjectTO.getVolumeId(), filename, volumeObjectTO.getPath()); - if (kbossTO.getDeltaMergeTreeTO() != null && CollectionUtils.isEmpty(kbossTO.getDeltaPaths())) { + if (kbossTO.getDeltaMergeTreeTO() != null && !hasVmSnapshotSucceedingLastBackup) { deltaPrimaryRef.setBackupDeltaParentPath(kbossTO.getDeltaMergeTreeTO().getParent().getPath()); + } else if (hasVmSnapshotSucceedingLastBackup) { + deltaPrimaryRef.setBackupDeltaParentPath(volumeObjectTO.getPath()); } InternalBackupStoragePoolVO referenceOnPrimary = internalBackupStoragePoolDao.persist(deltaPrimaryRef); @@ -1624,49 +1606,6 @@ protected void createDeltaReferences(boolean fullBackup, boolean runningVm, Back volumeUuidToDeltaPrimaryRef.put(volumeObjectTO.getUuid(), referenceOnPrimary); } - /** - * 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 mergeCurrentDeltasIntoVolume(Volume volume, VirtualMachine virtualMachine, String operation, boolean isVmRunning) { - List currents = internalBackupJoinDao.listCurrentsByVolumeIdDesc(volume.getId()); - if (currents.isEmpty()) { - logger.debug("Volume [{}] has no deltas to merge, doing nothing.", volume.getUuid()); - return; - } - - for (InternalBackupJoinVO current : currents) { - InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(volume.getId(), current.getId()); - - DataStore store = dataStoreManager.getDataStore(volume.getPoolId(), DataStoreRole.Primary); - VolumeObject volumeObject = VolumeObject.getVolumeObject(store, (VolumeVO)volume); - - DeltaMergeTreeTO deltaMergeTreeTO = createDeltaMergeTree(true, isVmRunning, delta, (VolumeObjectTO)volumeObject.getTO(), null, new ArrayList<>()); - 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)); - } - VolumeVO volumeVO = volumeDao.findById(volumeObject.getId()); - volumeVO.setPath(deltaMergeTreeTO.getParent().getPath()); - volumeDao.update(volumeVO.getId(), volumeVO); - - volume = volumeVO; - - List deltaOnPrimary = List.of(delta); - expungeOldDeltasAndUpdateVmSnapshotOrBackup(deltaOnPrimary, null, null); - - 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.", - current.getUuid(), volume.getUuid(), operation); - setEndOfChainAndRemoveCurrentForBackup(current); - } - } - } - protected HostVO getHostToRestore(VirtualMachine vm, boolean quickRestore, Long hostId) throws AgentUnavailableException { HostVO host; if (quickRestore) { @@ -1732,59 +1671,54 @@ protected VMSnapshotVO getSucceedingVmSnapshot(InternalBackupJoinVO backup) { } /** - * Returns ordered list of backups taken after the last backup. The list is ordered from oldest to newest. + * Given a VM snapshot, returns a map of volume id to list of snapshot references of the children of the VM snapshot. * */ - protected List getSucceedingBackupList(InternalBackupJoinVO backup) { - List internalBackupJoinVOS = new ArrayList<>(); - if (backup == null) { - return internalBackupJoinVOS; - } - - List currentBackups = internalBackupJoinDao.listCurrents(backup.getVmId(), false); - if (currentBackups.isEmpty()) { - return internalBackupJoinVOS; + 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()); } - internalBackupJoinVOS = currentBackups.stream().filter(internalBackupJoinVO -> internalBackupJoinVO.getDate().after(backup.getDate())).collect(Collectors.toList()); - logger.debug("Found the following backups that succeed the backup [{}]: [{}].", backup.getUuid(), internalBackupJoinVOS); - - return internalBackupJoinVOS; + return volumeToSnapshotRefs; } /** - * Given a list of volumes and VM snapshots/backups, maps the volumes to the delta references of the VM snapshots/backups. + * Given a list of volumes and VM snapshots, maps the volumes to the snapshot references of the VM snapshots. * */ - protected Map> mapVolumesToVmSnapshotAndBackupReferences(List volumeObjectTOs, List vmSnapshotVOList, List internalBackupJoinVOList) { - Map> volumeToSnapshotAndBackupRefs = new HashMap<>(); - if (vmSnapshotVOList.isEmpty() && internalBackupJoinVOList.isEmpty()) { - logger.trace("No VM snapshot nor backup to map to any volume, returning."); - return volumeToSnapshotAndBackupRefs; - } - - List> volumeIdAndResourcePathAndCreatedDateList = new ArrayList<>(); - for (InternalBackupJoinVO internalBackupJoinVO : internalBackupJoinVOList) { - volumeIdAndResourcePathAndCreatedDateList.add(new Ternary<>(internalBackupJoinVO.getVolumeId(), internalBackupJoinVO.getStoragePoolDeltaPath(), internalBackupJoinVO.getDate())); + 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) { - vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVO.getId()) - .forEach(snapshotDataStoreVO -> volumeIdAndResourcePathAndCreatedDateList.add(new Ternary<>(snapshotDataStoreVO.getVolumeId(), snapshotDataStoreVO.getInstallPath(), snapshotDataStoreVO.getCreated()))); - } - - volumeIdAndResourcePathAndCreatedDateList.sort(Comparator.comparing(Ternary::third)); - - for (Ternary volumeIdAndResourcePathAndCreatedDate : volumeIdAndResourcePathAndCreatedDateList) { - long volumeId = volumeIdAndResourcePathAndCreatedDate.first(); - String resourcePath = volumeIdAndResourcePathAndCreatedDate.second(); - - volumeToSnapshotAndBackupRefs.computeIfAbsent(volumeId, k -> new LinkedList<>()).addLast(resourcePath); + allRefs.addAll(vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshotVO.getId())); } - - logger.trace("Given volume objects [{}], VM snapshots [{}] and backups [{}], created the following map [{}].", volumeObjectTOs, vmSnapshotVOList, internalBackupJoinVOList, volumeToSnapshotAndBackupRefs); - return volumeToSnapshotAndBackupRefs; + 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() @@ -1827,68 +1761,30 @@ protected long updateDeltaReferencesAndCalculateBackupPhysicalSize(VolumeObjectT } /** - * Expunge the old backup deltas and if there were disk-only VM snapshot or backup deltas after the last backup, update their paths. + * Expunge the old backup deltas and if there were disk-only VM snapshot deltas after the last backup, update their paths. * */ - protected void expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(List oldDeltasOnPrimary, VMSnapshot vmSnapshot, - InternalBackupJoinVO lastBackup) { + protected void expungeOldDeltasAndUpdateVmSnapshotIfNeeded(List oldDeltasOnPrimary, VMSnapshot vmSnapshot) { List snapshotRefs = vmSnapshot == null ? List.of() : vmSnapshotHelper.getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(vmSnapshot.getId()); - List newBackupDeltas = new ArrayList<>(); - Map volumeIdNewBackupDeltaMap = new HashMap<>(); - - if (lastBackup != null) { - newBackupDeltas = internalBackupStoragePoolDao.listByBackupId(lastBackup.getId()); - volumeIdNewBackupDeltaMap = newBackupDeltas.stream().collect(Collectors.toMap(InternalBackupStoragePoolVO::getVolumeId, nbsp -> nbsp)); - } - 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) { - snapshotDataStoreVO.setInstallPath(oldBackupDelta.getBackupDeltaParentPath()); - logger.debug("Updating snapshot delta [{}] path to [{}].", snapshotDataStoreVO.getId(), oldBackupDelta.getBackupDeltaParentPath()); - snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); + if (snapshotDataStoreVO == null) { continue; } - if (lastBackup != null) { - InternalBackupStoragePoolVO newBackupDelta = volumeIdNewBackupDeltaMap.get(oldBackupDelta.getVolumeId()); - newBackupDelta.setBackupDeltaParentPath(oldBackupDelta.getBackupDeltaParentPath()); - logger.debug("Updating backup delta [{}] path to [{}].", newBackupDelta.getId(), oldBackupDelta.getBackupDeltaParentPath()); - internalBackupStoragePoolDao.update(newBackupDelta.getId(), newBackupDelta); - } + snapshotDataStoreVO.setInstallPath(oldBackupDelta.getBackupDeltaParentPath()); + logger.debug("Updating snapshot delta [{}] path to [{}].", snapshotDataStoreVO.getId(), oldBackupDelta.getBackupDeltaParentPath()); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); } } - /** - * Expunges old deltas on primary storage and updates the metadata for either - * the succeeding VM snapshot or the succeeding backup based on their chronological order. - * If only one (or neither) is provided, it proceeds with the available entities. - * - * @param oldDeltasOnPrimary The list of delta references on the primary storage to be removed; - * @param succeedingVmSnapshotVO The VM snapshot that follows the deltas being expunged; - * @param succeedingBackup The backup entity that follows the deltas being expunged. - */ - protected void expungeOldDeltasAndUpdateVmSnapshotOrBackup(List oldDeltasOnPrimary, VMSnapshot succeedingVmSnapshotVO, - InternalBackupJoinVO succeedingBackup) { - if (ObjectUtils.allNotNull(succeedingVmSnapshotVO, succeedingBackup)) { - if (succeedingVmSnapshotVO.getCreated().before(succeedingBackup.getDate())) { - expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, succeedingVmSnapshotVO, null); - } else { - expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, null, succeedingBackup); - } - } else { - expungeOldDeltasAndUpdateVmSnapshotOrBackupIfNeeded(oldDeltasOnPrimary, succeedingVmSnapshotVO, succeedingBackup); - } - } - - /** * 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, List succeedingBackupList) { + KbossTO kbossTO) { VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); InternalBackupStoragePoolVO deltaOnPrimary = deltasOnPrimary.stream() @@ -1902,12 +1798,12 @@ protected InternalBackupStoragePoolVO createDeltaMergeTreeForVolume(boolean chil logger.debug("Volume [{}] has a backup delta on primary storage [{}].", volumeObjectTO.getUuid(), deltaOnPrimary); - kbossTO.setDeltaMergeTreeTO(createDeltaMergeTree(childIsVolume, runningVm, deltaOnPrimary, volumeObjectTO, succeedingVmSnapshot, succeedingBackupList)); + kbossTO.setDeltaMergeTreeTO(createDeltaMergeTree(childIsVolume, runningVm, deltaOnPrimary, volumeObjectTO, succeedingVmSnapshot)); return deltaOnPrimary; } protected DeltaMergeTreeTO createDeltaMergeTree(boolean childIsVolume, boolean runningVm, InternalBackupStoragePoolVO deltaOnPrimary, - VolumeObjectTO volumeObjectTO, VMSnapshotVO succeedingVmSnapshot, List succeedingBackupsList) { + VolumeObjectTO volumeObjectTO, VMSnapshotVO succeedingVmSnapshot) { DataStore store = dataStoreManager.getDataStore(deltaOnPrimary.getStoragePoolId(), DataStoreRole.Primary); DataTO deltaChild; if (childIsVolume) { @@ -1917,12 +1813,11 @@ protected DeltaMergeTreeTO createDeltaMergeTree(boolean childIsVolume, boolean r } BackupDeltaTO deltaParent = new BackupDeltaTO(store.getTO(), Hypervisor.HypervisorType.KVM, deltaOnPrimary.getBackupDeltaParentPath()); - List succeedingSnapshotList = succeedingVmSnapshot != null ? vmSnapshotDao.listByParent(succeedingVmSnapshot.getId()) : new ArrayList<>(); List succeedingDeltaPaths = new ArrayList<>(); - if (succeedingVmSnapshot != null || CollectionUtils.isNotEmpty(succeedingBackupsList)) { - succeedingDeltaPaths = mapVolumesToVmSnapshotAndBackupReferences(List.of(volumeObjectTO), succeedingSnapshotList, succeedingBackupsList) - .getOrDefault(volumeObjectTO.getVolumeId(), new LinkedList<>()); + 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()); @@ -2071,7 +1966,7 @@ protected List populateDeltasToRemoveAndToMergeAndUpdateVolume VolumeObjectTO volumeObjectTO = optional.get(); if (volumesNotPartOfTheBackupBeingRestored.contains(volumeObjectTO)) { - deltasToBeMerged.add(createDeltaMergeTree(true, false, deltaOnPrimary, volumeObjectTO, null, new ArrayList<>())); + deltasToBeMerged.add(createDeltaMergeTree(true, false, deltaOnPrimary, volumeObjectTO, null)); continue; } @@ -2189,8 +2084,7 @@ protected List getVolumesThatAreNotPartOfTheBackup(List volumeTOs, HashMap volumeUuidToDeltaPrimaryRef, HashMap volumeUuidToDeltaSecondaryRef, TakeKbossBackupAnswer answer, List parentBackupDeltasOnPrimary, - VMSnapshotVO succeedingVmSnapshot, BackupVO backupVO, boolean fullBackup, VirtualMachine userVm, Long hostId, boolean endChain, boolean isolated, - InternalBackupJoinVO succeedingBackup) { + 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) { @@ -2198,7 +2092,7 @@ protected void processBackupSuccess(boolean runningVm, List volu physicalBackupSize, endChain, isolated, backupVO); } - expungeOldDeltasAndUpdateVmSnapshotOrBackup(parentBackupDeltasOnPrimary, succeedingVmSnapshot, succeedingBackup); + expungeOldDeltasAndUpdateVmSnapshotIfNeeded(parentBackupDeltasOnPrimary, succeedingVmSnapshots.isEmpty() ? null : succeedingVmSnapshots.get(0)); backupVO.setSize(physicalBackupSize); backupVO.setStatus(Backup.Status.BackedUp); @@ -2241,7 +2135,7 @@ protected void processRemovedBackups(List removedBackupIds) { * 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, VirtualMachine vm) { + 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)) { @@ -2262,7 +2156,6 @@ protected boolean processRemoveBackupFailures(boolean forced, Answer[] deleteAns 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); - vmInstanceDetailsDao.addDetail(vm.getId(), VmDetailConstants.LAST_KNOWN_STATE, vm.getState().name(), false); } for (Long failedToRemove : failedToRemoveBackupIdSet) { @@ -2369,41 +2262,17 @@ protected void handleRestoreException(Backup backup, VirtualMachine vm, Object j } 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 finishAllChains(VirtualMachine vm, List currents) { - if (currents.isEmpty()) { - logger.debug("There is no current active chain, no need to do anything."); - return true; - } - - for (InternalBackupJoinVO current : currents) { - if (!mergeCurrentBackupDeltas(current)) { - 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); - BackupVO backupVO = backupDao.findById(current.getId()); - backupVO.setStatus(Backup.Status.Error); - backupDao.update(backupVO.getId(), backupVO); - vmVO.setState(VirtualMachine.State.BackupError); - userVmDao.update(vmVO.getId(), vmVO); - - return false; - } - setEndOfChainAndRemoveCurrentForBackup(current); - } - return true; + 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, Long backupScheduleId) { - InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(vm.getId(), backupScheduleId); + 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; } - validateVmState(vm, "end backup chain"); - if (mergeCurrentBackupDeltas(current)) { setEndOfChainAndRemoveCurrentForBackup(current); return true; @@ -2418,11 +2287,8 @@ protected boolean endBackupChain(VirtualMachine vm, Long backupScheduleId) { * */ protected boolean mergeCurrentBackupDeltas(InternalBackupJoinVO backupJoinVO) { VirtualMachine userVm = userVmDao.findById(backupJoinVO.getVmId()); - - List succeedingBackupList = getSucceedingBackupList(backupJoinVO); - InternalBackupJoinVO succeedingBackup = succeedingBackupList.isEmpty() ? null : succeedingBackupList.get(0); VMSnapshotVO succeedingVmSnapshot = getSucceedingVmSnapshot(backupJoinVO); - MergeDiskOnlyVmSnapshotCommand cmd = buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(backupJoinVO, userVm, succeedingVmSnapshot, succeedingBackupList); + MergeDiskOnlyVmSnapshotCommand cmd = buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(backupJoinVO, userVm, succeedingVmSnapshot); Long hostId = vmSnapshotHelper.pickRunningHost(backupJoinVO.getVmId()); Answer answer = sendBackupCommand(hostId, cmd); @@ -2432,10 +2298,9 @@ protected boolean mergeCurrentBackupDeltas(InternalBackupJoinVO backupJoinVO) { return false; } - List deltasOnPrimary = internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()); - expungeOldDeltasAndUpdateVmSnapshotOrBackup(deltasOnPrimary, succeedingVmSnapshot, succeedingBackup); + expungeOldDeltasAndUpdateVmSnapshotIfNeeded(internalBackupStoragePoolDao.listByBackupId(backupJoinVO.getId()), succeedingVmSnapshot); - if (ObjectUtils.anyNotNull(succeedingVmSnapshot, succeedingBackup)) { + if (succeedingVmSnapshot != null) { return true; } @@ -2450,18 +2315,18 @@ protected boolean mergeCurrentBackupDeltas(InternalBackupJoinVO backupJoinVO) { } protected void createDeleteCommandsAndMergeTrees(List volumeObjectTOs, Commands commands, List deletedDeltas, - VMSnapshotVO vmSnapshotSucceedingCurrentBackup, List deltaMergeTreeTOList, InternalBackupJoinVO currentBackup) { + VMSnapshotVO vmSnapshotSucceedingCurrentBackup, List deltaMergeTreeTOList) { for (VolumeObjectTO volumeObjectTO : volumeObjectTOs) { - InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeIdAndBackupId(volumeObjectTO.getVolumeId(), currentBackup.getId()); + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeId(volumeObjectTO.getVolumeId()); if (delta == null) { continue; } - if (vmSnapshotSucceedingCurrentBackup == null) { + 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, new ArrayList<>())); + deltaMergeTreeTOList.add(createDeltaMergeTree(false, false, delta, volumeObjectTO, vmSnapshotSucceedingCurrentBackup)); } } } @@ -2495,17 +2360,16 @@ protected Pair, InternalBackupJoinVO> getParentsToBeE return new Pair<>(backupParentsToBeExpunged, lastAliveBackup); } - private MergeDiskOnlyVmSnapshotCommand buildMergeDiskOnlyVmSnapshotCommandForCurrentBackup(InternalBackupJoinVO backupJoinVO, VirtualMachine userVm, VMSnapshotVO vmSnapshot, - List succeedingBackupList) { + 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, new LinkedList<>()); - boolean childIsVolume = vmSnapshot == null && succeedingBackupList.isEmpty(); - createDeltaMergeTreeForVolume(childIsVolume, userVm.getState() == VirtualMachine.State.Running, deltasOnPrimary, vmSnapshot, kbossTO, succeedingBackupList); + 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 { @@ -2558,11 +2422,9 @@ protected List getBackupJoinParents(BackupVO backupVO, boo List ancestorBackups; if (includeRemoved) { - ancestorBackups = internalBackupJoinDao.listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(backupVO.getVmId(), backupVO.getBackupScheduleId(), - backupVO.getDate()); + ancestorBackups = internalBackupJoinDao.listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(backupVO.getVmId(), backupVO.getDate()); } else { - ancestorBackups = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getBackupScheduleId(), backupVO.getDate(), true, - false); + ancestorBackups = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getDate(), true, false); } for (int i = 0; i < ancestorBackups.size(); i++) { @@ -2589,8 +2451,7 @@ protected int getChainSizeForBackup(BackupOfferingVO offering, long zoneId) { * @return list of children, or and empty list if no children found. * */ protected List getBackupJoinChildren(BackupVO backupVO) { - List children = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getBackupScheduleId(), - backupVO.getDate(), false, true); + List children = internalBackupJoinDao.listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(backupVO.getVmId(), backupVO.getDate(), false, true); long parentId = backupVO.getId(); for (int i = 0; i < children.size(); i++) { @@ -2632,7 +2493,7 @@ protected void updateBackupStatusToBackingUp(List volumeTOs, Bac * 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(), backup.getScheduleId()); + InternalBackupJoinVO current = internalBackupJoinDao.findCurrent(backup.getVmId()); if (current != null) { backupDetailDao.removeDetail(current.getId(), CURRENT); @@ -2673,29 +2534,20 @@ protected void setBackupAsInvalidAndSendAlert(BackupVO backupVO, String msg) { backupVO.getName()), msg); } - protected void configureKbossTosForCleanup(UserVmVO userVmVO, List deltasOnPrimary, Map> volumeIdToDeltasAfterCurrent, - List deltasOnSecondary, List parentDeltasOnPrimary, List kbossTOS, boolean errorOnCreation) { + 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(); + .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; - if (errorOnCreation) { - InternalBackupStoragePoolVO parent = parentDeltasOnPrimary.stream().filter(delta -> delta.getVolumeId() == volumeObjectTO.getVolumeId()).findFirst().orElse(null); - kbossTO = new KbossTO(volumeObjectTO, parent == null ? deltaOnPrimary.getBackupDeltaParentPath() : parent.getBackupDeltaPath(), deltaOnSecondary.getBackupPath(), - volumeIdToDeltasAfterCurrent.get(volumeObjectTO.getId())); - if (parent != null) { - kbossTO.setParentDeltaPathOnPrimary(parent.getBackupDeltaParentPath()); - } - kbossTO.setOldVolumePath(volumeObjectTO.getPath()); - volumeObjectTO.setPath(deltaOnPrimary.getBackupDeltaPath()); - } else { - kbossTO = new KbossTO(volumeObjectTO, deltaOnPrimary.getBackupDeltaPath(), deltaOnSecondary.getBackupPath(), - volumeIdToDeltasAfterCurrent.get(volumeObjectTO.getId())); - kbossTO.setParentDeltaPathOnPrimary(deltaOnPrimary.getBackupDeltaParentPath()); - } + 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); } } @@ -2761,11 +2613,11 @@ protected void updateReferencesAfterPrepareForSnapshotRevert(List backupChainSize; + @Mock private DataStoreManager dataStoreManagerMock; @@ -287,7 +299,7 @@ public class KbossBackupProviderTest { private long vmId = 319832; private long volumeId = 41; - private Long backupId = 312L; + Long backupId = 312L; @Before public void setup() { @@ -359,7 +371,7 @@ public void removeVMFromBackupOfferingTestNoActiveChain() { @Test public void removeVMFromBackupOfferingTestWithActiveChain() { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(vmId, true); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); doReturn(true).when(kbossBackupProviderSpy).mergeCurrentBackupDeltas(any()); doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); @@ -369,12 +381,25 @@ public void removeVMFromBackupOfferingTestWithActiveChain() { 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(null).when(backupVoMock).getBackupScheduleId(); - doReturn(new ArrayList<>()).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + doReturn(new ArrayList<>()).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); @@ -385,9 +410,8 @@ public void getBackupJoinParentsTestIncludeRemovedEmptyList() { public void getBackupJoinParentsTestIncludeRemovedAncestorIsEndOfChain() { Date date = DateUtil.now(); doReturn(date).when(backupVoMock).getDate(); - doReturn(null).when(backupVoMock).getBackupScheduleId(); doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, null, date); + doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); @@ -398,13 +422,12 @@ public void getBackupJoinParentsTestIncludeRemovedAncestorIsEndOfChain() { public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestors() { Date date = DateUtil.now(); doReturn(date).when(backupVoMock).getDate(); - doReturn(null).when(backupVoMock).getBackupScheduleId(); 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, null, date); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); @@ -415,13 +438,12 @@ public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestors() { public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestorsNoEndOfChain() { Date date = DateUtil.now(); doReturn(date).when(backupVoMock).getDate(); - doReturn(null).when(backupVoMock).getBackupScheduleId(); 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, null, date); + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listIncludingRemovedByVmIdAndBeforeDateOrderByCreatedDesc(vmId, date); List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, true); @@ -432,13 +454,12 @@ public void getBackupJoinParentsTestIncludeRemovedAncestorMultipleAncestorsNoEnd public void getBackupJoinParentsTestNoRemovedAncestorMultipleAncestorsNoEndOfChain() { Date date = DateUtil.now(); doReturn(date).when(backupVoMock).getDate(); - doReturn(null).when(backupVoMock).getBackupScheduleId(); 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, null, date, true, + doReturn(List.of(internalBackupJoinVoMock1, internalBackupJoinVoMock2, internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listByBackedUpAndVmIdAndDateBeforeOrAfterOrderBy(vmId, date, true, false); List result = kbossBackupProviderSpy.getBackupJoinParents(backupVoMock, false); @@ -647,30 +668,32 @@ public void getSucceedingVmSnapshotListTestCurrentVmSnapshotHasParentsCreatedBef } @Test - public void mapVolumesToVmSnapshotReferencesTestVmSnapshotAndBackupVOListIsEmpty() { - kbossBackupProviderSpy.mapVolumesToVmSnapshotAndBackupReferences(List.of(), List.of(), List.of()); + public void mapVolumesToVmSnapshotReferencesTestVmSnapshotVOListIsEmpty() { + kbossBackupProviderSpy.mapVolumesToVmSnapshotReferences(List.of(), List.of()); verify(vmSnapshotHelperMock, Mockito.never()).getVolumeSnapshotsAssociatedWithKvmDiskOnlyVmSnapshot(1); } @Test - public void mapVolumesToVmSnapshotAndBackupReferencesTestVmSnapshotAndBackupVOListHasTwoElements() { + 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.mapVolumesToVmSnapshotAndBackupReferences(List.of(), List.of(vmSnapshotVoMock, vmSnapshotVoMock1), List.of()); + 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, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, - new LinkedList<>())); + 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()); } @@ -679,8 +702,8 @@ public void createDeltaReferencesTestFullBackupEndOfChain() { public void createDeltaReferencesTestIsolatedBackup() { doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); - kbossBackupProviderSpy.createDeltaReferences(true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, - new LinkedList<>())); + 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()); @@ -691,11 +714,11 @@ public void createDeltaReferencesTestIsolatedBackup() { @Test public void createDeltaReferencesTestNotFullBackupEndOfChain() { doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); - KbossTO kbossTO = new KbossTO(volumeObjectToMock, new LinkedList<>()); - doReturn(null).when(kbossBackupProviderSpy).createDeltaMergeTreeForVolume(false, true, List.of(), null, kbossTO, List.of()); + 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, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), 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); @@ -705,8 +728,8 @@ public void createDeltaReferencesTestNotFullBackupEndOfChain() { public void createDeltaReferencesTestFullBackupNotEndOfChainDoesNotHaveVmSnapshotSucceedingLastBackup() { doReturn(internalBackupDataStoreVoMock).when(internalBackupDataStoreDaoMock).persist(any()); - kbossBackupProviderSpy.createDeltaReferences(true, true, backupVoMock, List.of(), List.of(), new HashMap<>(), new HashMap<>(), null, new KbossTO(volumeObjectToMock, - new LinkedList<>())); + 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()); } @@ -769,7 +792,7 @@ public void orchestrateTakeBackupTestIsolatedBackupFailed() { assertFalse(result.first()); assertNull(result.second()); verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); - verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + 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()); } @@ -792,7 +815,7 @@ public void orchestrateTakeBackupTestIsolatedBackupSuccessWithCompression() { 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(), any()); + anyLong(), anyBoolean(), anyBoolean()); doReturn(true).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); doNothing().when(kbossBackupProviderSpy).compressBackupAsync(internalBackupJoinVoMock, 0, 0); @@ -800,9 +823,9 @@ public void orchestrateTakeBackupTestIsolatedBackupSuccessWithCompression() { assertTrue(result.first()); assertEquals(backupId, result.second()); verify(kbossBackupProviderSpy, Mockito.times(1)).setBackupAsIsolated(backupVoMock); - verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + 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(), any()); + anyLong(), anyBoolean(), anyBoolean()); verify(kbossBackupProviderSpy, Mockito.times(1)).compressBackupAsync(internalBackupJoinVoMock, 0, 0); } @@ -827,7 +850,7 @@ public void orchestrateTakeBackupTestBackupSuccessWithValidation() { 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(), any()); + anyLong(), anyBoolean(), anyBoolean()); doReturn(false).when(kbossBackupProviderSpy).offeringSupportsCompression(internalBackupJoinVoMock); doNothing().when(kbossBackupProviderSpy).validateBackupAsyncIfHasOfferingSupport(any(), anyLong(), anyLong()); @@ -836,9 +859,9 @@ public void orchestrateTakeBackupTestBackupSuccessWithValidation() { assertEquals(backupId, result.second()); verify(internalBackupStoragePoolDaoMock).listByBackupId(0); verify(internalBackupDataStoreDaoMock).listByBackupId(0); - verify(kbossBackupProviderSpy, Mockito.times(2)).createDeltaReferences(Mockito.anyBoolean(), Mockito.anyBoolean(), any(), any(), any(), any(), any(), any(), any()); + 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(), any()); + anyLong(), anyBoolean(), anyBoolean()); verify(kbossBackupProviderSpy, Mockito.times(1)).validateBackupAsyncIfHasOfferingSupport(internalBackupJoinVoMock, 0, 0); } @@ -975,7 +998,7 @@ public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithPare 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(), any()); + doReturn(false).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any()); doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); @@ -1005,7 +1028,7 @@ public void orchestrateDeleteBackupTestDeleteCurrentBackupWithNoChildrenWithPare 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(), any()); + doReturn(true).when(kbossBackupProviderSpy).processRemoveBackupFailures(anyBoolean(), any(), any(), any()); doNothing().when(kbossBackupProviderSpy).processRemovedBackups(any()); @@ -1048,6 +1071,8 @@ public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupTimeOut() throw 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()); @@ -1070,6 +1095,8 @@ public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupNullAnswers() t 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()); @@ -1080,6 +1107,7 @@ public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupNullAnswers() t 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); @@ -1092,6 +1120,8 @@ public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupAnswerFalse() t 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()); @@ -1103,6 +1133,7 @@ public void orchestrateRestoreVMFromBackupTestSameVmCurrentBackupAnswerFalse() t 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); @@ -1115,6 +1146,8 @@ public void orchestrateRestoreVMFromBackupTestSameVmQuickRestoreCurrentBackupAns 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()); @@ -1123,14 +1156,18 @@ public void orchestrateRestoreVMFromBackupTestSameVmQuickRestoreCurrentBackupAns 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); } @@ -1340,25 +1377,25 @@ public void validateBackupTestValidateWithValidationVm() { } @Test - public void finishBackupChainsTestInvalidState() { + public void finishBackupChainTestInvalidState() { doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); doReturn(VirtualMachine.State.Migrating).when(userVmVOMock).getState(); - boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); assertFalse(result); } @Test - public void finishBackupChainsTestRunningVm() { + public void finishBackupChainTestRunningVm() { doReturn(userVmVOMock).when(userVmDaoMock).findById(vmId); doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); - doReturn(true).when(kbossBackupProviderSpy).finishAllChains(eq(userVmVOMock), any()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(userVmVOMock); - boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); assertTrue(result); - verify(kbossBackupProviderSpy).finishAllChains(eq(userVmVOMock), any()); + verify(kbossBackupProviderSpy).endBackupChain(userVmVOMock); } @Test @@ -1367,7 +1404,7 @@ public void finishBackupChainTestBackupError() { doReturn(VirtualMachine.State.BackupError).when(userVmVOMock).getState(); doReturn(true).when(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); - boolean result = kbossBackupProviderSpy.finishBackupChains(virtualMachineMock); + boolean result = kbossBackupProviderSpy.finishBackupChain(virtualMachineMock); assertTrue(result); verify(kbossBackupProviderSpy).normalizeBackupErrorAndFinishChain(userVmVOMock); @@ -1375,6 +1412,8 @@ public void finishBackupChainTestBackupError() { @Test public void prepareVmForSnapshotRevertTestNoCurrentBackup() { + doReturn(null).when(internalBackupJoinDaoMock).findCurrent(vmId); + kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); verify(kbossBackupProviderSpy, never()).getSucceedingVmSnapshot(any()); @@ -1382,7 +1421,7 @@ public void prepareVmForSnapshotRevertTestNoCurrentBackup() { @Test public void prepareVmForSnapshotRevertTestCurrentBackupBeforeVmSnapshot() { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findCurrent(vmId); doReturn(Date.from(Instant.EPOCH)).when(internalBackupJoinVoMock).getDate(); doReturn(Date.from(Instant.now())).when(vmSnapshotVoMock).getCreated(); @@ -1393,12 +1432,12 @@ public void prepareVmForSnapshotRevertTestCurrentBackupBeforeVmSnapshot() { @Test (expected = CloudRuntimeException.class) public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotTimeout() throws OperationTimedoutException, AgentUnavailableException { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + 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(), any()); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList()); doThrow(OperationTimedoutException.class).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); @@ -1408,12 +1447,12 @@ public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotTimeout() @Test (expected = CloudRuntimeException.class) public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotNullAnswer() throws OperationTimedoutException, AgentUnavailableException { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + 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(), any()); + doNothing().when(kbossBackupProviderSpy).createDeleteCommandsAndMergeTrees(any(), any(), any(), any(), anyList()); doReturn(null).when(kbossBackupProviderSpy).sendBackupCommands(any(), any()); kbossBackupProviderSpy.prepareVmForSnapshotRevert(vmSnapshotVoMock, virtualMachineMock); @@ -1423,12 +1462,12 @@ public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotNullAnswer @Test public void prepareVmForSnapshotRevertTestCurrentBackupAfterVmSnapshotSuccess() throws OperationTimedoutException, AgentUnavailableException { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrents(anyLong(), anyBoolean()); + 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(), any()); + 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()); @@ -1565,6 +1604,7 @@ public void validateWithValidationVmTestValidateBackupFails() throws NoTransitio 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); @@ -1628,7 +1668,7 @@ public void endBackupChainIfConfiguredTestFeatureDisabled() { kbossBackupProviderSpy.endBackupChainIfConfigured(backupVoMock); - verify(kbossBackupProviderSpy, never()).endBackupChain(any(), any()); + verify(kbossBackupProviderSpy, never()).endBackupChain(any()); } @Test @@ -1639,10 +1679,11 @@ public void endBackupChainIfConfiguredTestNotCurrentAndNoCurrentChildren() { 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(), any()); + verify(kbossBackupProviderSpy, never()).endBackupChain(any()); } @Test @@ -1651,12 +1692,13 @@ public void endBackupChainIfConfiguredTestBackupIsCurrent() { doReturn(true).when(internalBackupJoinVoMock).getCurrent(); doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); doReturn(List.of()).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); - doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); - doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any(), 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(eq(userVmVOMock), anyLong()); + verify(kbossBackupProviderSpy, times(1)).endBackupChain(userVmVOMock); } @Test @@ -1667,12 +1709,13 @@ public void endBackupChainIfConfiguredTestLastChildIsCurrent() { InternalBackupJoinVO child = mock(InternalBackupJoinVO.class); doReturn(true).when(child).getCurrent(); doReturn(List.of(child)).when(kbossBackupProviderSpy).getBackupJoinChildren(any()); - doReturn(userVmVOMock).when(userVmDaoMock).findById(anyLong()); - doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any(), 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(eq(userVmVOMock), anyLong()); + verify(kbossBackupProviderSpy, times(1)).endBackupChain(userVmVOMock); } @@ -1687,7 +1730,7 @@ public void normalizeBackupErrorAndFinishChainTestAnswerNull() { 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(), any(), any(), any(),anyBoolean()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); doReturn(null).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); @@ -1706,7 +1749,7 @@ public void normalizeBackupErrorAndFinishChainTestAnswerFailed() { 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(), any(), any(), any(), anyBoolean()); + doNothing().when(kbossBackupProviderSpy).configureKbossTosForCleanup(any(), any(), any(), anyBoolean(), any(), any()); doReturn(answerMock).when(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); doReturn(false).when(answerMock).getResult(); @@ -1717,8 +1760,6 @@ public void normalizeBackupErrorAndFinishChainTestAnswerFailed() { @Test public void normalizeBackupErrorAndFinishChainTestSuccessCallsEndChain() { - doReturn(userVmVOMock).when(userVmDaoMock).findById(any()); - doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); @@ -1728,23 +1769,21 @@ public void normalizeBackupErrorAndFinishChainTestSuccessCallsEndChain() { 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(), any(), any(), any(), anyBoolean()); + 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(), any(), any(), any()); + doReturn(false).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any()); + doReturn(true).when(kbossBackupProviderSpy).endBackupChain(any()); boolean result = kbossBackupProviderSpy.normalizeBackupErrorAndFinishChain(userVmVOMock); assertTrue(result); - verify(kbossBackupProviderSpy).mergeCurrentBackupDeltas(internalBackupJoinVoMock); - verify(kbossBackupProviderSpy).finishBackupChains(userVmVOMock); + verify(kbossBackupProviderSpy).endBackupChain(userVmVOMock); } @Test public void normalizeBackupErrorAndFinishChainTestChainAlreadyEnded() { - doReturn(userVmVOMock).when(userVmDaoMock).findById(any()); - doReturn(VirtualMachine.State.Running).when(userVmVOMock).getState(); doReturn(null).when(vmInstanceDetailsDaoMock).findDetail(anyLong(), any()); doReturn(backupVoMock).when(backupDaoMock).findLatestByStatusAndVmId(any(), anyLong()); doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(anyLong()); @@ -1754,12 +1793,12 @@ public void normalizeBackupErrorAndFinishChainTestChainAlreadyEnded() { 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(), any(), any(), any(), anyBoolean()); + 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(), any(), any(), any()); + doReturn(true).when(kbossBackupProviderSpy).processCleanupBackupErrorAnswer(any(), any()); InternalBackupJoinVO current = mock(InternalBackupJoinVO.class); - doReturn(current).when(internalBackupJoinDaoMock).findCurrent(anyLong(), any()); + doReturn(current).when(internalBackupJoinDaoMock).findCurrent(anyLong()); doNothing().when(internalBackupStoragePoolDaoMock).expungeByBackupId(anyLong()); doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); @@ -1992,53 +2031,57 @@ public void deleteFailedBackupTestNonFailedBackupDoesNothing() { @Test public void mergeCurrentDeltaIntoVolumeTestNoDeltaDoesNothing() { doReturn(volumeId).when(volumeVoMock).getId(); - doReturn(List.of()).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(null).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); - kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); - verify(internalBackupJoinDaoMock, times(1)).listCurrentsByVolumeIdDesc(volumeId); + verify(internalBackupStoragePoolDaoMock, times(1)).findOneByVolumeId(volumeId); verify(internalBackupJoinDaoMock, never()).findById(anyLong()); } @Test (expected = CloudRuntimeException.class) public void mergeCurrentDeltaIntoVolumeTestNullAnswer() { doReturn(volumeId).when(volumeVoMock).getId(); - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); - doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); + 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.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); - verify(kbossBackupProviderSpy, never()).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + verify(kbossBackupProviderSpy, never()).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); } } @Test public void mergeCurrentDeltaIntoVolumeTestNoSucceedingSnapshot() { doReturn(volumeId).when(volumeVoMock).getId(); - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); - doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); + 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).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); doNothing().when(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); - doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeIdAndBackupId(anyLong(), anyLong()); try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); - kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); verify(volumeDaoMock).update(volumeId, volumeVoMock); - verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); verify(kbossBackupProviderSpy).setEndOfChainAndRemoveCurrentForBackup(any()); } } @@ -2046,24 +2089,24 @@ public void mergeCurrentDeltaIntoVolumeTestNoSucceedingSnapshot() { @Test public void mergeCurrentDeltaIntoVolumeTestWithSucceedingSnapshotWithMoreDeltas() { doReturn(volumeId).when(volumeVoMock).getId(); - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(volumeId); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(volumeId); doReturn(backupId).when(internalBackupStoragePoolVoMock).getBackupId(); - doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(anyBoolean(), anyBoolean(), any(), any(), any(), any()); - doReturn(backupDeltaToMock).when(deltaMergeTreeToMock).getParent(); + 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).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + doNothing().when(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); doReturn(List.of(internalBackupStoragePoolVoMock)).when(internalBackupStoragePoolDaoMock).listByBackupId(backupId); - doReturn(volumeVoMock).when(volumeDaoMock).findById(anyLong()); - doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeIdAndBackupId(anyLong(), anyLong()); try (MockedStatic volumeObjectMockedStatic = Mockito.mockStatic(VolumeObject.class)) { when(VolumeObject.getVolumeObject(any(), any())).thenReturn(volumeObjectMock); - kbossBackupProviderSpy.mergeCurrentDeltasIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); + kbossBackupProviderSpy.mergeCurrentDeltaIntoVolume(volumeVoMock, virtualMachineMock, "detach", true); verify(kbossBackupProviderSpy).sendBackupCommand(anyLong(), any()); - verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotOrBackup(anyList(), any(), any()); + verify(volumeDaoMock, never()).update(volumeId, volumeVoMock); + verify(kbossBackupProviderSpy).expungeOldDeltasAndUpdateVmSnapshotIfNeeded(anyList(), any()); verify(kbossBackupProviderSpy, never()).setEndOfChainAndRemoveCurrentForBackup(any()); } } @@ -2143,13 +2186,90 @@ public void getHostToRestoreTestQuickRestoreWithHostDisabledThrows() throws Agen 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, null); + volumeObjectToMock, null); assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); assertTrue(result.getGrandChildren().isEmpty()); @@ -2164,7 +2284,7 @@ public void createDeltaMergeTreeTestChildIsDeltaWithoutSucceedingSnapshot() { doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, true, internalBackupStoragePoolVoMock, - volumeObjectToMock, null, null); + volumeObjectToMock, null); assertEquals(volumeObjectToMock, result.getVolumeObjectTO()); assertEquals("parent-path", result.getParent().getPath()); @@ -2180,14 +2300,16 @@ public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotReferences doReturn("parent-path").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); doReturn("child-path").when(internalBackupStoragePoolVoMock).getBackupDeltaPath(); doReturn(volumeId).when(volumeObjectToMock).getVolumeId(); - doReturn("path").when(volumeObjectToMock).getPath(); + doReturn("snapshot-grandchild").when(snapshotRefMock).getInstallPath(); - DeltaMergeTreeTO result = kbossBackupProviderSpy.createDeltaMergeTree(false, false, internalBackupStoragePoolVoMock, - volumeObjectToMock, vmSnapshotVoMock, List.of()); + 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("path", result.getGrandChildren().get(0).getPath()); + assertEquals("snapshot-grandchild", result.getGrandChildren().get(0).getPath()); } @Test @@ -2197,8 +2319,11 @@ public void createDeltaMergeTreeTestChildIsDeltaWithSucceedingSnapshotButNoRefer 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, List.of()); + volumeObjectToMock, vmSnapshotVoMock); assertEquals(1, result.getGrandChildren().size()); assertEquals("/volume/path", result.getGrandChildren().get(0).getPath()); @@ -2275,16 +2400,14 @@ public void populateDeltasToRemoveAndToMergeAndUpdateVolumePathsTestVolumeIsPart Set deltasToRemove = new java.util.HashSet<>(); - doReturn(deltaMergeTreeToMock).when(kbossBackupProviderSpy).createDeltaMergeTree(eq(true), eq(false), eq(internalBackupStoragePoolVoMock), eq(volumeObjectToMock), eq(null), - eq(new ArrayList<>())); + 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(eq(true), eq(false), eq(internalBackupStoragePoolVoMock), eq(volumeObjectToMock), eq(null), - eq(new ArrayList<>())); + verify(kbossBackupProviderSpy, times(1)).createDeltaMergeTree(true, false, internalBackupStoragePoolVoMock, volumeObjectToMock, null); verify(dataStoreManagerMock, never()).getDataStore(anyLong(), eq(DataStoreRole.Primary)); } @@ -2384,7 +2507,7 @@ public void processRemoveBackupFailuresTestNoFailuresReturnsTrueAndRemovesNothin List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); - boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, deleteAnswers, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, deleteAnswers, removedBackupIds, internalBackupJoinVoMock); assertTrue(result); assertEquals(List.of(backupId, 200L), removedBackupIds); @@ -2403,11 +2526,10 @@ public void processRemoveBackupFailuresTestFailureOnCurrentBackupNotForcedSetsEr doReturn(backupId).when(backupVoMock).getId(); doReturn(backupVoMock).when(backupDaoMock).findByIdIncludingRemoved(backupId); - doReturn(VirtualMachine.State.Stopped).when(virtualMachineMock).getState(); List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); - boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); assertFalse(result); assertEquals(List.of(200L), removedBackupIds); @@ -2426,7 +2548,7 @@ public void processRemoveBackupFailuresTestFailureOnCurrentBackupForcedSetBackup List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); - boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(true, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(true, new Answer[]{failedCurrentBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); assertFalse(result); assertEquals(List.of(200L), removedBackupIds); @@ -2449,7 +2571,7 @@ public void processRemoveBackupFailuresTestFailureOnOtherBackupMarksItExpunged() List removedBackupIds = new ArrayList<>(List.of(backupId, 200L)); - boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedOtherBackupAnswer}, removedBackupIds, internalBackupJoinVoMock, virtualMachineMock); + boolean result = kbossBackupProviderSpy.processRemoveBackupFailures(false, new Answer[]{failedOtherBackupAnswer}, removedBackupIds, internalBackupJoinVoMock); assertFalse(result); assertEquals(List.of(backupId), removedBackupIds); 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 d73efc51be1a..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 @@ -550,7 +550,7 @@ protected Host getVMHypervisorHostForBackup(VirtualMachine vm) { } @Override - public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long scheduleId) { + public Pair takeBackup(final VirtualMachine vm, Boolean quiesceVM, boolean isolated) { final Host host = getVMHypervisorHostForBackup(vm); final BackupRepository backupRepository = backupRepositoryDao.findByBackupOfferingId(vm.getBackupOfferingId()); 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 09eb877e0ab1..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 @@ -251,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, false, null); + Pair result = nasBackupProvider.takeBackup(vm, false, false); Assert.assertTrue(result.first()); Assert.assertNotNull(result.second()); 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 31186385d578..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 @@ -492,7 +492,7 @@ public Pair restoreBackedUpVolume(Backup backup, Backup.VolumeI } @Override - public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated, Long scheduleId) { + public Pair takeBackup(VirtualMachine vm, Boolean quiesceVM, boolean isolated) { String networkerServer; String clusterName; 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 361b3349b011..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, boolean isolated, Long scheduleId) { + 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); 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 index 8ca17fc0c6cf..430d1c6ea3c2 100644 --- 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 @@ -16,16 +16,17 @@ // under the License. package com.cloud.hypervisor.kvm.resource.wrapper; -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.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +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; @@ -39,19 +40,12 @@ import org.libvirt.Error; import org.libvirt.LibvirtException; -import com.cloud.agent.api.Answer; -import com.cloud.agent.api.to.DataTO; -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 com.cloud.utils.exception.CloudRuntimeException; +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 { @@ -64,14 +58,14 @@ public Answer execute(CleanupKbossBackupErrorCommand command, LibvirtComputingRe cleanupBackupDeltasOnSecondary(command, storagePoolManager, kbossTOS); if (command.isRunningVM()) { - Pair>, Boolean> volumeTosAndIsVmRunning = cleanupRunningVm(command, serverResource); + 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) { + private Pair, Boolean> cleanupRunningVm(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { Domain dm = null; try { dm = serverResource.getDomain(serverResource.getLibvirtUtilitiesHelper().getConnection(), command.getVmName()); @@ -81,7 +75,7 @@ private Pair>, Boolean> cleanupRunningVm(Cleanu return new Pair<>(mergeDeltasForStoppedVmIfNeeded(command, serverResource), false); } logger.error("Error while trying to get VM [{}]. Aborting the process.", command.getVmName(), e); - return new Pair<>(Map.of(), false); + return new Pair<>(List.of(), false); } finally { if (dm != null) { try { @@ -93,140 +87,87 @@ private Pair>, Boolean> cleanupRunningVm(Cleanu } } - private Map> mergeDeltasForStoppedVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { - HashMap> volumeToChainEnded = new HashMap<>(); + 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 volumePathMissing = !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(volumeObjectTO.getPath()))); - boolean deltaPathMissing = !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getDeltaPathOnPrimary()))); - boolean basePathMissing = kbossTO.getParentDeltaPathOnPrimary() != null && !Files.exists(Path.of(kvmStoragePool.getLocalPathFor(kbossTO.getParentDeltaPathOnPrimary()))); - List grandchildren = kbossTO.getDeltaPaths().isEmpty() ? List.of() : List.of(new BackupDeltaTO(volumeObjectTO.getDataStore(), - Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPaths().get(0))); - - Boolean chainEnded = mergeDeltaIfNeeded(serverResource, kbossTO, volumeObjectTO, grandchildren, volumePathMissing, deltaPathMissing, basePathMissing, - command.isErrorOnCreate(), false, command.isTopDelta(), command.isEndOfChain()); - volumeToChainEnded.put(volumeObjectTO.getUuid(), new Pair<>(volumeObjectTO.getPath(), chainEnded)); + 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 volumeToChainEnded; + return volumeObjectTOList; } - private Map> mergeDeltasForRunningVmIfNeeded(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource, Domain dm) throws LibvirtException { - HashMap> volumeIdToPathAndChainEnded = new HashMap<>(); + 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()) { - VolumeObjectTO volumeObjectTO = kbossTO.getVolumeObjectTO(); - String volumePath = volumeObjectTO.getPath(); LibvirtVMDef.DiskDef diskDef = parser.getDisks().stream() - .filter(disk -> hasPath(disk, volumePath, kbossTO.getDeltaPathOnPrimary(), kbossTO.getParentDeltaPathOnPrimary())) - .findFirst().orElse(null); + .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.", volumeObjectTO.getUuid()); - return Map.of(); + logger.warn("Volume [{}] does not match any record we have. This must be manually normalized.", kbossTO.getVolumeObjectTO().getUuid()); + return List.of(); } - List backingStoreList = diskDef.getBackingStoreList(); - backingStoreList.add(0, diskDef.getDiskPath()); - - boolean volumePathMissing = true; - boolean deltaPathMissing = true; - boolean basePathMissing = kbossTO.getParentDeltaPathOnPrimary() != null; - for (String delta : backingStoreList) { - if (StringUtils.contains(delta, volumePath)) { - volumePathMissing = false; - } - if (StringUtils.contains(delta, kbossTO.getDeltaPathOnPrimary())) { - deltaPathMissing = false; - } - if (StringUtils.contains(delta, kbossTO.getParentDeltaPathOnPrimary())) { - basePathMissing = false; - } - } + 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())); - Boolean chainEnded = mergeDeltaIfNeeded(serverResource, kbossTO, volumeObjectTO, List.of(), volumePathMissing, deltaPathMissing, basePathMissing, - command.isErrorOnCreate(), true, command.isTopDelta(), command.isEndOfChain()); - volumeIdToPathAndChainEnded.put(volumeObjectTO.getUuid(), new Pair<>(volumeObjectTO.getPath(), chainEnded)); + mergeDeltaIfNeeded(serverResource, kbossTO, backupErrorDeltaExists, parentBackupDeltaExists, shouldBaseDeltaExist, baseDeltaExists, true, + kbossTO.getVolumeObjectTO(), volumeObjectTOList); } - return volumeIdToPathAndChainEnded; + return volumeObjectTOList; } - private boolean hasPath(LibvirtVMDef.DiskDef diskDef, String... paths) { - List chain = diskDef.getBackingStoreList(); - chain = chain != null ? chain : new ArrayList<>(); - chain.add(diskDef.getDiskPath()); - for (String delta : chain) { - if (Arrays.stream(paths).anyMatch(path -> StringUtils.contains(delta, path))) { + 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; - } - } - return false; - } - - /** - * @return True if error chain is already ended, false otherwise. - * */ - private boolean mergeDeltaIfNeeded(LibvirtComputingResource serverResource, KbossTO kbossTO, VolumeObjectTO volumeObjectTO, List grandChildren, - boolean volumePathMissing, boolean deltaPathMissing, boolean basePathMissing, boolean errorOnCreate, boolean runningVm, boolean isTopDelta, boolean isEndOfChain) { - String errorMessage = String.format("Volume [%s] is inconsistent in an anomalous way. We cannot normalize it automatically.", volumeObjectTO.getUuid()); - if (!errorOnCreate) { - // Base should never be missing if it is not an error from creation. If the volume path is missing and it is not the delta that was being removed, it is an anomaly as well. - if (basePathMissing || (volumePathMissing && !isTopDelta)) { - logger.warn(errorMessage); - throw new CloudRuntimeException(String.format ("Unable to find the base delta or the volume path was not found. We cannot normalize it automatically. At least " + - "one of these should exist: volume [%s]; base path [%s].", volumeObjectTO.getPath(), kbossTO.getParentDeltaPathOnPrimary())); - } - // This means that the delta merge likely succeeded but the host was unable to reply to the Management Server - if (deltaPathMissing) { - // This is if the delta being merged was the top delta. Then we must update its path. - if (volumePathMissing) { - volumeObjectTO.setPath(kbossTO.getParentDeltaPathOnPrimary()); - } + } 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; } - return false; - } - - DeltaMergeTreeTO deltaMergeTreeTO; - boolean errorChainFinished; - if (volumePathMissing && !deltaPathMissing) { // The process was not started for this volume - DataTO child; - // If it is the top delta, we should set the volume path as the delta path on primary, as it is the real path. This will get updated later after being merged. - if (isTopDelta) { - volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); - child = volumeObjectTO; - } else { // Otherwise, we set it as the old path of the volume. In this case, this will be its final path. - volumeObjectTO.setPath(kbossTO.getOldVolumePath()); - child = new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPathOnPrimary()); - } - logger.debug("Volume [{}] is consistent, the backup process for it was not started. Its current path is [{}]. We will merge the old backup chain.", - volumeObjectTO.getUuid(), volumeObjectTO.getPath()); + } 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.getParentDeltaPathOnPrimary()), child, grandChildren); - errorChainFinished = true; - } else if (!isEndOfChain && !volumePathMissing && (deltaPathMissing || kbossTO.getParentDeltaPathOnPrimary() == null)) { // The process was completed for this volume - logger.debug("Volume [{}] is consistent, the backup process was completed for it. Its current path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); - return false; - } else if (isEndOfChain && volumePathMissing && !basePathMissing) { // The process was completed for this volume - volumeObjectTO.setPath(kbossTO.getParentDeltaPathOnPrimary()); - logger.debug("Volume [{}] is consistent, the backup process was completed for it. Its current path is [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getPath()); - return true; - } else if (!volumePathMissing && !deltaPathMissing) { // The process stopped midway - logger.debug("Volume [{}] is inconsistent, but we can normalize it. We will merge the delta created by the last backup with the base volume.", + 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()), new BackupDeltaTO(volumeObjectTO.getDataStore(), Hypervisor.HypervisorType.KVM, kbossTO.getDeltaPathOnPrimary()), - grandChildren); - errorChainFinished = false; - isTopDelta = false; + kbossTO.getParentDeltaPathOnPrimary()), volumeObjectTO, List.of()); } else { - logger.warn(errorMessage); - throw new CloudRuntimeException(errorMessage + " Maybe it is a good idea to open an issue to get help on this."); + logger.warn("Volume [{}] is inconsistent in an anomalous way. We cannot normalize it automatically.", volumeObjectTO.getUuid()); + return false; } try { @@ -235,19 +176,15 @@ private boolean mergeDeltaIfNeeded(LibvirtComputingResource serverResource, Kbos } else { serverResource.mergeDeltaForStoppedVm(deltaMergeTreeTO); } - if (isTopDelta) { - volumeObjectTO.setPath(deltaMergeTreeTO.getParent().getPath()); - } - return errorChainFinished; + 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); - throw new CloudRuntimeException(ex); + return false; } } - /** - * Checks if the VM is really stopped by checking if its root volume has had any writes on the last 30 seconds. - * */ private boolean isVmReallyStopped(CleanupKbossBackupErrorCommand command, LibvirtComputingResource serverResource) { VolumeObjectTO volume = command.getKbossTOs().stream() .filter(kbossTO -> kbossTO.getVolumeObjectTO().getDeviceId() == 0) 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 865d2bfb1e50..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 @@ -57,7 +57,6 @@ 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 static com.cloud.hypervisor.kvm.storage.KVMStorageProcessor.poolTypesToDeleteChainInfo; @@ -182,12 +181,10 @@ protected void revertVolumeToSnapshot(KVMStoragePool kvmStoragePoolSecondary, Sn try { replaceVolumeWithSnapshot(volumePath, snapshotPath); - if (CollectionUtils.isNotEmpty(volumeObjectTo.getDeltasToRemove()) && poolTypesToDeleteChainInfo.contains(kvmStoragePoolPrimary.getType()) && + if (volumeObjectTo.getChainInfo() != null && poolTypesToDeleteChainInfo.contains(kvmStoragePoolPrimary.getType()) && volumeObjectTo.getFormat() == Storage.ImageFormat.QCOW2 && deleteChain) { - for (String deltaPath : volumeObjectTo.getDeltasToRemove()) { - logger.debug("Deleting leftover backup delta at [{}].", deltaPath); - kvmStoragePoolPrimary.deletePhysicalDisk(deltaPath, volumeObjectTo.getFormat()); - } + 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) { 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 index 5ff9bbaaad0f..d2332f4f99b1 100644 --- 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 @@ -40,7 +40,6 @@ import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; import org.libvirt.LibvirtException; import java.io.File; @@ -144,8 +143,8 @@ protected void cleanupVm(TakeKbossBackupCommand command, LibvirtComputingResourc volumeObjectTO.setPath(kbossTO.getDeltaPathOnPrimary()); if (deltaMergeTreeTO != null) { - List snapshotDataStoreVos = kbossTO.getDeltaPaths(); - mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVM, volumeUuid, CollectionUtils.isEmpty(snapshotDataStoreVos)); + List snapshotDataStoreVos = kbossTO.getVmSnapshotDeltaPaths(); + mergeBackupDelta(resource, deltaMergeTreeTO, volumeObjectTO, vmName, runningVM, volumeUuid, snapshotDataStoreVos.isEmpty()); } if (command.isEndChain() || command.isIsolated()) { @@ -169,7 +168,7 @@ protected Pair copyBackupDeltaToSecondary(KVMStoragePoolManager st int waitInMillis) { VolumeObjectTO delta = kbossTO.getVolumeObjectTO(); String parentDeltaPathOnSecondary = kbossTO.getPathBackupParentOnSecondary(); - List deltaPathsToCopy = ObjectUtils.defaultIfNull(kbossTO.getDeltaPaths(), new ArrayList<>()); + List deltaPathsToCopy = CollectionUtils.isEmpty(kbossTO.getVmSnapshotDeltaPaths()) ? new ArrayList<>() : new ArrayList<>(kbossTO.getVmSnapshotDeltaPaths()); deltaPathsToCopy.add(delta.getPath()); KVMStoragePool parentImagePool = null; 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 b1d43286b725..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 @@ -99,7 +99,6 @@ import org.apache.cloudstack.utils.qemu.QemuObject.EncryptFormat; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.commons.collections.MapUtils; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.BooleanUtils; @@ -2907,11 +2906,9 @@ public Answer deleteVolume(final DeleteCommand cmd) { } } pool.deletePhysicalDisk(vol.getPath(), vol.getFormat()); - if (CollectionUtils.isNotEmpty(vol.getDeltasToRemove()) && poolTypesToDeleteChainInfo.contains(pool.getType()) && vol.getFormat() == ImageFormat.QCOW2 && cmd.isDeleteChain()) { - for (String deltaPath : vol.getDeltasToRemove()) { - logger.debug("Deleting leftover backup delta at [{}].", deltaPath); - pool.deletePhysicalDisk(deltaPath, 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) { 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 index b726b92c7424..8354993e61a1 100644 --- 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 @@ -266,7 +266,7 @@ public void copyBackupDeltaToSecondaryTest() throws LibvirtException, QemuImgExc doReturn(volumePath).when(volumeObjectToMock1).getPath(); doReturn(volUuid1).when(volumeObjectToMock1).getUuid(); doReturn(parentPath).when(kbossTO1).getPathBackupParentOnSecondary(); - doReturn(new ArrayList<>(List.of(deltaPath2))).when(kbossTO1).getDeltaPaths(); + 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); diff --git a/plugins/storage/volume/ontap/README.md b/plugins/storage/volume/ontap/README.md index e7e066aafb55..059bf99e09d8 100644 --- a/plugins/storage/volume/ontap/README.md +++ b/plugins/storage/volume/ontap/README.md @@ -36,15 +36,15 @@ The NetApp ONTAP Storage Plugin provides integration between Apache CloudStack a ### Component Structure -| Package | Description | -|---------|-------------------------------------------------------| -| `driver` | Primary datastore driver implementation | +| Package | Description | +| --- | --- | +| `driver` | Primary datastore driver implementation | | `feign` | REST API clients and data models for ONTAP operations | -| `lifecycle` | Storage pool lifecycle management | -| `listener` | Host connection event handlers | -| `provider` | Main provider and strategy factory | -| `service` | ONTAP Storage strategy implementations (NAS/SAN) | -| `utils` | Constants and helper utilities | +| `lifecycle` | Storage pool lifecycle management | +| `listener` | Host connection event handlers | +| `provider` | Main provider and strategy factory | +| `service` | ONTAP Storage strategy implementations (NAS/SAN) | +| `utils` | Constants and helper utilities | ## Requirements @@ -74,7 +74,7 @@ ONTAP requires a minimum volume size of **1.56 GB** (1,677,721,600 bytes). The p When creating an ONTAP primary storage pool, provide the following details in the URL field (semicolon-separated key=value pairs): | Parameter | Required | Description | -|-----------|----------|-------------| +| --- | --- | --- | | `username` | Yes | ONTAP cluster admin username | | `password` | Yes | ONTAP cluster admin password | | `svmName` | Yes | Storage Virtual Machine name | @@ -90,7 +90,7 @@ username=admin;password=secretpass;svmName=svm1;protocol=ISCSI;managementLIF=192 ## Port Configuration | Protocol | Default Port | -|----------|--------------| +| --- | --- | | NFS | 2049 | | iSCSI | 3260 | | ONTAP Management API | 443 (HTTPS) | diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java index 09519c5641cb..464514f58ce1 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java @@ -23,6 +23,7 @@ import javax.inject.Inject; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.auth.UserAuthenticator; import org.apache.commons.collections.CollectionUtils; @@ -170,7 +171,12 @@ Pair authenticate(String username, String if (mappedAccount == null || mappedAccount.getRemoved() != null) { throw new CloudRuntimeException("Mapped account for users does not exist. Please contact your administrator."); } - _accountManager.moveUser(userAccount.getId(), userAccount.getDomainId(), mappedAccount); + try { + _accountManager.moveUser(userAccount.getId(), userAccount.getDomainId(), mappedAccount); + } catch (ResourceAllocationException e) { + throw new CloudRuntimeException(String.format("Failed to move User [%s] to mapped Account [%s] due to insufficient Project limits.", + userAccount.getUsername(), mappedAccount.getAccountName()), e); + } } // else { the user hasn't changed in ldap, the ldap group stayed the same, hurray, pass, fun thou self a lot of fun } } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java index 79274ba904b1..956e6fca00af 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java @@ -70,10 +70,10 @@ public class RegisterOAuthProviderCmd extends BaseCmd { description = "Domain path for domain-specific OAuth provider. Ignored when Domain ID is passed.", since = "4.23.0") private String domainPath; - @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keycloak provider)") + @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keycloak provider)", since = "4.23.0") private String authorizeUrl; - @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for keycloak provider)") + @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for keycloak provider)", since = "4.23.0") private String tokenUrl; @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java index f32c08e048eb..f6e60caaade7 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java @@ -61,10 +61,10 @@ public final class UpdateOAuthProviderCmd extends BaseCmd { @Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider") private String redirectUri; - @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL pre-registered in the specific OAuth provider") + @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL pre-registered in the specific OAuth provider", since = "4.23.0") private String authorizeUrl; - @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL pre-registered in the specific OAuth provider") + @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL pre-registered in the specific OAuth provider", since = "4.23.0") private String tokenUrl; @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "OAuth provider will be enabled or disabled based on this value") diff --git a/server/src/main/java/com/cloud/projects/ProjectManager.java b/server/src/main/java/com/cloud/projects/ProjectManager.java index 5f58205208be..17a443befaec 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManager.java +++ b/server/src/main/java/com/cloud/projects/ProjectManager.java @@ -19,6 +19,7 @@ import java.util.List; import com.cloud.user.Account; +import com.cloud.user.User; import org.apache.cloudstack.framework.config.ConfigKey; public interface ProjectManager extends ProjectService { @@ -47,6 +48,8 @@ public interface ProjectManager extends ProjectService { long getInvitationTimeout(); + boolean cleanupProjectsForUser(Project project, User user); + public static final String MESSAGE_CREATE_TUNGSTEN_PROJECT_EVENT = "Message.CreateTungstenProject.Event"; public static final String MESSAGE_DELETE_TUNGSTEN_PROJECT_EVENT = "Message.DeleteTungstenProject.Event"; diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index 92af441d06b9..9ba5402443ef 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -617,6 +617,37 @@ public boolean addUserToProject(Long projectId, String username, String email, L } } + /** + * Transfers all project associations and project invitations from one user to another. + * + * @param oldUser the user whose project associations are being transferred + * @param newUser the user to whom the project associations are being transferred + * @throws ResourceAllocationException if there is an issue with allocating the required project resources to the new user + */ + @Override + public void moveProjectAssociationsToUser(User oldUser, User newUser) throws ResourceAllocationException { + _projectInvitationDao.move(oldUser, newUser); + + List projectAccounts = _projectAccountDao.listBy(null, oldUser.getAccountId(), oldUser.getId()); + if (projectAccounts.isEmpty()) { + return; + } + + Account oldAccount = _accountDao.findById(oldUser.getAccountId()); + Account newAccount = _accountDao.findById(newUser.getAccountId()); + long requiredProjectsAmount = oldAccount.getId() != newAccount.getId() + ? projectAccounts.stream().filter(pa -> pa.getAccountRole() == ProjectAccount.Role.Admin).count() + : 0L; + + try (CheckedReservation projectReservation = new CheckedReservation(newAccount, ResourceType.project, null, null, requiredProjectsAmount, reservationDao, _resourceLimitMgr)) { + _projectAccountDao.move(oldUser, newUser); + if (requiredProjectsAmount > 0) { + _resourceLimitMgr.incrementResourceCount(newAccount.getId(), ResourceType.project, requiredProjectsAmount); + _resourceLimitMgr.decrementResourceCount(oldAccount.getId(), ResourceType.project, requiredProjectsAmount); + } + } + } + @Override public Project findByNameAndDomainId(String name, long domainId) { return _projectDao.findByNameAndDomain(name, domainId); @@ -1033,50 +1064,41 @@ public boolean deleteUserFromProject(long projectId, long userId) { //verify permissions _accountMgr.checkAccess(caller, AccessType.ModifyProject, true, _accountMgr.getAccount(project.getProjectAccountId())); - //Check if the user exists in the project - ProjectAccount projectUser = _projectAccountDao.findByProjectIdUserId(projectId, user.getAccountId(), user.getId()); - if (projectUser == null) { - deletePendingInvite(projectId, user); + boolean success = cleanupProjectsForUser(project, user); + if (!success) { InvalidParameterValueException ex = new InvalidParameterValueException("User " + user.getUsername() + " is not assigned to the project with specified id"); - // Use the projectVO object and not the projectAccount object to inject the projectId. ex.addProxyObject(project.getUuid(), "projectId"); throw ex; } - return deleteUserFromProject(projectId, user); + return true; } - private void deletePendingInvite(Long projectId, User user) { - ProjectInvitation invite = _projectInvitationDao.findByUserIdProjectId(user.getId(), user.getAccountId(), projectId); - if (invite != null) { - boolean success = _projectInvitationDao.remove(invite.getId()); - if (success){ - logger.info("Successfully deleted invite pending for the user : {}", user); - } else { - logger.info("Failed to delete project invite for user: {}", user); - } - } - } + /** + * Cleans up project associations and invitations for a specified user in a given project. + * + * @param project the project from which the user is being cleaned up; if null, cleanup applies to all projects associated with the user + * @param user the user whose project associations and invitations are being cleaned up + * @return true if any project accounts associated with the user were removed, false otherwise + */ + @Override + public boolean cleanupProjectsForUser(Project project, User user) { + return Transaction.execute((TransactionCallback) status -> { + Long projectId = project != null ? project.getId() : null; + long userId = user.getId(); + long accountId = user.getAccountId(); - @DB - private boolean deleteUserFromProject(Long projectId, User user) { - return Transaction.execute(new TransactionCallback() { - @Override - public Boolean doInTransaction(TransactionStatus status) { - boolean success = true; - ProjectAccountVO projectAccount = _projectAccountDao.findByProjectIdUserId(projectId, user.getAccountId(), user.getId()); - success = _projectAccountDao.remove(projectAccount.getId()); + _projectInvitationDao.removeBy(projectId, accountId, userId); + + List projectAccounts = _projectAccountDao.listBy(projectId, accountId, userId); + for (ProjectAccountVO projectAccount : projectAccounts) { + _projectAccountDao.remove(projectAccount.getId()); if (projectAccount.getAccountRole() == Role.Admin) { - _resourceLimitMgr.decrementResourceCount(user.getAccountId(), ResourceType.project); + _resourceLimitMgr.decrementResourceCount(accountId, ResourceType.project); } - if (success) { - logger.debug("Removed user {} from project. Removing any invite sent to the user", user); - ProjectInvitation invite = _projectInvitationDao.findByUserIdProjectId(user.getId(), user.getAccountId(), projectId); - if (invite != null) { - success = success && _projectInvitationDao.remove(invite.getId()); - } - } - return success; + logger.debug("Removed user [{}] from project [{}].", user, projectAccount.getProjectId()); } + + return !projectAccounts.isEmpty(); }); } diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index eca1a571dd88..e3840e297254 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; @@ -148,7 +149,7 @@ void buildACLViewSearchCriteria(SearchCriteria s * moves a user to another account within the same domain * @return true if the user was successfully moved */ - boolean moveUser(MoveUserCmd moveUserCmd); + boolean moveUser(MoveUserCmd moveUserCmd) throws ResourceAllocationException; @Override UserAccount updateUser(UpdateUserCmd cmd); @@ -190,7 +191,7 @@ void buildACLViewSearchCriteria(SearchCriteria s ConfigKey UseSecretKeyInResponse = new ConfigKey("Advanced", Boolean.class, "use.secret.key.in.response", "false", "This parameter allows the users to enable or disable of showing secret key as a part of response for various APIs. By default it is set to false.", true); - boolean moveUser(long id, Long domainId, Account newAccount); + boolean moveUser(long id, Long domainId, Account newAccount) throws ResourceAllocationException; UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(final Long domainId, final Long userAccountId); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index db9c1d1dafde..e4d835f74b11 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -45,10 +45,13 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.projects.dao.ProjectInvitationDao; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.SSHKeyPairDao; import com.cloud.user.dao.UserAccountDao; import com.cloud.user.dao.UserDao; +import com.cloud.utils.db.TransactionCallbackWithException; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.acl.ApiKeyPairManagerImpl; import org.apache.cloudstack.acl.ApiKeyPairPermissionVO; @@ -315,6 +318,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private ProjectAccountDao _projectAccountDao; @Inject + private ProjectInvitationDao projectInvitationDao; + @Inject private IPAddressDao _ipAddressDao; @Inject private HostDao hostDao; @@ -2521,14 +2526,29 @@ public boolean deleteUser(DeleteUserCmd deleteUserCmd) { checkAccountAndAccess(user, account); verifyCallerPrivilegeForUserOrAccountOperations(user); - removeUserApiKeys(id); + return deleteAndCleanupUser(user); + } + + /** + * Removes the specified user and performs cleanup operations associated with the user. + * + * @param user the user to be deleted and cleaned up + * @return true if the user was successfully marked as removed, false otherwise + */ + protected boolean deleteAndCleanupUser(User user) { + return Transaction.execute((TransactionCallback) status -> { + long userId = user.getId(); + + removeUserApiKeys(userId); + _projectMgr.cleanupProjectsForUser(null, user); - return _userDao.remove(id); + return _userDao.remove(userId); + }); } @Override @ActionEvent(eventType = EventTypes.EVENT_USER_MOVE, eventDescription = "moving User to a new account") - public boolean moveUser(MoveUserCmd cmd) { + public boolean moveUser(MoveUserCmd cmd) throws ResourceAllocationException { final Long id = cmd.getId(); UserVO user = getValidUserVO(id); Account oldAccount = _accountDao.findById(user.getAccountId()); @@ -2542,7 +2562,7 @@ public boolean moveUser(MoveUserCmd cmd) { } @Override - public boolean moveUser(long id, Long domainId, Account newAccount) { + public boolean moveUser(long id, Long domainId, Account newAccount) throws ResourceAllocationException { UserVO user = getValidUserVO(id); Account oldAccount = _accountDao.findById(user.getAccountId()); checkAccountAndAccess(user, oldAccount); @@ -2550,24 +2570,22 @@ public boolean moveUser(long id, Long domainId, Account newAccount) { return moveUser(user, newAccount.getId()); } - private boolean moveUser(UserVO user, long newAccountId) { + private boolean moveUser(UserVO user, long newAccountId) throws ResourceAllocationException { if (newAccountId == user.getAccountId()) { // could do a not silent fail but the objective of the user is reached return true; // no need to create a new user object for this user } - return Transaction.execute(new TransactionCallback<>() { - @Override - public Boolean doInTransaction(TransactionStatus status) { - UserVO newUser = new UserVO(user); - user.setExternalEntity(user.getUuid()); - user.setUuid(UUID.randomUUID().toString()); - _userDao.update(user.getId(), user); - newUser.setAccountId(newAccountId); - boolean success = _userDao.remove(user.getId()); - UserVO persisted = _userDao.persist(newUser); - return success && persisted.getUuid().equals(user.getExternalEntity()); - } + return Transaction.execute((TransactionCallbackWithException) status -> { + UserVO newUser = new UserVO(user); + user.setExternalEntity(user.getUuid()); + user.setUuid(UUID.randomUUID().toString()); + _userDao.update(user.getId(), user); + newUser.setAccountId(newAccountId); + UserVO persisted = _userDao.persist(newUser); + _projectMgr.moveProjectAssociationsToUser(user, persisted); + boolean success = _userDao.remove(user.getId()); + return success && persisted.getUuid().equals(user.getExternalEntity()); }); } diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 58bad20e4f1a..58e435b6406d 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -971,7 +971,6 @@ public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) { throw new InvalidParameterValueException("Could not find the requested backup schedule."); } checkCallerAccessToBackupScheduleVm(schedule.getVmId()); - finalizeBackupScheduleIfNeeded(schedule); return backupScheduleDao.remove(schedule.getId()); } @@ -979,33 +978,6 @@ public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) { return deleteAllVmBackupSchedules(vmId); } - /** - * Terminates the backup schedule if necessary. - * - * @param backupSchedule the backup schedule to be processed for termination. - * @throws CloudRuntimeException if the backup offering associated with the - * virtual machine was not found or if the backup provider could not finalize - * the backup schedule. - */ - protected void finalizeBackupScheduleIfNeeded(BackupSchedule backupSchedule) { - VMInstanceVO vm = findVmById(backupSchedule.getVmId()); - - if (vm.getBackupOfferingId() == null) { - logger.debug("The virtual machine {} backup offering has already been removed; therefore, it is not necessary to finalize the backup schedule.", vm.getUuid()); - return; - } - - BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId()); - if (backupOffering == null) { - throw new CloudRuntimeException("Could not find the backup offering of the backup schedule virtual machine."); - } - - BackupProvider backupProvider = getBackupProvider(backupOffering.getProvider()); - if (!backupProvider.removeVMBackupSchedule(vm, backupSchedule)) { - throw new CloudRuntimeException(String.format("Failed to finalize VM backup schedule with ID [%s].", backupSchedule.getUuid())); - } - } - /** * Checks if the backup framework is enabled for the zone in which the VM with specified ID is allocated and * if the caller has access to the VM. @@ -1030,7 +1002,6 @@ protected boolean deleteAllVmBackupSchedules(long vmId) { List vmBackupSchedules = backupScheduleDao.listByVM(vmId); boolean success = true; for (BackupScheduleVO vmBackupSchedule : vmBackupSchedules) { - finalizeBackupScheduleIfNeeded(vmBackupSchedule); success = success && backupScheduleDao.remove(vmBackupSchedule.getId()); } return success; @@ -1096,7 +1067,7 @@ private void createCheckedBackup(CreateBackupCmd cmd, Account owner, boolean isS CheckedReservation backupStorageReservation = new CheckedReservation(owner, Resource.ResourceType.backup_storage, backupSize, reservationDao, resourceLimitMgr)) { - Pair result = backupProvider.takeBackup(vm, cmd.getQuiesceVM(), cmd.isIsolated(), backupScheduleId); + Pair result = backupProvider.takeBackup(vm, cmd.getQuiesceVM(), cmd.isIsolated()); if (!result.first()) { throw new CloudRuntimeException("Failed to create Instance Backup"); } diff --git a/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java index 5c30188a8c41..12088c76de3c 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/InternalBackupServiceImpl.java @@ -70,7 +70,6 @@ import java.util.HashMap; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; public class InternalBackupServiceImpl extends ComponentLifecycleBase implements InternalBackupService, VmWorkJobHandler { protected Logger logger = LogManager.getLogger(getClass()); @@ -127,17 +126,16 @@ public void configureChainInfo(DataTO volumeTo, Command cmd) { return; } VolumeObjectTO volumeObjectTO = (VolumeObjectTO) volumeTo; - List backupDeltas = internalBackupStoragePoolDao.listByVolumeId(volumeObjectTO.getVolumeId()); - if (backupDeltas.isEmpty()) { + InternalBackupStoragePoolVO backupDelta = internalBackupStoragePoolDao.findOneByVolumeId(volumeObjectTO.getVolumeId()); + if (backupDelta == null) { return; } - volumeObjectTO.setDeltasToRemove(backupDeltas.stream().map(InternalBackupStoragePoolVO::getBackupDeltaParentPath).collect(Collectors.toSet())); + volumeObjectTO.setChainInfo(backupDelta.getBackupDeltaParentPath()); if (cmd instanceof DeleteCommand) { ((DeleteCommand) cmd).setDeleteChain(true); - } else if (cmd instanceof RevertSnapshotCommand) { + } + if (cmd instanceof RevertSnapshotCommand) { ((RevertSnapshotCommand) cmd).setDeleteChain(true); - } else { - return; } logger.debug("Configured chain info for volume [{}]. Set it as [{}].", volumeObjectTO.getUuid(), volumeObjectTO.getChainInfo()); } @@ -145,23 +143,20 @@ public void configureChainInfo(DataTO volumeTo, Command cmd) { @Override public void cleanupBackupMetadata(long volumeId) { logger.debug("Cleaning up backup metadata for volume [{}].", volumeId); - List currents = internalBackupJoinDao.listCurrentsByVolumeIdDesc(volumeId); - if (currents.isEmpty()) { + InternalBackupStoragePoolVO delta = internalBackupStoragePoolDao.findOneByVolumeId(volumeId); + if (delta == null) { return; } internalBackupStoragePoolDao.expungeByVolumeId(volumeId); - for (InternalBackupJoinVO current : currents) { - if (CollectionUtils.isNotEmpty(internalBackupStoragePoolDao.listByBackupId(current.getId()))) { - continue; - } - - logger.debug("Volume [{}] was the last volume with deltas in backup [{}]. Setting the backup as END_OF_CHAIN and not current.", volumeId, current.getUuid()); - backupDetailDao.removeDetail(current.getId(), BackupDetailsDao.CURRENT); - if (!current.getEndOfChain()) { - backupDetailDao.persist(new BackupDetailVO(current.getId(), BackupDetailsDao.END_OF_CHAIN, Boolean.TRUE.toString(), true)); - } + if (CollectionUtils.isNotEmpty(internalBackupStoragePoolDao.listByBackupId(delta.getBackupId()))) { + return; + } + InternalBackupJoinVO joinVO = internalBackupJoinDao.findById(delta.getBackupId()); + logger.debug("Volume [{}] was the last volume with deltas in backup [{}]. Setting the backup as not current and not END_OF_CHAIN.", volumeId, joinVO.getUuid()); + backupDetailDao.removeDetail(joinVO.getId(), BackupDetailsDao.CURRENT); + if (!joinVO.getEndOfChain()) { + backupDetailDao.persist(new BackupDetailVO(joinVO.getId(), BackupDetailsDao.END_OF_CHAIN, Boolean.TRUE.toString(), true)); } - } @@ -307,7 +302,7 @@ public boolean finishBackupChain(long vmId) { if (internalBackupProvider == null) { return false; } - return internalBackupProvider.finishBackupChains(vm); + return internalBackupProvider.finishBackupChain(vm); } @Override diff --git a/server/src/main/java/org/apache/cloudstack/region/RegionManager.java b/server/src/main/java/org/apache/cloudstack/region/RegionManager.java index fedd66d94401..4e7eaad9f2c0 100644 --- a/server/src/main/java/org/apache/cloudstack/region/RegionManager.java +++ b/server/src/main/java/org/apache/cloudstack/region/RegionManager.java @@ -18,6 +18,7 @@ import java.util.List; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; @@ -128,7 +129,7 @@ public interface RegionManager { * @param moveUserCmd * @return */ - boolean moveUser(MoveUserCmd moveUserCmd); + boolean moveUser(MoveUserCmd moveUserCmd) throws ResourceAllocationException; /** * update an existing domain diff --git a/server/src/main/java/org/apache/cloudstack/region/RegionManagerImpl.java b/server/src/main/java/org/apache/cloudstack/region/RegionManagerImpl.java index 3085f655943f..a49ba4085cb2 100644 --- a/server/src/main/java/org/apache/cloudstack/region/RegionManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/region/RegionManagerImpl.java @@ -24,6 +24,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.exception.ResourceAllocationException; import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; import org.springframework.stereotype.Component; @@ -227,7 +228,7 @@ public boolean deleteUser(DeleteUserCmd cmd) { * {@inheritDoc} */ @Override - public boolean moveUser(MoveUserCmd cmd) { + public boolean moveUser(MoveUserCmd cmd) throws ResourceAllocationException { return _accountMgr.moveUser(cmd); } diff --git a/server/src/main/java/org/apache/cloudstack/region/RegionServiceImpl.java b/server/src/main/java/org/apache/cloudstack/region/RegionServiceImpl.java index 982395637e35..f0db79f7cee7 100644 --- a/server/src/main/java/org/apache/cloudstack/region/RegionServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/region/RegionServiceImpl.java @@ -22,6 +22,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.exception.ResourceAllocationException; import org.springframework.stereotype.Component; import org.apache.cloudstack.api.command.admin.account.DeleteAccountCmd; @@ -154,7 +155,7 @@ public boolean deleteUser(DeleteUserCmd cmd) { * {@inheritDoc} */ @Override - public boolean moveUser(MoveUserCmd cmd) { + public boolean moveUser(MoveUserCmd cmd) throws ResourceAllocationException { return _regionMgr.moveUser(cmd); } diff --git a/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java b/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java index 0abcf9591d44..36a2e78d461b 100644 --- a/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java +++ b/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java @@ -21,6 +21,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.projects.ProjectAccount.Role; import com.cloud.user.Account; +import com.cloud.user.User; import com.cloud.utils.component.ManagerBase; import javax.naming.ConfigurationException; @@ -214,6 +215,17 @@ public long getInvitationTimeout() { return 0; } + @Override + public boolean cleanupProjectsForUser(Project project, User user) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void moveProjectAssociationsToUser(User oldUser, User newUser) throws ResourceAllocationException { + // TODO Auto-generated method stub + } + @Override public Project findByProjectAccountIdIncludingRemoved(long projectAccountId) { return null; diff --git a/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java b/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java index b9b568facc28..ca6c2193fbd5 100644 --- a/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java +++ b/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java @@ -17,9 +17,11 @@ package com.cloud.projects; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.reservation.dao.ReservationDao; import org.apache.cloudstack.webhook.WebhookHelper; import org.apache.commons.collections.CollectionUtils; import org.junit.Assert; @@ -28,6 +30,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.Spy; @@ -35,7 +38,17 @@ import org.mockito.stubbing.Answer; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import com.cloud.configuration.Resource.ResourceType; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.projects.ProjectAccount.Role; +import com.cloud.projects.dao.ProjectAccountDao; import com.cloud.projects.dao.ProjectDao; +import com.cloud.projects.dao.ProjectInvitationDao; +import com.cloud.resourcelimit.CheckedReservation; +import com.cloud.user.AccountVO; +import com.cloud.user.ResourceLimitService; +import com.cloud.user.User; +import com.cloud.user.dao.AccountDao; import com.cloud.utils.component.ComponentContext; @@ -49,6 +62,21 @@ public class ProjectManagerImplTest { @Mock ProjectDao projectDao; + @Mock + ProjectInvitationDao projectInvitationDao; + + @Mock + ProjectAccountDao projectAccountDao; + + @Mock + AccountDao accountDao; + + @Mock + ResourceLimitService resourceLimitMgr; + + @Mock + ReservationDao reservationDao; + List updateProjects; @Before @@ -128,4 +156,158 @@ public void testDeleteWebhooksForAccountNoBean() { Assert.assertTrue(CollectionUtils.isEmpty(result)); } } + + @Test + public void cleanupProjectsForUserTestNoAssociationsReturnsFalse() { + Project project = Mockito.mock(Project.class); + Mockito.when(project.getId()).thenReturn(100L); + User user = mockUser(1L, 10L); + Mockito.when(projectAccountDao.listBy(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(Collections.emptyList()); + + boolean result = projectManager.cleanupProjectsForUser(project, user); + + Assert.assertFalse(result); + Mockito.verify(projectInvitationDao).removeBy(100L, 10L, 1L); + Mockito.verify(projectAccountDao, Mockito.never()).remove(Mockito.anyLong()); + Mockito.verify(resourceLimitMgr, Mockito.never()).decrementResourceCount(Mockito.anyLong(), Mockito.any(ResourceType.class)); + } + + @Test + public void cleanupProjectsForUserTestRemovesAdminAndRegularAssociations() { + Project project = Mockito.mock(Project.class); + Mockito.when(project.getId()).thenReturn(100L); + User user = mockUser(1L, 10L); + ProjectAccountVO admin = mockProjectAccount(1L, Role.Admin); + ProjectAccountVO regular = mockProjectAccount(2L, Role.Regular); + Mockito.when(projectAccountDao.listBy(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(List.of(admin, regular)); + + boolean result = projectManager.cleanupProjectsForUser(project, user); + + Assert.assertTrue(result); + Mockito.verify(projectInvitationDao).removeBy(100L, 10L, 1L); + Mockito.verify(projectAccountDao).remove(1L); + Mockito.verify(projectAccountDao).remove(2L); + Mockito.verify(resourceLimitMgr).decrementResourceCount(10L, ResourceType.project); + } + + @Test + public void cleanupProjectsForUserTestNullProject() { + User user = mockUser(1L, 10L); + ProjectAccountVO admin = mockProjectAccount(1L, Role.Admin); + Mockito.when(projectAccountDao.listBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L))) + .thenReturn(List.of(admin)); + + boolean result = projectManager.cleanupProjectsForUser(null, user); + + Assert.assertTrue(result); + Mockito.verify(projectInvitationDao).removeBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L)); + Mockito.verify(projectAccountDao).remove(1L); + Mockito.verify(resourceLimitMgr).decrementResourceCount(10L, ResourceType.project); + } + + @Test + public void moveProjectAssociationsToUserTestNoProjectAccounts() throws ResourceAllocationException { + User oldUser = mockUser(1L, 10L); + User newUser = mockUser(2L, 20L); + Mockito.when(projectAccountDao.listBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L))) + .thenReturn(Collections.emptyList()); + + projectManager.moveProjectAssociationsToUser(oldUser, newUser); + + Mockito.verify(projectInvitationDao).move(oldUser, newUser); + Mockito.verify(projectAccountDao, Mockito.never()).move(Mockito.any(), Mockito.any()); + Mockito.verifyNoInteractions(accountDao); + Mockito.verifyNoInteractions(resourceLimitMgr); + } + + @Test + public void moveProjectAssociationsToUserTestSameAccount() throws ResourceAllocationException { + User oldUser = mockUser(1L, 10L); + User newUser = mockUser(2L, 10L); + ProjectAccountVO regular = mockProjectAccount(1L, Role.Regular); + Mockito.when(projectAccountDao.listBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L))) + .thenReturn(List.of(regular)); + AccountVO oldAccount = mockAccount(10L); + AccountVO newAccount = mockAccount(10L); + Mockito.when(accountDao.findById(10L)).thenReturn(oldAccount).thenReturn(newAccount); + + try (MockedConstruction ignored = Mockito.mockConstruction(CheckedReservation.class)) { + projectManager.moveProjectAssociationsToUser(oldUser, newUser); + } + + Mockito.verify(projectInvitationDao).move(oldUser, newUser); + Mockito.verify(projectAccountDao).move(oldUser, newUser); + Mockito.verify(resourceLimitMgr, Mockito.never()) + .incrementResourceCount(Mockito.anyLong(), Mockito.any(ResourceType.class), Mockito.anyLong()); + Mockito.verify(resourceLimitMgr, Mockito.never()) + .decrementResourceCount(Mockito.anyLong(), Mockito.any(ResourceType.class), Mockito.anyLong()); + } + + @Test + public void moveProjectAssociationsToUserTestDifferentAccountsWithAdminRole() throws ResourceAllocationException { + User oldUser = mockUser(1L, 10L); + User newUser = mockUser(2L, 20L); + ProjectAccountVO admin = mockProjectAccount(1L, Role.Admin); + Mockito.when(projectAccountDao.listBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L))) + .thenReturn(List.of(admin)); + AccountVO oldAccount = mockAccount(10L); + AccountVO newAccount = mockAccount(20L); + Mockito.when(accountDao.findById(10L)).thenReturn(oldAccount); + Mockito.when(accountDao.findById(20L)).thenReturn(newAccount); + + try (MockedConstruction ignored = Mockito.mockConstruction(CheckedReservation.class)) { + projectManager.moveProjectAssociationsToUser(oldUser, newUser); + } + + Mockito.verify(projectInvitationDao).move(oldUser, newUser); + Mockito.verify(projectAccountDao).move(oldUser, newUser); + Mockito.verify(resourceLimitMgr).incrementResourceCount(20L, ResourceType.project, 1L); + Mockito.verify(resourceLimitMgr).decrementResourceCount(10L, ResourceType.project, 1L); + } + + @Test + public void moveProjectAssociationsToUserTestDifferentAccountsWithoutAdminRole() throws ResourceAllocationException { + User oldUser = mockUser(1L, 10L); + User newUser = mockUser(2L, 20L); + ProjectAccountVO regular = mockProjectAccount(1L, Role.Regular); + Mockito.when(projectAccountDao.listBy(Mockito.isNull(), Mockito.eq(10L), Mockito.eq(1L))) + .thenReturn(List.of(regular)); + AccountVO oldAccount = mockAccount(10L); + AccountVO newAccount = mockAccount(20L); + Mockito.when(accountDao.findById(10L)).thenReturn(oldAccount); + Mockito.when(accountDao.findById(20L)).thenReturn(newAccount); + + try (MockedConstruction ignored = Mockito.mockConstruction(CheckedReservation.class)) { + projectManager.moveProjectAssociationsToUser(oldUser, newUser); + } + + Mockito.verify(projectInvitationDao).move(oldUser, newUser); + Mockito.verify(projectAccountDao).move(oldUser, newUser); + Mockito.verify(resourceLimitMgr, Mockito.never()) + .incrementResourceCount(Mockito.anyLong(), Mockito.any(ResourceType.class), Mockito.anyLong()); + Mockito.verify(resourceLimitMgr, Mockito.never()) + .decrementResourceCount(Mockito.anyLong(), Mockito.any(ResourceType.class), Mockito.anyLong()); + } + + private User mockUser(long id, long accountId) { + User user = Mockito.mock(User.class); + Mockito.when(user.getId()).thenReturn(id); + Mockito.when(user.getAccountId()).thenReturn(accountId); + return user; + } + + private AccountVO mockAccount(long id) { + AccountVO account = Mockito.mock(AccountVO.class); + Mockito.when(account.getId()).thenReturn(id); + return account; + } + + private ProjectAccountVO mockProjectAccount(long id, Role role) { + ProjectAccountVO projectAccount = Mockito.mock(ProjectAccountVO.class); + Mockito.when(projectAccount.getId()).thenReturn(id); + Mockito.when(projectAccount.getAccountRole()).thenReturn(role); + return projectAccount; + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 61cdde697dd0..379ae76d1197 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -2131,4 +2131,16 @@ public void testCreateUserSuccess() { ); Assert.assertNotNull(userResultVO); } + + @Test + public void deleteAndCleanupUserTestUserCleanup() { + long userId = userVoMock.getId(); + Mockito.doNothing().when(accountManagerImpl).removeUserApiKeys(userId); + Mockito.doReturn(true).when(_projectMgr).cleanupProjectsForUser(null, userVoMock); + + accountManagerImpl.deleteAndCleanupUser(userVoMock); + + Mockito.verify(accountManagerImpl).removeUserApiKeys(userId); + Mockito.verify(_projectMgr).cleanupProjectsForUser(null, userVoMock); + } } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 927b2831c6a7..04bd03670079 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -725,7 +725,7 @@ public void createBackupTestCreateScheduledBackup() throws ResourceAllocationExc when(backup.getId()).thenReturn(backupId); when(backup.getSize()).thenReturn(newBackupSize); when(backupProvider.getName()).thenReturn("testbackupprovider"); - when(backupProvider.takeBackup(vmInstanceVOMock, null, false, scheduleId)).thenReturn(new Pair<>(true, backup)); + when(backupProvider.takeBackup(vmInstanceVOMock, null, false)).thenReturn(new Pair<>(true, backup)); Map backupProvidersMap = new HashMap<>(); backupProvidersMap.put(backupProvider.getName().toLowerCase(), backupProvider); ReflectionTestUtils.setField(backupManager, "backupProvidersMap", backupProvidersMap); @@ -955,7 +955,6 @@ public void deleteAllVmBackupSchedulesTestReturnSuccessWhenAllSchedulesAreDelete Mockito.when(backupSchedules.get(0).getId()).thenReturn(2L); Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L); Mockito.when(backupScheduleDao.remove(Mockito.anyLong())).thenReturn(true); - Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteAllVmBackupSchedules(vmId); assertTrue(success); @@ -971,7 +970,6 @@ public void deleteAllVmBackupSchedulesTestReturnFalseWhenAnyDeletionFails() { Mockito.when(backupSchedules.get(1).getId()).thenReturn(3L); Mockito.when(backupScheduleDao.remove(2L)).thenReturn(true); Mockito.when(backupScheduleDao.remove(3L)).thenReturn(false); - Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteAllVmBackupSchedules(vmId); assertFalse(success); @@ -1017,7 +1015,6 @@ public void deleteBackupScheduleTestDeleteSpecificScheduleWhenItsIdIsSpecified() Mockito.doNothing().when(backupManager).checkCallerAccessToBackupScheduleVm(vmId); when(backupScheduleVOMock.getId()).thenReturn(id); when(backupScheduleDao.remove(id)).thenReturn(true); - Mockito.doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(Mockito.any()); boolean success = backupManager.deleteBackupSchedule(deleteBackupScheduleCmdMock); assertTrue(success); @@ -1332,7 +1329,6 @@ public void testDeleteBackupScheduleByVmId() { when(schedule.getId()).thenReturn(scheduleId); when(backupScheduleDao.listByVM(vmId)).thenReturn(List.of(schedule)); when(backupScheduleDao.remove(scheduleId)).thenReturn(true); - doNothing().when(backupManager).finalizeBackupScheduleIfNeeded(any()); boolean result = backupManager.deleteBackupSchedule(cmd); assertTrue(result); @@ -2878,45 +2874,4 @@ public void createBackupOfferingTestAddsNoDetails() { verify(backupOfferingDao).persist(any()); verify(backupOfferingDetailsDao, never()).saveDetails(any()); } - - @Test(expected = CloudRuntimeException.class) - public void endScheduleBackupChainIfNeededTestInvalidVirtualMachineThrowCloudRuntimeException() { - Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); - - backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); - } - - @Test(expected = CloudRuntimeException.class) - public void endScheduleBackupChainIfNeededTestInvalidBackupOfferingThrowCloudRuntimeException() { - Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); - Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); - - backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); - } - - @Test - public void endScheduleBackupChainIfNeededTestBackupProviderSuccessDoesNotThrowException() { - Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); - Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); - Mockito.doReturn(2L).when(vmInstanceVOMock).getBackupOfferingId(); - Mockito.doReturn(backupOfferingVOMock).when(backupOfferingDao).findById(2L); - Mockito.doReturn(BackupManagerImpl.KBOSS_BACKUP_PROVIDER).when(backupOfferingVOMock).getProvider(); - Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider(BackupManagerImpl.KBOSS_BACKUP_PROVIDER); - Mockito.doReturn(true).when(backupProvider).removeVMBackupSchedule(vmInstanceVOMock, backupScheduleVOMock); - - backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); - } - - @Test(expected = CloudRuntimeException.class) - public void endScheduleBackupChainIfNeededTestBackupProviderFailThrowCloudRuntimeException() { - Mockito.doReturn(1L).when(backupScheduleVOMock).getVmId(); - Mockito.doReturn(vmInstanceVOMock).when(vmInstanceDao).findById(1L); - Mockito.doReturn(2L).when(vmInstanceVOMock).getBackupOfferingId(); - Mockito.doReturn(backupOfferingVOMock).when(backupOfferingDao).findById(2L); - Mockito.doReturn(BackupManagerImpl.KBOSS_BACKUP_PROVIDER).when(backupOfferingVOMock).getProvider(); - Mockito.doReturn(backupProvider).when(backupManager).getBackupProvider(BackupManagerImpl.KBOSS_BACKUP_PROVIDER); - Mockito.doReturn(false).when(backupProvider).removeVMBackupSchedule(vmInstanceVOMock, backupScheduleVOMock); - - backupManager.finalizeBackupScheduleIfNeeded(backupScheduleVOMock); - } } diff --git a/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java index 5ad0aabaf825..008a590c3e5a 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/InternalBackupServiceImplTest.java @@ -17,7 +17,9 @@ package org.apache.cloudstack.backup; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doNothing; @@ -33,6 +35,8 @@ import org.apache.cloudstack.backup.dao.InternalBackupJoinDao; import org.apache.cloudstack.backup.dao.InternalBackupStoragePoolDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.RevertSnapshotCommand; import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreObjectDownloadVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; @@ -140,26 +144,83 @@ public void configureChainInfoTestNonVolumeObjectReturnsImmediately() { internalBackupServiceImplSpy.configureChainInfo(dataToMock, cmdMock); - verify(internalBackupStoragePoolDaoMock, never()).listByVolumeId(anyLong()); + verify(internalBackupStoragePoolDaoMock, never()).findOneByVolumeId(anyLong()); + } + + @Test + public void configureChainInfoTestVolumeWithoutBackupDeltaReturnsImmediately() { + doReturn(VOLUME_ID).when(volumeObjectToMock).getVolumeId(); + doReturn(null).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + + internalBackupServiceImplSpy.configureChainInfo(volumeObjectToMock, mock(Command.class)); + + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + verify(volumeObjectToMock, never()).setChainInfo(anyString()); + } + + @Test + public void configureChainInfoTestSetsChainInfoForGenericCommand() { + doReturn(VOLUME_ID).when(volumeObjectToMock).getVolumeId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + doReturn("/path/to/parent").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + + Command cmdMock = mock(Command.class); + + internalBackupServiceImplSpy.configureChainInfo(volumeObjectToMock, cmdMock); + + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + verify(volumeObjectToMock).setChainInfo("/path/to/parent"); + } + + @Test + public void configureChainInfoTestSetsDeleteChainForDeleteCommand() { + doReturn(VOLUME_ID).when(volumeObjectToMock).getVolumeId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + doReturn("/path/to/parent").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + + DeleteCommand deleteCommand = new DeleteCommand(volumeObjectToMock); + + internalBackupServiceImplSpy.configureChainInfo(volumeObjectToMock, deleteCommand); + + verify(volumeObjectToMock).setChainInfo("/path/to/parent"); + assertTrue(deleteCommand.isDeleteChain()); + } + + @Test + public void configureChainInfoTestSetsDeleteChainForRevertSnapshotCommand() { + doReturn(VOLUME_ID).when(volumeObjectToMock).getVolumeId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + doReturn("/path/to/parent").when(internalBackupStoragePoolVoMock).getBackupDeltaParentPath(); + + RevertSnapshotCommand revertSnapshotCommand = new RevertSnapshotCommand(snapshotObjectToMock, snapshotObjectToMock); + + internalBackupServiceImplSpy.configureChainInfo(volumeObjectToMock, revertSnapshotCommand); + + verify(volumeObjectToMock).setChainInfo("/path/to/parent"); + assertTrue(revertSnapshotCommand.isDeleteChain()); } @Test public void cleanupBackupMetadataTestNoDeltaReturnsImmediately() { + doReturn(null).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); + internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock, never()).expungeByVolumeId(VOLUME_ID); verify(internalBackupJoinDaoMock, never()).findById(anyLong()); } @Test public void cleanupBackupMetadataTestDeltaExistsButOtherDeltasRemainReturnsImmediately() { - doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(BACKUP_ID).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); doReturn(List.of(internalBackupStoragePoolVoMock, mock(InternalBackupStoragePoolVO.class))) .when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); verify(internalBackupJoinDaoMock, never()).findById(anyLong()); @@ -167,30 +228,38 @@ public void cleanupBackupMetadataTestDeltaExistsButOtherDeltasRemainReturnsImmed @Test public void cleanupBackupMetadataTestLastDeltaAndEndOfChainTrue() { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(BACKUP_ID).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(BACKUP_ID); doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); doReturn(true).when(internalBackupJoinVoMock).getEndOfChain(); internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + verify(internalBackupJoinDaoMock).findById(BACKUP_ID); verify(backupDetailDaoMock).removeDetail(BACKUP_ID, BackupDetailsDao.CURRENT); verify(backupDetailDaoMock, never()).persist(any()); } @Test public void cleanupBackupMetadataTestLastDeltaAndEndOfChainFalse() { - doReturn(List.of(internalBackupJoinVoMock)).when(internalBackupJoinDaoMock).listCurrentsByVolumeIdDesc(VOLUME_ID); + doReturn(BACKUP_ID).when(internalBackupStoragePoolVoMock).getBackupId(); + doReturn(internalBackupStoragePoolVoMock).when(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); doReturn(List.of()).when(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + doReturn(internalBackupJoinVoMock).when(internalBackupJoinDaoMock).findById(BACKUP_ID); doReturn(BACKUP_ID).when(internalBackupJoinVoMock).getId(); doReturn(false).when(internalBackupJoinVoMock).getEndOfChain(); internalBackupServiceImplSpy.cleanupBackupMetadata(VOLUME_ID); + verify(internalBackupStoragePoolDaoMock).findOneByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).expungeByVolumeId(VOLUME_ID); verify(internalBackupStoragePoolDaoMock).listByBackupId(BACKUP_ID); + verify(internalBackupJoinDaoMock).findById(BACKUP_ID); verify(backupDetailDaoMock).removeDetail(BACKUP_ID, BackupDetailsDao.CURRENT); verify(backupDetailDaoMock).persist(any(BackupDetailVO.class)); } diff --git a/tools/build/build_asf.sh b/tools/build/build_asf.sh index 44d41472b05a..bc43d3b5b1d2 100755 --- a/tools/build/build_asf.sh +++ b/tools/build/build_asf.sh @@ -103,6 +103,9 @@ perl -pi -e "s/-SNAPSHOT//" tools/marvin/marvin/deployAndRun.py perl -pi -e "s/-SNAPSHOT//" tools/docker/Dockerfile perl -pi -e "s/-SNAPSHOT//" tools/docker/Dockerfile.marvin perl -pi -e "s/-SNAPSHOT//" tools/docker/Dockerfile.centos6 +perl -pi -e "s/-SNAPSHOT//" tools/docker/Dockerfile.s390x +perl -pi -e "s/-SNAPSHOT//" plugins/hypervisors/ovm/pom.xml +perl -pi -e "s/-SNAPSHOT//" plugins/hypervisors/ovm3/pom.xml case "$currentversion" in *-SNAPSHOT*) diff --git a/ui/src/config/section/infra/phynetworks.js b/ui/src/config/section/infra/phynetworks.js index 9b7a0a83f7f3..0863eff6ec0b 100644 --- a/ui/src/config/section/infra/phynetworks.js +++ b/ui/src/config/section/infra/phynetworks.js @@ -94,7 +94,7 @@ export default { icon: 'edit-outlined', label: 'label.update.physical.network', dataView: true, - args: ['vlan', 'tags', 'externaldetails'] + args: ['vlan', 'tags'] }, { api: 'addTrafficType',