diff --git a/.asf.yaml b/.asf.yaml index 3b2f7691d136..f27b5afa4258 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -16,7 +16,6 @@ # under the License. # https://cwiki.apache.org/confluence/display/INFRA/git+-+.asf.yaml+features ---- github: description: "Apache CloudStack is an opensource Infrastructure as a Service (IaaS) cloud computing platform" homepage: https://cloudstack.apache.org/ @@ -57,8 +56,26 @@ github: - Damans227 - jmsperu - GaOrtiga + - bhouse-nexthop - protected_branches: ~ + protected_branches: + + rulesets: + - name: "Default Branch Protection" + type: branch + branches: + includes: + - "~DEFAULT_BRANCH" + excludes: [] + bypass_teams: + - root + restrict_deletion: true + restrict_force_push: true + + copilot_code_review: + enabled: true + review_drafts: true + review_on_push: true notifications: commits: commits@cloudstack.apache.org diff --git a/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md b/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md deleted file mode 100644 index 72d61b434efa..000000000000 --- a/.github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# Report formatting guidelines ---- - -## Report Structure Guidelines - -### 1. Header Levels -**Use h3 (###) or lower for all headers in your issue report to maintain proper document hierarchy.** - -When creating GitHub issues or discussions: -- Use `###` (h3) for main sections (e.g., "### Test Summary") -- Use `####` (h4) for subsections (e.g., "#### Device-Specific Results") -- Never use `##` (h2) or `#` (h1) in reports - these are reserved for titles - -### 2. Progressive Disclosure -**Wrap detailed test results in `
Section Name` tags to improve readability and reduce scrolling.** - -Use collapsible sections for: -- Verbose details (full test logs, raw data) -- Secondary information (minor warnings, extra context) -- Per-item breakdowns when there are many items - -Always keep critical information visible (summary, critical issues, key metrics). - -### 3. Report Structure Pattern - -1. **Overview**: 1-2 paragraphs summarizing key findings -2. **Critical Information**: Show immediately (summary stats, critical issues) -3. **Details**: Use `
Section Name` for expanded content -4. **Context**: Add helpful metadata (workflow run, date, trigger) - -### Design Principles (Airbnb-Inspired) - -Reports should: -- **Build trust through clarity**: Most important info immediately visible -- **Exceed expectations**: Add helpful context like trends, comparisons -- **Create delight**: Use progressive disclosure to reduce overwhelm -- **Maintain consistency**: Follow patterns across all reports - -### Example Report Structure - -```markdown -### Summary -- Key metric 1: value -- Key metric 2: value -- Status: ✅/⚠️/❌ - -### Critical Issues -[Always visible - these are important] - -
-View Detailed Results - -[Comprehensive details, logs, traces] - -
- -
-View All Warnings - -[Minor issues and potential problems] - -
- -### Recommendations -[Actionable next steps - keep visible] -``` - -## Workflow Run References - -- Format run IDs as links: `[§12345](https://github.com/owner/repo/actions/runs/12345)` -- Include up to 3 most relevant run URLs at end under `**References:**` -- Do NOT add footer attribution (system adds automatically) diff --git a/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md b/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md deleted file mode 100644 index bc08afb42be9..000000000000 --- a/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# Report formatting guidelines ---- - -## Report Structure Guidelines - -### 1. Header Levels -**Use h3 (###) or lower for all headers in your issue report to maintain proper document hierarchy.** - -When creating GitHub issues or discussions: -- Use `###` (h3) for main sections (e.g., "### Test Summary") -- Use `####` (h4) for subsections (e.g., "#### Device-Specific Results") -- Never use `##` (h2) or `#` (h1) in reports - these are reserved for titles - -### 2. Progressive Disclosure -**Wrap detailed test results in `
Section Name` tags to improve readability and reduce scrolling.** - -Use collapsible sections for: -- Verbose details (full test logs, raw data) -- Secondary information (minor warnings, extra context) -- Per-item breakdowns when there are many items - -Always keep critical information visible (summary, critical issues, key metrics). - -### 3. Report Structure Pattern - -1. **Overview**: 1-2 paragraphs summarizing key findings -2. **Critical Information**: Show immediately (summary stats, critical issues) -3. **Details**: Use `
Section Name` for expanded content -4. **Context**: Add helpful metadata (workflow run, date, trigger) - -### Design Principles (Airbnb-Inspired) - -Reports should: -- **Build trust through clarity**: Most important info immediately visible -- **Exceed expectations**: Add helpful context like trends, comparisons -- **Create delight**: Use progressive disclosure to reduce overwhelm -- **Maintain consistency**: Follow patterns across all reports - -### Example Report Structure - -```markdown -### Summary -- Key metric 1: value -- Key metric 2: value -- Status: ✅/⚠️/❌ - -### Critical Issues -[Always visible - these are important] - -
-View Detailed Results - -[Comprehensive details, logs, traces] - -
- -
-View All Warnings - -[Minor issues and potential problems] - -
- -### Recommendations -[Actionable next steps - keep visible] -``` - -## Workflow Run References - -- Format run IDs as links: `[§12345](https://github.com/owner/repo/actions/runs/12345)` -- Include up to 3 most relevant run URLs at end under `**References:**` -- Do NOT add footer attribution (system adds automatically) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4eb508f5f6f8..62b816719c7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9d03bf1209b..63484def8f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: if: github.repository == 'apache/cloudstack' runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 persist-credentials: false @@ -251,7 +251,7 @@ jobs: smoke/test_list_storage_pools smoke/test_list_volumes"] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 persist-credentials: false @@ -348,7 +348,7 @@ jobs: echo -e "Simulator CI Test Results: (only failures listed)\n" python3 ./tools/marvin/xunit-reader.py integration-test-results/ - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: jacoco-coverage.xml fail_ci_if_error: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a2d581dcac20..f6bc65ad4b5b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,16 +35,16 @@ jobs: language: ["actions"] steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "Security" diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/daily-issue-triage.lock.yml similarity index 90% rename from .github/workflows/issue-triage-agent.lock.yml rename to .github/workflows/daily-issue-triage.lock.yml index 3acf91dcc1ef..bd07aeefd811 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/daily-issue-triage.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3cfe810a6f1f402ae902098f1dc14b99a4281a5986d58b76e96eec1a2fde7646","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"919fb17c7928e5e96d9c0a2854670a42f9c5f6cfc2059b46009bb3c23640d0ca","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} # 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"}]} # ___ _ _ # / _ \ | | (_) @@ -16,18 +16,17 @@ # # This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. # -# To update this file, edit github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115 and run: +# To update this file, edit githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 and run: # gh aw compile # Not all edits will cause changes to this file. # # For more information: https://github.github.com/gh-aw/introduction/overview/ # +# Scheduled daily triage that processes untriaged CloudStack issues in batches. +# Detects duplicates, filters spam, and assigns CloudStack-specific labels +# (type:*, component:*, Severity:*, status:*), then posts a structured triage report. # -# Source: github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115 -# -# Resolved workflow manifest: -# Imports: -# - github/gh-aw/.github/workflows/shared/reporting.md@359795d49ada21681ab616bd4cbcb144a7387115 +# Source: githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -36,13 +35,12 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 @@ -52,10 +50,10 @@ # - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 -name: "Issue Triage Agent" +name: "Daily Issue Triage" on: schedule: - - cron: "49 14 * * 1-5" + - cron: "26 13 * * 1-5" # Friendly format: daily around 14:00 on weekdays (scattered) workflow_dispatch: inputs: @@ -70,7 +68,7 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" -run-name: "Issue Triage Agent" +run-name: "Daily Issue Triage" jobs: activation: @@ -92,13 +90,13 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_BODY_MODIFIED: "false" @@ -112,7 +110,7 @@ jobs: 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: "Issue Triage Agent" + GH_AW_INFO_WORKFLOW_NAME: "Daily Issue Triage" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" @@ -121,9 +119,8 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_INFO_FRONTMATTER_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_FRONTMATTER_EMOJI: "🔧" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -138,7 +135,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -163,7 +160,7 @@ jobs: id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_WORKFLOW_FILE: "issue-triage-agent.lock.yml" + GH_AW_WORKFLOW_FILE: "daily-issue-triage.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | @@ -197,20 +194,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_7c51e8f15cc7af75_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_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' - Tools: add_comment, add_labels, missing_tool, missing_data, noop + Tools: add_comment(max:10), add_labels(max:10), missing_tool, missing_data, noop - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_7c51e8f15cc7af75_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -239,14 +236,12 @@ jobs: {{/if}} - GH_AW_PROMPT_9a8f42497f411c95_EOF + GH_AW_PROMPT_7c51e8f15cc7af75_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9a8f42497f411c95_EOF' + cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF' - {{#runtime-import .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md}} - {{#runtime-import .github/workflows/shared/noop-reminder.md}} - {{#runtime-import .github/workflows/issue-triage-agent.md}} - GH_AW_PROMPT_9a8f42497f411c95_EOF + {{#runtime-import .github/workflows/daily-issue-triage.md}} + GH_AW_PROMPT_7c51e8f15cc7af75_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -326,8 +321,7 @@ jobs: agent: needs: activation runs-on: ubuntu-latest - permissions: - issues: read + permissions: read-all concurrency: group: "gh-aw-copilot-${{ github.workflow }}" env: @@ -336,9 +330,10 @@ 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: issuetriageagent + GH_AW_WORKFLOW_ID_SANITIZED: dailyissuetriage outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} has_patch: ${{ steps.collect_output.outputs.has_patch }} @@ -354,15 +349,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 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: "Issue Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_BODY_MODIFIED: "false" @@ -376,7 +371,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -398,27 +393,44 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 env: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: activation path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: GH_AW_SUB_AGENT_DIR: ".github/agents" @@ -435,16 +447,16 @@ 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_40bb017992a44aa9_EOF' - {"add_comment":{"max":1},"add_labels":{"allowed":["bug","feature","enhancement","documentation","question","help-wanted","good-first-issue"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_40bb017992a44aa9_EOF + 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 - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"bug\" \"feature\" \"enhancement\" \"documentation\" \"question\" \"help-wanted\" \"good-first-issue\"]." + "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Maximum 10 label(s) can be added. Target: *." }, "repo_params": {}, "dynamic_tools": [] @@ -619,8 +631,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -639,8 +649,6 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="copilot" - export GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' - echo GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' >> "$GITHUB_ENV" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') case "${DOCKER_HOST:-}" in @@ -653,7 +661,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_8874a73d1c6a94b7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_37cac1d5ee0c175c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -667,8 +675,11 @@ jobs: }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -694,7 +705,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_8874a73d1c6a94b7_EOF + GH_AW_MCP_CONFIG_37cac1d5ee0c175c_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -719,7 +730,7 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 5 + timeout-minutes: 60 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -906,6 +917,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/pre-agent-audit.txt @@ -937,7 +950,7 @@ jobs: issues: write pull-requests: write concurrency: - group: "gh-aw-conclusion-issue-triage-agent" + group: "gh-aw-conclusion-daily-issue-triage" cancel-in-progress: false queue: max outputs: @@ -948,15 +961,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 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: "Issue Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_BODY_MODIFIED: "false" @@ -981,9 +994,9 @@ 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: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_REPORT_AS_ISSUE: "true" @@ -999,9 +1012,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_WORKFLOW_NAME: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} @@ -1018,9 +1031,9 @@ jobs: 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: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1034,9 +1047,9 @@ jobs: 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: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1050,15 +1063,16 @@ 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: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "issue-triage-agent" + GH_AW_WORKFLOW_ID: "daily-issue-triage" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} @@ -1072,7 +1086,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "5" + GH_AW_TIMEOUT_MINUTES: "60" GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1098,15 +1112,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 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: "Issue Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_BODY_MODIFIED: "false" @@ -1127,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1175,8 +1189,8 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - WORKFLOW_NAME: "Issue Triage Agent" - WORKFLOW_DESCRIPTION: "No description provided" + WORKFLOW_NAME: "Daily Issue Triage" + WORKFLOW_DESCRIPTION: "Scheduled daily triage that processes untriaged CloudStack issues in batches.\nDetects duplicates, filters spam, and assigns CloudStack-specific labels\n(type:*, component:*, Severity:*, status:*), then posts a structured triage report." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1299,18 +1313,17 @@ jobs: pull-requests: write timeout-minutes: 15 env: - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/issue-triage-agent" + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/daily-issue-triage" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.52" - GH_AW_WORKFLOW_EMOJI: "🔧" - GH_AW_WORKFLOW_ID: "issue-triage-agent" - GH_AW_WORKFLOW_NAME: "Issue Triage Agent" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/359795d49ada21681ab616bd4cbcb144a7387115/.github/workflows/issue-triage-agent.md" + GH_AW_WORKFLOW_ID: "daily-issue-triage" + GH_AW_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/d7c1dc4b72b00607a67caaffdcc216cb64379cf9/workflows/daily-issue-triage.md" outputs: code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} @@ -1323,15 +1336,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 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: "Issue Triage Agent" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage-agent.lock.yml@${{ github.ref }} + GH_AW_SETUP_WORKFLOW_NAME: "Daily Issue Triage" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/daily-issue-triage.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AWF_VERSION: "v0.25.55" GH_AW_INFO_BODY_MODIFIED: "false" @@ -1368,7 +1381,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\":1},\"add_labels\":{\"allowed\":[\"bug\",\"feature\",\"enhancement\",\"documentation\",\"question\",\"help-wanted\",\"good-first-issue\"]},\"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\":\"*\"},\"create_report_incomplete_issue\":{},\"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 new file mode 100644 index 000000000000..719dca1f3c63 --- /dev/null +++ b/.github/workflows/daily-issue-triage.md @@ -0,0 +1,171 @@ +--- +description: | + Scheduled daily triage that processes untriaged CloudStack issues in batches. + Detects duplicates, filters spam, and assigns CloudStack-specific labels + (type:*, component:*, Severity:*, status:*), then posts a structured triage report. + +name: Daily Issue Triage + +on: + schedule: daily around 14:00 on weekdays + workflow_dispatch: + +permissions: read-all + +network: defaults + +safe-outputs: + add-labels: + target: "*" + max: 10 + add-comment: + target: "*" + max: 10 + +tools: + web-fetch: + github: + toolsets: [issues, labels] + min-integrity: none + +source: githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9 +timeout-minutes: 60 +--- + +# Daily Issue Triage + + + +You are a batch triage assistant for GitHub issues in **${{ github.repository }}** (Apache CloudStack). Your task is to find untriaged issues and triage them one by one. Your triage comments are written for maintainers reviewing the triage, not for the issue author. + +Do not make assumptions beyond what the issue content supports. Do not invent missing context. + +## Step 1: Find untriaged issues + +Use the `search_issues` tool to find open issues that need triage. An issue is considered untriaged if it has **no labels applied**. + +Query: `repo:${{ github.repository }} is:issue is:open no:label` + +Paginate through all results to find untriaged issues. Do not stop at the first page. + +From the results, filter out: +- Issues that already have a triage comment (look for "🎯 Triage report" in comments). **Never retriage an issue that has already been triaged.** +- Issues created by bots (unless they look like real user issues). +- Issues that have any labels already applied (even if they weren't applied by this workflow). + +Process the **oldest untriaged issues first**. Note: this workflow is capped at 10 label-sets and 10 comments per run, so the backlog will drain over several daily runs — that is intentional. + +## Step 2: Fetch labels (once) + +Before triaging any issues, fetch the list of labels available in this repository using the `list_labels` tool. Use this live list for all issues in the batch — only apply labels that actually exist in the repository. + +CloudStack uses a prefixed label taxonomy. Choose from these families: + +- **Type** (pick the single best one): `type:bug`, `type:new-feature`, `type:enhancement`, `type:improvement`, `type:regression`, `type:security`, `type:question`, `type:config`, `type:cleanup` +- **Component** (apply when clearly identifiable; more than one is allowed): e.g. `component:kvm`, `component:vmware`, `component:XenServer`, `component:api`, `component:UI`, `component:networking`, `component:virtual-router`, `component:management-server`, `component:primary-storage`, `component:secondary-storage`, `component:kubernetes`, `component:database`, and others — use the full list returned by `list_labels`. +- **Severity** (bugs only, when assessable): `Severity:BLOCKER`, `Severity:Critical`, `Severity:Major`, `Severity:Minor`, `Severity:Trivial` +- **Duplicate / invalid**: `status:duplicate`, `status:invalid` +- **Help wanted / newcomer-friendly**: `status:Help-wanted` + +## Step 3: Triage each issue + +For each untriaged issue, perform the following steps: + +### 3a: Gather context + +1. Retrieve the full issue content using the `get_issue` tool. +2. Fetch any comments on the issue using the `get_issue_comments` tool. +3. Search for similar issues using the `search_issues` tool. + +### 3b: Spam and quality check + +**Spam and invalid issues:** If the issue is obviously spam, bot-generated, gibberish, or a test issue: +- Apply the `status:invalid` label. +- **Do not close the issue** — closing is a human decision. Note in the report that it looks like spam/invalid so a maintainer can act. +- Move to the next issue. + +**Incomplete issues:** If the issue lacks enough detail for meaningful triage, add a comment that politely asks the author to provide the missing information: +- For bugs: steps to reproduce, expected vs actual behavior, logs/errors, environment details (CloudStack version, hypervisor, etc.). +- For other issue types: equivalent details that would make the report actionable. +- Apply a `type:question` label if appropriate. +- Be specific about what is missing and why it is needed. +- Move to the next issue. + +### 3c: Select labels + +- Be cautious with labels; they can trigger automation. +- Choose a single `type:*` label that best reflects the issue's nature. +- Add `component:*` label(s) when the affected area is clear from the content. +- Add a `Severity:*` label for bugs when severity can be reasonably assessed. +- Do not apply labels that do not exist in the repository. +- It is better to under-label than to speculatively add labels. + +### 3d: Detect duplicates and related issues + +- Review the similar issues found in Step 3a. +- Classify matches as: + - **Duplicate** (high confidence): the issue describes the same problem as an existing open issue. Include up to 3. + - **Related**: similar domain or adjacent problem, but not a duplicate. Include up to 3. +- If a high-confidence duplicate is found, apply the `status:duplicate` label. +- If no similar issues are found, state that explicitly in your report. + +### 3e: Assess coding agent suitability + +Assess whether the issue is suitable for automated coding agent assignment: +- **Suitable**: clear requirements, sufficient context, well-defined success criteria, self-contained scope. +- **Needs more info**: potentially suitable but missing details needed to start. +- **Not suitable**: requires investigation, design decisions, extensive coordination, or policy/architectural choices. + +### 3f: Additional analysis + +- Search the web for relevant documentation, error messages, or known solutions if applicable. +- Write notes, debugging strategies, and/or reproduction steps relevant to the issue. +- Suggest resources or links that might help resolve the issue. + +### 3g: Apply results and post comment + +Apply all triage results for this issue: +- Use `update_issue` to apply the chosen labels. +- Add an issue comment with the triage report using the format below. + +Then move to the next issue. + +## Processing order + +1. Fetch available labels (Step 2, once at the start). +2. Find untriaged issues (Step 1). +3. For each issue (oldest first), run Step 3 (gather, check, label, detect duplicates, comment). + +## Comment format + +Use this structure for each triage comment. Use collapsed sections to keep it tidy. + +```markdown +## 🎯 Triage report + +{2-3 sentence summary to help a maintainer quickly grasp the issue.} + +### 📊 Assessment + +| Dimension | Value | Reasoning | +|---|---|---| +| **Type** | [type:* label or "none"] | [brief] | +| **Component** | [component:* label(s) or "none"] | [brief] | +| **Severity** | [Severity:* label or "n/a"] | [brief] | +| **Labels** | [all labels applied or "none"] | [brief] | +| **Coding agent** | [Suitable / Needs more info / Not suitable] | [brief] | + +### 🔗 Similar issues + +- issue-url (duplicate/related) — [brief explanation] + +
💡 Notes and suggestions + +{Debugging strategies, reproduction steps, resource links, sub-task checklists, nudges for the team.} + +
+``` + +If no similar issues were found, omit the "Similar issues" section. If there are no notes to add, omit the collapsed section. + +**Important**: Never close issues. Only apply labels and post comments. diff --git a/.github/workflows/daily-repo-status.lock.yml b/.github/workflows/daily-repo-status.lock.yml index 7c3d20a166ea..0992d3b67de0 100644 --- a/.github/workflows/daily-repo-status.lock.yml +++ b/.github/workflows/daily-repo-status.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ae44897dc32d20c421588b31a279653abf436964ad86c58885e0edbfbb2f0416","compiler_version":"v0.76.1","strict":true,"agent_id":"copilot"} +# 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-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"}]} # ___ _ _ # / _ \ | | (_) @@ -36,12 +36,12 @@ # - GITHUB_TOKEN # # Custom actions used: -# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 +# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.55@sha256:138c363411decc9a61a5af9b95e8d64c76648b00add0ba06fc7ba786f0e72731 @@ -91,7 +91,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -107,7 +107,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-haiku-4.5" GH_AW_INFO_VERSION: "1.0.52" GH_AW_INFO_AGENT_VERSION: "1.0.52" GH_AW_INFO_CLI_VERSION: "v0.76.1" @@ -136,7 +136,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: | @@ -195,20 +195,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_603e0a48f86a9470_EOF' + cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' - GH_AW_PROMPT_603e0a48f86a9470_EOF + GH_AW_PROMPT_eeb322738661ed58_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_603e0a48f86a9470_EOF' + cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_603e0a48f86a9470_EOF + GH_AW_PROMPT_eeb322738661ed58_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_603e0a48f86a9470_EOF' + cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -237,12 +237,12 @@ jobs: {{/if}} - GH_AW_PROMPT_603e0a48f86a9470_EOF + GH_AW_PROMPT_eeb322738661ed58_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_603e0a48f86a9470_EOF' + cat << 'GH_AW_PROMPT_eeb322738661ed58_EOF' {{#runtime-import .github/workflows/daily-repo-status.md}} - GH_AW_PROMPT_603e0a48f86a9470_EOF + GH_AW_PROMPT_eeb322738661ed58_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -352,7 +352,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -374,7 +374,7 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Create gh-aw temp directory @@ -450,9 +450,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_9b6646f0d620a78f_EOF' + 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_9b6646f0d620a78f_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_51571b44da85874d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -658,7 +658,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_d075a7f45ab51044_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_4b3a7789a6eea081_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -702,7 +702,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_d075a7f45ab51044_EOF + GH_AW_MCP_CONFIG_4b3a7789a6eea081_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -750,7 +750,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_AGENT_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: claude-haiku-4.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 @@ -957,7 +957,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1108,7 +1108,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1137,7 +1137,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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # --- Threat Detection --- @@ -1238,7 +1238,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-haiku-4.5 GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: v0.76.1 @@ -1312,7 +1312,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-haiku-4.5" GH_AW_ENGINE_VERSION: "1.0.52" GH_AW_WORKFLOW_ID: "daily-repo-status" GH_AW_WORKFLOW_NAME: "Repo Status" @@ -1330,7 +1330,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@46d564922b082d0db93244972e8005ea6904ee5f # v0.76.1 + uses: github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} diff --git a/.github/workflows/daily-repo-status.md b/.github/workflows/daily-repo-status.md index 8debf5708437..49b553940b8b 100644 --- a/.github/workflows/daily-repo-status.md +++ b/.github/workflows/daily-repo-status.md @@ -16,6 +16,10 @@ permissions: network: defaults +engine: + id: copilot + model: claude-haiku-4.5 + tools: github: # If in a public repo, setting `lockdown: false` allows diff --git a/.github/workflows/docker-cloudstack-simulator.yml b/.github/workflows/docker-cloudstack-simulator.yml index dc00968563d0..ac778a48af54 100644 --- a/.github/workflows/docker-cloudstack-simulator.yml +++ b/.github/workflows/docker-cloudstack-simulator.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Login to Docker Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ secrets.DOCKER_REGISTRY }} username: ${{ secrets.DOCKERHUB_USER }} @@ -47,7 +47,7 @@ jobs: - name: Set Docker repository name run: echo "DOCKER_REPOSITORY=apache" >> $GITHUB_ENV - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/issue-triage-agent.md b/.github/workflows/issue-triage-agent.md deleted file mode 100644 index 26c43644618c..000000000000 --- a/.github/workflows/issue-triage-agent.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -on: - schedule: daily around 14:00 on weekdays - workflow_dispatch: null -permissions: - issues: read -imports: -- github/gh-aw/.github/workflows/shared/reporting.md@359795d49ada21681ab616bd4cbcb144a7387115 -safe-outputs: - add-comment: {} - add-labels: - allowed: - - bug - - feature - - enhancement - - documentation - - question - - help-wanted - - good-first-issue -emoji: 🔧 -source: github/gh-aw/.github/workflows/issue-triage-agent.md@359795d49ada21681ab616bd4cbcb144a7387115 -strict: true -timeout-minutes: 5 -tools: - cli-proxy: true - github: - toolsets: - - issues - - labels ---- -# Issue Triage Agent - -List open issues in ${{ github.repository }} that have no labels. For each unlabeled issue, analyze the title and body, then add one of the allowed labels: `bug`, `feature`, `enhancement`, `documentation`, `question`, `help-wanted`, or `good-first-issue`. - -Skip issues that: -- Already have any of these labels -- Have been assigned to any user (especially non-bot users) - -After adding the label to an issue, mention the issue author in a comment using this format (follow shared/reporting.md guidelines): - -**Comment Template**: -```markdown -### 🏷️ Issue Triaged - -Hi @{author}! I've categorized this issue as **{label_name}** based on the following analysis: - -**Reasoning**: {brief_explanation_of_why_this_label} - -
-View Triage Details - -#### Analysis -- **Keywords detected**: {list_of_keywords_that_matched} -- **Issue type indicators**: {what_made_this_fit_the_category} -- **Confidence**: {High/Medium/Low} - -#### Recommended Next Steps -- {context_specific_suggestion_1} -- {context_specific_suggestion_2} - -
- -**References**: [Triage run §{run_id}](https://github.com/github/gh-aw/actions/runs/{run_id}) -``` - -**Key formatting requirements**: -- Use h3 (###) for the main heading -- Keep reasoning visible for quick understanding -- Wrap detailed analysis in `
` tags -- Include workflow run reference -- Keep total comment concise (collapsed details prevent noise) - -## Batch Comment Optimization - -For efficiency, if multiple issues are triaged in a single run: -1. Add individual labels to each issue -2. Add a brief comment to each issue (using the template above) -3. Optionally: Create a discussion summarizing all triage actions for that run - -This provides both per-issue context and batch visibility. - -## Labels - -- `bug`: Indicates a problem or error in the code that needs fixing. -- `feature`: Represents a new feature request or enhancement to existing functionality. -- `enhancement`: Suggests improvements to existing features or code. -- `documentation`: Pertains to issues related to documentation, such as missing or unclear docs. -- `question`: Used for issues that are asking for clarification or have questions about the project. -- `help-wanted`: Indicates that the issue is a good candidate for external contributions and help -- `good-first-issue`: Marks issues that are suitable for newcomers to the project, often with simpler scope. - -{{#runtime-import shared/noop-reminder.md}} diff --git a/.github/workflows/main-sonar-check.yml b/.github/workflows/main-sonar-check.yml index ad58d07a9894..f48409fbc0c3 100644 --- a/.github/workflows/main-sonar-check.yml +++ b/.github/workflows/main-sonar-check.yml @@ -31,7 +31,7 @@ jobs: name: Sonar JaCoCo Coverage runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 persist-credentials: false @@ -55,7 +55,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: mvn -B -P quality org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.branch.name=${{ github.ref_name }} - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: ./client/target/site/jacoco-aggregate/jacoco.xml fail_ci_if_error: true @@ -63,3 +63,5 @@ jobs: verbose: true name: codecov token: ${{ secrets.CODECOV_TOKEN }} + - name: Compute Coverage Grade + run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml diff --git a/.github/workflows/merge-conflict-checker.yml b/.github/workflows/merge-conflict-checker.yml index f23719f7183a..badf8c4b4c5b 100644 --- a/.github/workflows/merge-conflict-checker.yml +++ b/.github/workflows/merge-conflict-checker.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Conflict Check - uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" dirtyLabel: "status:has-conflicts" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 1ea00be9112f..4de995479c59 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check Out - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/rat.yml b/.github/workflows/rat.yml index a36a956856e7..f8b0aa149324 100644 --- a/.github/workflows/rat.yml +++ b/.github/workflows/rat.yml @@ -28,7 +28,7 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Setup Environment diff --git a/.github/workflows/sonar-check.yml b/.github/workflows/sonar-check.yml index 49a49d009580..fbb3cb9f540d 100644 --- a/.github/workflows/sonar-check.yml +++ b/.github/workflows/sonar-check.yml @@ -29,7 +29,7 @@ jobs: name: Sonar JaCoCo Coverage runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 persist-credentials: false @@ -57,7 +57,7 @@ jobs: HEADREF: ${{ github.event.pull_request.head.ref }} run: | mvn -B -P quality org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=apache_cloudstack -Dsonar.pullrequest.key="$PR_ID" -Dsonar.pullrequest.branch="$HEADREF" -Dsonar.pullrequest.github.repository=apache/cloudstack -Dsonar.pullrequest.provider=GitHub -Dsonar.pullrequest.github.summary_comment=true - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: ./client/target/site/jacoco-aggregate/jacoco.xml fail_ci_if_error: true @@ -65,3 +65,51 @@ jobs: verbose: true name: codecov token: ${{ secrets.CODECOV_TOKEN }} + - name: Compute Coverage Grade + id: grade + run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml + - name: Post Coverage Grade Comment on PR + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const grade = '${{ steps.grade.outputs.coverage_grade }}'; + const label = '${{ steps.grade.outputs.coverage_grade_label }}'; + const linePct = '${{ steps.grade.outputs.line_coverage }}'; + const branchPct = '${{ steps.grade.outputs.branch_coverage }}'; + const emojiMap = { A: '🟢', B: '🟡', C: '🟠', D: '🔴', F: '⛔' }; + const emoji = emojiMap[grade] ?? '❓'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + const branchRow = branchPct !== 'N/A' + ? `| Branch coverage | **${branchPct}%** |` + : ''; + + const body = [ + `## ${emoji} Test Coverage Grade: \`${grade}\` — ${label}`, + '', + '| Metric | Value |', + '|--------|-------|', + `| Line coverage | **${linePct}%** |`, + branchRow, + '', + '### Grade Scale', + '| Grade | Line Coverage | Meaning |', + '|-------|--------------|---------|', + '| 🟢 A | ≥ 80% | Excellent - this code sleeps well at night 😴 |', + '| 🟡 B | 60-79% | Good - almost there, don\'t stop now 😉 |', + '| 🟠 C | 40-59% | Acceptable - your code is wearing a seatbelt, but no airbags 😬 |', + '| 🔴 D | 20-39% | Marginal - boldly shipping where no test has gone before 🖖 |', + '| ⛔ F | < 20% | Failing - tests? what tests? 🔥 |', + '', + '> Branch coverage is shown as a secondary signal. Grade is determined by **line coverage**.', + `> [View full Actions run](${runUrl})`, + ].filter(l => l !== undefined).join('\n'); + + 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/ui.yml b/.github/workflows/ui.yml index ff27809db8f6..aeccb5624f72 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: npm run lint npm run test:unit - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 if: github.repository == 'apache/cloudstack' with: working-directory: ui diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 755ae125edf0..91537e25267e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -151,7 +151,7 @@ repos: ^server/src/test/resources/certs/rsa_self_signed\.key$| ^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$| ^systemvm/agent/certs/localhost\.key$| - ^systemvm/agent/certs/realhostip\.key$| + ^systemvm/agent/certs/systemvm\.key$| ^test/integration/smoke/test_ssl_offloading\.py$ - id: end-of-file-fixer exclude: \.vhd$|\.svg$ diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index ba4a3874664a..0a459d8d4fc5 100644 --- a/agent/conf/agent.properties +++ b/agent/conf/agent.properties @@ -78,6 +78,14 @@ zone=default # Generated with "uuidgen". local.storage.uuid= +# Enable TLS for image server transfers. The keys are read from: +# cert file = /etc/cloudstack/agent/cloud.crt +# key file = /etc/cloudstack/agent/cloud.key +image.server.tls.enabled=true + +# The Address for the network interface that the image server listens on. If not specified, it will listen on the Management network. +#image.server.listen.address= + # Location for KVM virtual router scripts. # The path defined in this property is relative to the directory "/usr/share/cloudstack-common/". domr.scripts.dir=scripts/network/domr/kvm diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index e69a7efdc9c7..18a6b6df1006 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -123,6 +123,20 @@ public class AgentProperties{ */ public static final Property LOCAL_STORAGE_PATH = new Property<>("local.storage.path", "/var/lib/libvirt/images/"); + /** + * Enables TLS on the KVM image server transfer endpoint.
+ * Data type: Boolean.
+ * Default value: true + */ + public static final Property IMAGE_SERVER_TLS_ENABLED = new Property<>("image.server.tls.enabled", true); + + /** + * The IP address that the KVM image server listens on.
+ * Data type: String.
+ * Default value: null + */ + public static final Property IMAGE_SERVER_LISTEN_ADDRESS = new Property<>("image.server.listen.address", null, String.class); + /** * Directory where Qemu sockets are placed.
* These sockets are for the Qemu Guest Agent and SSVM provisioning.
diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index b52348201516..8b14cfd3a390 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -63,6 +63,8 @@ public static String[] toStrings(Host.Type... types) { String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; String HOST_SSH_PORT = "host.ssh.port"; + String GUEST_OS_CATEGORY_ID = "guest.os.category.id"; + String GUEST_OS_RULE = "guest.os.rule"; int DEFAULT_SSH_PORT = 22; diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index 53692f932a4e..c32bb711c0f2 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -232,7 +232,7 @@ Network createPrivateNetwork(String networkName, String displayText, long physic /** * Requests an IP address for the guest NIC */ - NicSecondaryIp allocateSecondaryGuestIP(long nicId, IpAddresses requestedIpPair) throws InsufficientAddressCapacityException; + NicSecondaryIp allocateSecondaryGuestIP(long nicId, IpAddresses requestedIpPair, String description) throws InsufficientAddressCapacityException; boolean releaseSecondaryIpFromNic(long ipAddressId); diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 5b3e97698fda..ddf5978497ba 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -170,6 +170,7 @@ public static enum StoragePoolType { ISO(false, false, EncryptionSupport.Unsupported), // for iso image LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR CLVM(true, false, EncryptionSupport.Unsupported), + CLVM_NG(true, false, EncryptionSupport.Hypervisor), RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD SharedMountPoint(true, true, EncryptionSupport.Hypervisor), VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index 1a9bcc6ee98b..43ead8d287b9 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; +import com.cloud.dc.DataCenter; import com.cloud.exception.ResourceAllocationException; import com.cloud.offering.DiskOffering; import com.cloud.user.Account; @@ -70,6 +71,10 @@ public interface VolumeApiService { */ Volume allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationException; + Volume allocVolume(long ownerId, Long zoneId, Long diskOfferingId, Long vmId, Long snapshotId, String name, + Long cmdSize, Boolean displayVolume, Long cmdMinIops, Long cmdMaxIops, String customId) + throws ResourceAllocationException; + /** * Creates the volume based on the given criteria * @@ -80,6 +85,8 @@ public interface VolumeApiService { */ Volume createVolume(CreateVolumeCmd cmd); + Volume createVolume(long volumeId, Long vmId, Long snapshotId, Long storageId, Boolean display); + /** * Resizes the volume based on the given criteria * @@ -203,4 +210,6 @@ Volume updateVolume(long volumeId, String path, String state, Long storageId, Pair checkAndRepairVolume(CheckAndRepairVolumeCmd cmd) throws ResourceAllocationException; Long getVolumePhysicalSize(Storage.ImageFormat format, String path, String chainInfo); + + Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone, boolean encryptEnabledOnly); } diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 4145e2b89eb3..fc450e9179c5 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -88,10 +88,16 @@ User createUser(String userName, String password, String firstName, String lastN Account getActiveAccountById(long accountId); + Account getActiveAccountByUuid(String accountUuid); + Account getAccount(long accountId); + Account getAccountByUuid(String accountUuid); + User getActiveUser(long userId); + User getOneActiveUserForAccount(Account account); + User getUserIncludingRemoved(long userId); boolean isRootAdmin(Long accountId); diff --git a/api/src/main/java/com/cloud/vm/NicSecondaryIp.java b/api/src/main/java/com/cloud/vm/NicSecondaryIp.java index 2856e0aea756..d25627c1782d 100644 --- a/api/src/main/java/com/cloud/vm/NicSecondaryIp.java +++ b/api/src/main/java/com/cloud/vm/NicSecondaryIp.java @@ -38,6 +38,8 @@ public interface NicSecondaryIp extends ControlledEntity, Identity, InternalIden String getIp6Address(); + String getDescription(); + long getNetworkId(); long getVmId(); diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 9e56bf4f17b2..33cc6da70812 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -130,4 +130,10 @@ public interface VmDetailConstants { String EXTERNAL_DETAIL_PREFIX = "External:"; String CLOUDSTACK_VM_DETAILS = "cloudstack.vm.details"; String CLOUDSTACK_VLAN = "cloudstack.vlan"; + + // KVM Checkpoints related + String ACTIVE_CHECKPOINT_ID = "active.checkpoint.id"; + String ACTIVE_CHECKPOINT_CREATE_TIME = "active.checkpoint.create.time"; + String LAST_CHECKPOINT_ID = "last.checkpoint.id"; + String LAST_CHECKPOINT_CREATE_TIME = "last.checkpoint.create.time"; } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 694830ea2f36..2150cfb2200f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -79,6 +79,7 @@ public class ApiConstants { public static final String BOOTABLE = "bootable"; public static final String BIND_DN = "binddn"; public static final String BIND_PASSWORD = "bindpass"; + public static final String BLANK_INSTANCE = "blankinstance"; public static final String BUS_ADDRESS = "busaddress"; public static final String BYTES_READ_RATE = "bytesreadrate"; public static final String BYTES_READ_RATE_MAX = "bytesreadratemax"; @@ -220,6 +221,7 @@ public class ApiConstants { public static final String DOMAIN_PATH = "domainpath"; public static final String DOMAIN_ID = "domainid"; public static final String DOMAIN__ID = "domainId"; + public static final String DUMMY = "dummy"; public static final String DURATION = "duration"; public static final String ELIGIBLE = "eligible"; public static final String EMAIL = "email"; @@ -263,6 +265,7 @@ public class ApiConstants { public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork"; public static final String FOR_SYSTEM_VMS = "forsystemvms"; public static final String FOR_PROVIDER = "forprovider"; + public static final String FROM_CHECKPOINT_ID = "fromcheckpointid"; public static final String FULL_PATH = "fullpath"; public static final String GATEWAY = "gateway"; public static final String IP6_GATEWAY = "ip6gateway"; @@ -335,6 +338,7 @@ public class ApiConstants { public static final String IS_2FA_VERIFIED = "is2faverified"; public static final String IS_2FA_MANDATED = "is2famandated"; + public static final String IS_ACTIVE = "isactive"; public static final String IS_ASYNC = "isasync"; public static final String IP_AVAILABLE = "ipavailable"; public static final String IP_LIMIT = "iplimit"; @@ -445,6 +449,7 @@ public class ApiConstants { public static final String MAX_VGPU_PER_PHYSICAL_GPU = "maxvgpuperphysicalgpu"; public static final String GUEST_OS_LIST = "guestoslist"; public static final String GUEST_OS_COUNT = "guestoscount"; + public static final String GUEST_OS_RULE = "guestosrule"; public static final String OS_MAPPING_CHECK_ENABLED = "osmappingcheckenabled"; public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate"; public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled"; @@ -598,6 +603,8 @@ public class ApiConstants { public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine"; public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot"; public static final String TARGET_IQN = "targetiqn"; + public static final String TARIFF_ID = "tariffid"; + public static final String TARIFF_NAME = "tariffname"; public static final String TASKS_FILTER = "tasksfilter"; public static final String TEMPLATE_FILTER = "templatefilter"; public static final String TEMPLATE_ID = "templateid"; @@ -611,6 +618,7 @@ public class ApiConstants { public static final String TENANT_NAME = "tenantname"; public static final String TOTAL = "total"; public static final String TOTAL_SUBNETS = "totalsubnets"; + public static final String TO_CHECKPOINT_ID = "tocheckpointid"; public static final String TOTAL_QUOTA = "totalquota"; public static final String TYPE = "type"; public static final String TRUST_STORE = "truststore"; @@ -658,6 +666,7 @@ public class ApiConstants { public static final String VIRTUAL_MACHINE_STATE = "vmstate"; public static final String VIRTUAL_MACHINES = "virtualmachines"; public static final String USAGE_ID = "usageid"; + public static final String USAGE_NAME = "usagename"; public static final String USAGE_TYPE = "usagetype"; public static final String INCLUDE_TAGS = "includetags"; @@ -874,6 +883,7 @@ public class ApiConstants { public static final String IS_SOURCE_NAT = "issourcenat"; public static final String IS_STATIC_NAT = "isstaticnat"; public static final String ITERATIONS = "iterations"; + public static final String ITEMS = "items"; public static final String SORT_BY = "sortby"; public static final String CHANGE_CIDR = "changecidr"; public static final String PURPOSE = "purpose"; @@ -1333,8 +1343,10 @@ public class ApiConstants { public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement"; public static final String VNF_CIDR_LIST = "vnfcidrlist"; + public static final String AUTHORIZE_URL = "authorizeurl"; public static final String CLIENT_ID = "clientid"; public static final String REDIRECT_URI = "redirecturi"; + public static final String TOKEN_URL = "tokenurl"; public static final String IS_TAG_A_RULE = "istagarule"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java index 18c96c371591..1ee41ac86c22 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiServerService.java @@ -21,8 +21,11 @@ import javax.servlet.http.HttpSession; +import org.apache.cloudstack.context.CallContext; + import com.cloud.domain.Domain; import com.cloud.exception.CloudAuthenticationException; +import com.cloud.user.Account; import com.cloud.user.UserAccount; public interface ApiServerService { @@ -52,4 +55,20 @@ public ResponseObject loginUser(HttpSession session, String username, String pas String getDomainId(Map params); boolean isPostRequestsAndTimestampsEnforced(); + + AsyncCmdResult processAsyncCmd(BaseAsyncCmd cmdObj, Map params, CallContext ctx, Long callerUserId, Account caller) throws Exception; + + class AsyncCmdResult { + public final Long objectId; + public final String objectUuid; + public final BaseAsyncCmd asyncCmd; + public final long jobId; + + public AsyncCmdResult(Long objectId, String objectUuid, BaseAsyncCmd asyncCmd, long jobId) { + this.objectId = objectId; + this.objectUuid = objectUuid; + this.asyncCmd = asyncCmd; + this.jobId = jobId; + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java index 30baa71d6d85..b9b13dcfd884 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseAsyncCreateCustomIdCmd.java @@ -20,7 +20,7 @@ public abstract class BaseAsyncCreateCustomIdCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.CUSTOM_ID, type = CommandType.STRING, description = "An optional field, in case you want to set a custom id to the resource. Allowed to Root Admins only") - private String customId; + protected String customId; public String getCustomId() { return customId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java new file mode 100644 index 000000000000..c98bfb850529 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CreateImageTransferCmd.java @@ -0,0 +1,100 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.VolumeResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createImageTransfer", + description = "Create image transfer for a disk in backup. This API is intended for testing only and is disabled by default.", + responseObject = ImageTransferResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class CreateImageTransferCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.BACKUP_ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + description = "ID of the backup") + private Long backupId; + + @Parameter(name = ApiConstants.VOLUME_ID, + type = CommandType.UUID, + entityType = VolumeResponse.class, + required = true, + description = "ID of the disk/volume") + private Long volumeId; + + @Parameter(name = ApiConstants.DIRECTION, + type = CommandType.STRING, + required = true, + description = "Direction of the transfer: upload, download") + private String direction; + + @Parameter(name = ApiConstants.FORMAT, + type = CommandType.STRING, + description = "Format for the image transfer: raw/cow. 'raw' will create an NBD backend. 'cow' will use the File backend." + + "For download, only the 'raw' format is supported. Default: raw") + private String format; + + public Long getBackupId() { + return backupId; + } + + public Long getVolumeId() { + return volumeId; + } + + public ImageTransfer.Direction getDirection() { + return ImageTransfer.Direction.valueOf(direction); + } + + public ImageTransfer.Format getFormat() { + return EnumUtils.getEnum(ImageTransfer.Format.class, format); + } + + @Override + public void execute() { + ImageTransferResponse response = kvmBackupExportService.createImageTransfer(this); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java new file mode 100644 index 000000000000..d0e17e86d427 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteVmCheckpointCmd.java @@ -0,0 +1,85 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "deleteVirtualMachineCheckpoint", + description = "Delete a VM checkpoint. This API is intended for testing only and is disabled by default.", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class DeleteVmCheckpointCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = "checkpointid", + type = CommandType.STRING, + required = true, + description = "Checkpoint ID") + private String checkpointId; + + public Long getVmId() { + return vmId; + } + + public String getCheckpointId() { + return checkpointId; + } + + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public void setCheckpointId(String checkpointId) { + this.checkpointId = checkpointId; + } + + @Override + public void execute() { + boolean result = kvmBackupExportService.deleteVmCheckpoint(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java new file mode 100644 index 000000000000..45173f8668ee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeBackupCmd.java @@ -0,0 +1,103 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "finalizeBackup", + description = "Finalize a VM backup session. This API is intended for testing only and is disabled by default.", + responseObject = BackupResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class FinalizeBackupCmd extends BaseAsyncCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Inject + private BackupManager backupManager; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + required = true, + description = "ID of the backup") + private Long backupId; + + public Long getVmId() { + return vmId; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public void execute() { + Backup backup = kvmBackupExportService.finalizeBackup(this); + + if (backup == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Backup"); + } + + BackupResponse response = backupManager.createBackupResponse(backup, null); + + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CREATE; + } + + @Override + public String getEventDescription() { + return "Finalizing backup " + backupId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java new file mode 100644 index 000000000000..dfc43e233bf2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/FinalizeImageTransferCmd.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "finalizeImageTransfer", + description = "Finalize an image transfer. This API is intended for testing only and is disabled by default.", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class FinalizeImageTransferCmd extends BaseCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ImageTransferResponse.class, + required = true, + description = "ID of the image transfer") + private Long imageTransferId; + + public Long getImageTransferId() { + return imageTransferId; + } + + @Override + public void execute() { + boolean result = kvmBackupExportService.finalizeImageTransfer(this); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java new file mode 100644 index 000000000000..d810d21ab5f8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListImageTransfersCmd.java @@ -0,0 +1,81 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "listImageTransfers", + description = "List image transfers for a backup. This API is intended for testing only and is disabled by default.", + responseObject = ImageTransferResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class ListImageTransfersCmd extends BaseListCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ImageTransferResponse.class, + description = "ID of the Image Transfer") + private Long id; + + @Parameter(name = ApiConstants.BACKUP_ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + description = "ID of the backup") + private Long backupId; + + public Long getId() { + return id; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public void execute() { + List responses = kvmBackupExportService.listImageTransfers(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses); + response.setObjectName(ImageTransfer.class.getSimpleName().toLowerCase()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java new file mode 100644 index 000000000000..a61661e982de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListVmCheckpointsCmd.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.CheckpointResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.KVMBackupExportService; + +@APICommand(name = "listVirtualMachineCheckpoints", + description = "List checkpoints for a VM. This API is intended for testing only and is disabled by default.", + responseObject = CheckpointResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) +public class ListVmCheckpointsCmd extends BaseListCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + public Long getVmId() { + return vmId; + } + + @Override + public void execute() { + List responses = kvmBackupExportService.listVmCheckpoints(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return 0; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java new file mode 100644 index 000000000000..1bf6d45db049 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/StartBackupCmd.java @@ -0,0 +1,120 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.KVMBackupExportService; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "startBackup", + description = "Start a VM backup session using pull mode backup-begin on the KVM host. This API is intended for testing only and is disabled by default.", + responseObject = BackupResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin}) + public class StartBackupCmd extends BaseAsyncCreateCmd implements AdminCmd { + + @Inject + private KVMBackupExportService kvmBackupExportService; + + @Inject + private BackupManager backupManager; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + description = "the name of the backup") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, + type = CommandType.STRING, + description = "the description for the backup") + private String description; + + public Long getVmId() { + return vmId; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + @Override + public void execute() { + try { + Backup backup = kvmBackupExportService.startBackup(this); + BackupResponse response = backupManager.createBackupResponse(backup, null); + + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public void create() { + Backup backup = kvmBackupExportService.createBackup(this); + + if (backup != null) { + setEntityId(backup.getId()); + setEntityUuid(backup.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create Backup"); + } + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CREATE; + } + + @Override + public String getEventDescription() { + return "Starting backup for Instance " + vmId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java index f6f66415f533..055443934609 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ListCfgsByCmd.java @@ -19,23 +19,23 @@ import java.util.ArrayList; import java.util.List; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.commons.lang3.StringUtils; - import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.commons.lang3.StringUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.Pair; @@ -94,6 +94,13 @@ public class ListCfgsByCmd extends BaseListCmd { description = "The ID of the Image Store to update the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + @Parameter(name = ApiConstants.GROUP, type = CommandType.STRING, description = "Lists configuration by group name (primarily used for UI)", since = "4.18.0") private String groupName; @@ -139,6 +146,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + public String getGroupName() { return groupName; } @@ -200,6 +211,9 @@ private void setScope(ConfigurationResponse cfgResponse) { if (getImageStoreId() != null){ cfgResponse.setScope("imagestore"); } + if (getManagementServerId() != null){ + cfgResponse.setScope("managementserver"); + } } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java index 2d511cff34db..3c3c36b29d7e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java @@ -23,16 +23,16 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.ImageStoreResponse; -import org.apache.cloudstack.framework.config.ConfigKey; - import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -84,6 +84,13 @@ public class ResetCfgCmd extends BaseCmd { description = "The ID of the Image Store to reset the parameter value for corresponding image store") private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -116,6 +123,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -149,6 +160,9 @@ public void execute() { if (getImageStoreId() != null) { response.setScope(ConfigKey.Scope.ImageStore.name()); } + if (getManagementServerId() != null) { + response.setScope(ConfigKey.Scope.ManagementServer.name()); + } response.setValue(cfg.second()); this.setResponseObject(response); } else { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java index 97dee8f638af..9db9529dc8d8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java @@ -16,9 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.admin.config; -import com.cloud.utils.crypt.DBEncryptionUtil; import org.apache.cloudstack.acl.RoleService; -import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiArgValidator; import org.apache.cloudstack.api.ApiConstants; @@ -29,13 +27,17 @@ import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConfigurationResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ImageStoreResponse; +import org.apache.cloudstack.api.response.ManagementServerResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.commons.lang3.StringUtils; import com.cloud.user.Account; +import com.cloud.utils.crypt.DBEncryptionUtil; @APICommand(name = "updateConfiguration", description = "Updates a configuration.", responseObject = ConfigurationResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -88,6 +90,13 @@ public class UpdateCfgCmd extends BaseCmd { validations = ApiArgValidator.PositiveNumber) private Long imageStoreId; + @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, + type = CommandType.UUID, + entityType = ManagementServerResponse.class, + description = "the ID of the Management Server to update the parameter value for corresponding management server", + since = "4.23.0") + private Long managementServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -112,7 +121,7 @@ public Long getClusterId() { return clusterId; } - public Long getStoragepoolId() { + public Long getStoragePoolId() { return storagePoolId; } @@ -128,6 +137,10 @@ public Long getImageStoreId() { return imageStoreId; } + public Long getManagementServerId() { + return managementServerId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -182,7 +195,7 @@ public ConfigurationResponse setResponseScopes(ConfigurationResponse response) { if (getClusterId() != null) { response.setScope("cluster"); } - if (getStoragepoolId() != null) { + if (getStoragePoolId() != null) { response.setScope("storagepool"); } if (getAccountId() != null) { @@ -191,6 +204,9 @@ public ConfigurationResponse setResponseScopes(ConfigurationResponse response) { if (getDomainId() != null) { response.setScope("domain"); } + if (getManagementServerId() != null) { + response.setScope(ConfigKey.Scope.ManagementServer.name().toLowerCase()); + } return response; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index c085abd42c76..69bca7c79e21 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -49,9 +49,14 @@ public class UpdateHostCmd extends BaseCmd { @Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, - description = "The ID of OS category to update the host with") + description = "the ID of OS category used to prioritize VMs with matching OS category during the allocation process. " + + "It cannot be used alongside the 'guestosrule' parameter.") private Long osCategoryId; + @Parameter(name = ApiConstants.GUEST_OS_RULE, type = CommandType.STRING, description = "the guest OS rule written in JavaScript to match with the OS of the VM." + + "It cannot be used alongside the 'oscategoryid' parameter.") + private String guestOsRule; + @Parameter(name = ApiConstants.ALLOCATION_STATE, type = CommandType.STRING, description = "Change resource state of host, valid values are [Enable, Disable]. Operation may failed if host in states not allowing Enable/Disable") @@ -96,6 +101,10 @@ public Long getOsCategoryId() { return osCategoryId; } + public String getGuestOsRule() { + return guestOsRule; + } + public String getAllocationState() { return allocationState; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java index 11d7c1d2ffab..28c79517f4b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java @@ -50,7 +50,7 @@ public class RegisterUserKeysCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "API key pair name.") private String name; - @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.") + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "API key pair description.", length = 1024) private String description; @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the API key pair. " + @@ -138,6 +138,9 @@ public List> getRules() { String description = detail.get(ApiConstants.DESCRIPTION); if (StringUtils.isNotEmpty(description)) { + if (description.length() > 255) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Rule description cannot be longer than 255 characters."); + } ruleDetails.put(ApiConstants.DESCRIPTION, description); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java index e11d20d06466..50ff97ac676f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java @@ -85,6 +85,9 @@ public class AssignVMCmd extends BaseCmd { "In case no security groups are provided the Instance is part of the default security group.") private List securityGroupIdList; + // Internal flag to allow assignment without adding a network + private boolean skipNetwork = false; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -113,6 +116,34 @@ public List getSecurityGroupIdList() { return securityGroupIdList; } + public boolean isSkipNetwork() { + return skipNetwork; + } + + ///////////////////////////////////////////////////// + /////////////////// Setters ///////////////////////// + ///////////////////////////////////////////////////// + + public void setVirtualMachineId(Long virtualMachineId) { + this.virtualMachineId = virtualMachineId; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + public void setSkipNetwork(boolean skipNetwork) { + this.skipNetwork = skipNetwork; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java index e64c8b3f46c6..b7c3b2ba41a6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java @@ -41,6 +41,19 @@ public class DeployVMCmdByAdmin extends DeployVMCmd implements AdminCmd { @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "Destination Cluster ID to deploy the Instance to - parameter available for root admin only", since = "4.13") private Long clusterId; + @Parameter(name = ApiConstants.BLANK_INSTANCE, + type = CommandType.BOOLEAN, + description = "Whether to create a blank instance without storage and network", + since = "4.23.0") + private Boolean blankInstance; + + // Internal flag to allow deploying instance with a given type + private String instanceType; + + ///////////////////////////////////////////////////// + ////////////////// Getters ////////////////////////// + ///////////////////////////////////////////////////// + public Long getPodId() { return podId; } @@ -48,4 +61,37 @@ public Long getPodId() { public Long getClusterId() { return clusterId; } + + @Override + public boolean isBlankInstance() { + return Boolean.TRUE.equals(blankInstance); + } + + @Override + public String getInstanceType() { + if (!isBlankInstance()) { + return null; + } + return instanceType; + } + + ///////////////////////////////////////////////////// + ////////////////// Setters ////////////////////////// + ///////////////////////////////////////////////////// + + public void setClusterId(Long clusterId) { + this.clusterId = clusterId; + } + + public void setBlankInstance(boolean blankInstance) { + this.blankInstance = blankInstance; + } + + public void setInstanceType(String instanceType) { + this.instanceType = instanceType; + } + + public void setCustomId(String customId) { + this.customId = customId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java index 93d4b610b900..cbe6494d4004 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DestroyVMCmdByAdmin.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.command.admin.vm; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.command.admin.AdminCmd; import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; @@ -27,4 +29,20 @@ @APICommand(name = "destroyVirtualMachine", description = "Destroys an Instance. Once destroyed, only the administrator can recover it.", responseObject = UserVmResponse.class, responseView = ResponseView.Full, entityType = {VirtualMachine.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = true) -public class DestroyVMCmdByAdmin extends DestroyVMCmd implements AdminCmd {} +public class DestroyVMCmdByAdmin extends DestroyVMCmd implements AdminCmd { + + @Parameter( name = ApiConstants.FORCED, + type = CommandType.BOOLEAN, + description = "Force destroy the Instance", + since = "4.23.0") + Boolean forced; + + @Override + public boolean isForced() { + return Boolean.TRUE.equals(forced); + } + + public void setForced(Boolean forced) { + this.forced = forced; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java index 5c5c8776bce3..cd6baaf827bb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java @@ -193,6 +193,22 @@ public Boolean getGpuEnabled() { return gpuEnabled; } + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public void setCpuNumber(Integer cpuNumber) { + this.cpuNumber = cpuNumber; + } + + public void setMemory(Integer memory) { + this.memory = memory; + } + + public void setEncryptRoot(Boolean encryptRoot) { + this.encryptRoot = encryptRoot; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java index 6274e7e14963..9d96f104885e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddIpToVmNicCmd.java @@ -56,6 +56,9 @@ public class AddIpToVmNicCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.IP_ADDRESS, type = CommandType.STRING, required = false, description = "Secondary IP Address") private String ipAddr; + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the secondary IP address", length = 2048) + private String description; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -160,7 +163,7 @@ public void create() throws ResourceAllocationException { } try { - result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair); + result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair, description); if (result != null) { setEntityId(result.getId()); setEntityUuid(result.getUuid()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java index 6347c38811e8..f6ef955956f6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/AddNicToVMCmd.java @@ -100,6 +100,26 @@ public String getMacAddress() { return NetUtils.standardizeMacAddress(macaddr); } + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public void setNetworkId(Long netId) { + this.netId = netId; + } + + public void setIpaddr(String ipaddr) { + this.ipaddr = ipaddr; + } + + public void setMacAddress(String macaddr) { + this.macaddr = macaddr; + } + + public void setDhcpOptions(Map dhcpOptions) { + this.dhcpOptions = dhcpOptions; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java index 8c29d7338b85..68b0821ba44d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/BaseDeployVMCmd.java @@ -61,10 +61,10 @@ import com.cloud.network.Network.IpAddresses; import com.cloud.offering.DiskOffering; import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.net.Dhcp; import com.cloud.utils.net.NetUtils; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.VmDiskInfo; -import com.cloud.utils.net.Dhcp; public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityGroupAction, UserCmd { @@ -75,13 +75,13 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme ///////////////////////////////////////////////////// @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, description = "availability zone for the virtual machine") - private Long zoneId; + protected Long zoneId; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "host name for the virtual machine", validations = {ApiArgValidator.RFCComplianceDomainName}) - private String name; + protected String name; @Parameter(name = ApiConstants.DISPLAY_NAME, type = CommandType.STRING, description = "an optional user generated name for the virtual machine") - private String displayName; + protected String displayName; @Parameter(name=ApiConstants.PASSWORD, type=CommandType.STRING, description="The password of the virtual machine. If null, a random password will be generated for the VM.", since="4.19.0.0") @@ -89,21 +89,21 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme //Owner information @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "an optional account for the virtual machine. Must be used with domainId.") - private String accountName; + protected String accountName; @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "an optional domainId for the virtual machine. If the account parameter is used, domainId must also be used. If account is NOT provided then virtual machine will be assigned to the caller account and domain.") - private Long domainId; + protected Long domainId; //Network information //@ACL(accessType = AccessType.UseEntry) @Parameter(name = ApiConstants.NETWORK_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = NetworkResponse.class, description = "list of network ids used by virtual machine. Can't be specified with ipToNetworkList parameter") - private List networkIds; + protected List networkIds; @Parameter(name = ApiConstants.BOOT_TYPE, type = CommandType.STRING, required = false, description = "Guest VM Boot option either custom[UEFI] or default boot [BIOS]. Not applicable with VMware if the template is marked as deploy-as-is, as we honour what is defined in the template.", since = "4.14.0.0") - private String bootType; + protected String bootType; @Parameter(name = ApiConstants.BOOT_MODE, type = CommandType.STRING, required = false, description = "Boot Mode [Legacy] or [Secure] Applicable when Boot Type Selected is UEFI, otherwise Legacy only for BIOS. Not applicable with VMware if the template is marked as deploy-as-is, as we honour what is defined in the template.", since = "4.14.0.0") - private String bootMode; + protected String bootMode; @Parameter(name = ApiConstants.BOOT_INTO_SETUP, type = CommandType.BOOLEAN, required = false, description = "Boot into hardware setup or not (ignored if startVm = false, only valid for vmware)", since = "4.15.0.0") private Boolean bootIntoSetup; @@ -138,7 +138,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @Parameter(name = ApiConstants.HYPERVISOR, type = CommandType.STRING, description = "the hypervisor on which to deploy the virtual machine. " + "The parameter is required and respected only when hypervisor info is not set on the ISO/Template passed to the call") - private String hypervisor; + protected String hypervisor; @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, description = "an optional binary data that can be sent to the virtual machine upon a successful deployment. " + @@ -147,10 +147,10 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme "Using HTTP POST (via POST body), you can send up to 1MB of data after base64 encoding. " + "You also need to change vm.userdata.max.length value", length = 1048576) - private String userData; + protected String userData; @Parameter(name = ApiConstants.USER_DATA_ID, type = CommandType.UUID, entityType = UserDataResponse.class, description = "the ID of the Userdata", since = "4.18") - private Long userdataId; + protected Long userdataId; @Parameter(name = ApiConstants.USER_DATA_DETAILS, type = CommandType.MAP, description = "used to specify the parameters values for the variables in userdata.", since = "4.18") private Map userdataDetails; @@ -160,7 +160,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private String sshKeyPairName; @Parameter(name = ApiConstants.SSH_KEYPAIRS, type = CommandType.LIST, collectionType = CommandType.STRING, since="4.17", description = "names of the ssh key pairs used to login to the virtual machine") - private List sshKeyPairNames; + protected List sshKeyPairNames; @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "destination Host ID to deploy the VM to - parameter available for root admin only") private Long hostId; @@ -168,7 +168,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @ACL @Parameter(name = ApiConstants.SECURITY_GROUP_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = SecurityGroupResponse.class, description = "comma separated list of security groups id that going to be applied to the virtual machine. " + "Should be passed only when vm is created from a zone with Basic Network support." + " Mutually exclusive with securitygroupnames parameter") - private List securityGroupIdList; + protected List securityGroupIdList; @ACL @Parameter(name = ApiConstants.SECURITY_GROUP_NAMES, type = CommandType.LIST, collectionType = CommandType.STRING, entityType = SecurityGroupResponse.class, description = "comma separated list of security groups names that going to be applied to the virtual machine." @@ -189,10 +189,10 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private String macAddress; @Parameter(name = ApiConstants.KEYBOARD, type = CommandType.STRING, description = "an optional keyboard device type for the virtual machine. valid value can be one of de,de-ch,es,es-latam,fi,fr,fr-be,fr-ch,is,it,jp,nl-be,no,pt,uk,us") - private String keyboard; + protected String keyboard; @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Deploy vm for the project") - private Long projectId; + protected Long projectId; @Parameter(name = ApiConstants.START_VM, type = CommandType.BOOLEAN, description = "true if start vm after creating; defaulted to true if not specified") private Boolean startVm; @@ -200,7 +200,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme @ACL @Parameter(name = ApiConstants.AFFINITY_GROUP_IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = AffinityGroupResponse.class, description = "comma separated list of affinity groups id that are going to be applied to the virtual machine." + " Mutually exclusive with affinitygroupnames parameter") - private List affinityGroupIdList; + protected List affinityGroupIdList; @ACL @Parameter(name = ApiConstants.AFFINITY_GROUP_NAMES, type = CommandType.LIST, collectionType = CommandType.STRING, entityType = AffinityGroupResponse.class, description = "comma separated list of affinity groups names that are going to be applied to the virtual machine." @@ -208,10 +208,10 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private List affinityGroupNameList; @Parameter(name = ApiConstants.DISPLAY_VM, type = CommandType.BOOLEAN, since = "4.2", description = "an optional field, whether to the display the vm to the end user or not.", authorized = {RoleType.Admin}) - private Boolean displayVm; + protected Boolean displayVm; @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, since = "4.3", description = "used to specify the custom parameters. 'extraconfig' is not allowed to be passed in details") - private Map details; + protected Map details; @Parameter(name = ApiConstants.DEPLOYMENT_PLANNER, type = CommandType.STRING, description = "Deployment planner to use for vm allocation. Available to ROOT admin only", since = "4.4", authorized = { RoleType.Admin }) private String deploymentPlanner; @@ -225,7 +225,7 @@ public abstract class BaseDeployVMCmd extends BaseAsyncCreateCustomIdCmd impleme private Map dataDiskTemplateToDiskOfferingList; @Parameter(name = ApiConstants.EXTRA_CONFIG, type = CommandType.STRING, since = "4.12", description = "an optional URL encoded string that can be passed to the virtual machine upon successful deployment", length = 5120) - private String extraConfig; + protected String extraConfig; @Parameter(name = ApiConstants.COPY_IMAGE_TAGS, type = CommandType.BOOLEAN, since = "4.13", description = "if true the image tags (if any) will be copied to the VM, default value is false") private Boolean copyImageTags; @@ -798,6 +798,11 @@ public IoDriverPolicy getIoDriverPolicy() { } return null; } + + public String getInstanceType() { + return null; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 050592b97a3b..0a943fab118d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -16,10 +16,11 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Stream; -import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -40,6 +41,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.uservm.UserVm; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; @APICommand(name = "deployVirtualMachine", description = "Creates and automatically starts an Instance based on a service offering, disk offering, and Template.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class}, @@ -88,6 +90,111 @@ public boolean isVolumeOrSnapshotProvided() { return volumeId != null || snapshotId != null; } + public boolean isBlankInstance() { + return false; + } + + + + ///////////////////////////////////////////////////// + ////////////////// Setters ////////////////////////// + ///////////////////////////////////////////////////// + public void setZoneId(Long zoneId) { + this.zoneId = zoneId; + } + + public void setName(String name) { + this.name = name; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public void setNetworkIds(List networkIds) { + this.networkIds = networkIds; + } + + public void setBootType(String bootType) { + this.bootType = bootType; + } + + public void setBootMode(String bootMode) { + this.bootMode = bootMode; + } + + public void setHypervisor(String hypervisor) { + this.hypervisor = hypervisor; + } + + public void setUserData(String userData) { + this.userData = userData; + } + + public void setKeyboard(String keyboard) { + this.keyboard = keyboard; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + public void setDisplayVm(Boolean displayVm) { + this.displayVm = displayVm; + } + + public void setUserDataId(Long userDataId) { + this.userdataId = userDataId; + } + + public void setAffinityGroupIds(List ids) { + this.affinityGroupIdList = ids; + } + + public void setDetails(Map details) { + this.details = details; + } + + public void setExtraConfig(String extraConfig) { + this.extraConfig = extraConfig; + } + + public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) { + this.dynamicScalingEnabled = dynamicScalingEnabled; + } + + public void setServiceOfferingId(Long serviceOfferingId) { + this.serviceOfferingId = serviceOfferingId; + } + + public void setTemplateId(Long templateId) { + this.templateId = templateId; + } + + public void setVolumeId(Long volumeId) { + this.volumeId = volumeId; + } + + public void setSnapshotId(Long snapshotId) { + this.snapshotId = snapshotId; + } + + public void setSshKeyPairNames(List sshKeyPairNames) { + this.sshKeyPairNames = sshKeyPairNames; + } + + public void setSecurityGroupList(List securityGroupIdList) { + this.securityGroupIdList = securityGroupIdList; + } + @Override public void execute() { UserVm result; @@ -132,7 +239,7 @@ public void execute() { @Override public void create() throws ResourceAllocationException { - if (Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) { + if (!isBlankInstance() && Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) { throw new CloudRuntimeException("Please provide only one of the following parameters - template ID, volume ID or snapshot ID"); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 9e2f2bcb72ce..aec0688f1779 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -90,6 +90,10 @@ public List getVolumeIds() { return volumeIds; } + public boolean isForced() { + return false; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java index e3ad0502f454..a5f06e6ecbb3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java @@ -288,6 +288,14 @@ public boolean isCleanupExtraConfig() { return Boolean.TRUE.equals(cleanupExtraConfig); } + public void setId(Long id) { + this.id = id; + } + + public void setSecurityGroupIdList(List securityGroupIdList) { + this.securityGroupIdList = securityGroupIdList; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java index f39853512281..f50abaf73c96 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/AssignVolumeCmd.java @@ -70,6 +70,21 @@ public Long getProjectid() { return projectid; } + ///////////////////////////////////////////////////// + /////////////////// Setter/////////////////////////// + ///////////////////////////////////////////////////// + public void setVolumeId(Long volumeId) { + this.volumeId = volumeId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public void setProjectId(Long projectid) { + this.projectid = projectid; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java index 78de05648486..ec7a626fa156 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java @@ -114,7 +114,8 @@ public class CreateVolumeCmd extends BaseAsyncCreateCustomIdCmd implements UserC type = CommandType.UUID, entityType = StoragePoolResponse.class, description = "Storage pool ID to create the volume in. Cannot be used with the snapshotid parameter.", - authorized = {RoleType.Admin}) + authorized = {RoleType.Admin}, + since = "4.22.1") private Long storageId; ///////////////////////////////////////////////////// @@ -150,6 +151,10 @@ public Long getMaxIops() { } public Long getSnapshotId() { + if (storageId != null && snapshotId != null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Snapshot ID cannot be specified with the Storage ID."); + } return snapshotId; } @@ -163,7 +168,8 @@ private Long getProjectId() { public Long getStorageId() { if (snapshotId != null && storageId != null) { - throw new IllegalArgumentException("StorageId parameter cannot be specified with the SnapshotId parameter."); + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Storage ID cannot be specified with the Snapshot ID."); } return storageId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java index 66a558abf982..f6e811da6055 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/DetachVolumeCmd.java @@ -77,6 +77,10 @@ public Long getVirtualMachineId() { return virtualMachineId; } + public void setId(Long id) { + this.id = id; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java index b855bfe40b8d..51fcaa9836e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -127,6 +127,18 @@ public class BackupResponse extends BaseResponse { @Param(description = "Indicates whether the VM from which the backup was taken is expunged or not", since = "4.22.0") private Boolean isVmExpunged; + @SerializedName(ApiConstants.FROM_CHECKPOINT_ID) + @Param(description = "Previous active checkpoint ID for incremental backups", since = "4.23.0") + private String fromCheckpointId; + + @SerializedName(ApiConstants.TO_CHECKPOINT_ID) + @Param(description = "Next checkpoint ID for incremental backups", since = "4.23.0") + private String toCheckpointId; + + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "Host ID where the backup is running", since = "4.23.0") + private String hostId; + public String getId() { return id; } @@ -314,4 +326,28 @@ public void setVmOfferingRemoved(Boolean vmOfferingRemoved) { public void setVmExpunged(Boolean isVmExpunged) { this.isVmExpunged = isVmExpunged; } + + public void setFromCheckpointId(String fromCheckpointId) { + this.fromCheckpointId = fromCheckpointId; + } + + public String getFromCheckpointId() { + return this.fromCheckpointId; + } + + public void setToCheckpointId(String toCheckpointId) { + this.toCheckpointId = toCheckpointId; + } + + public String getToCheckpointId() { + return this.toCheckpointId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } + + public String getHostId() { + return this.hostId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java new file mode 100644 index 000000000000..2bec7711064f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/CheckpointResponse.java @@ -0,0 +1,53 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class CheckpointResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the checkpoint ID") + private String id; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the checkpoint creation time") + private Date created; + + @SerializedName(ApiConstants.IS_ACTIVE) + @Param(description = "whether this is the active checkpoint") + private Boolean isActive; + + public void setId(String id) { + this.id = id; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 5d085d1bee05..f8e1af8d5b76 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -63,6 +63,10 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "The OS category name of the host") private String osCategoryName; + @SerializedName(ApiConstants.GUEST_OS_RULE) + @Param(description = "the guest OS rule") + private String guestOsRule; + @SerializedName(ApiConstants.IP_ADDRESS) @Param(description = "The IP address of the host") private String ipAddress; @@ -999,4 +1003,12 @@ public void setExtensionName(String extensionName) { public String getExtensionName() { return extensionName; } + + public String getGuestOsRule() { + return guestOsRule; + } + + public void setGuestOsRule(String guestOsRule) { + this.guestOsRule = guestOsRule; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java new file mode 100644 index 000000000000..15576e8f1012 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ImageTransferResponse.java @@ -0,0 +1,104 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.api.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.ImageTransfer; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = ImageTransfer.class) +public class ImageTransferResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the image transfer") + private String id; + + @SerializedName("backupid") + @Param(description = "the backup ID") + private String backupId; + + @SerializedName("vmid") + @Param(description = "the VM ID") + private String vmId; + + @SerializedName(ApiConstants.VOLUME_ID) + @Param(description = "the disk/volume ID") + private String diskId; + + @SerializedName("devicename") + @Param(description = "the device name (vda, vdb, etc)") + private String deviceName; + + @SerializedName("transferurl") + @Param(description = "the transfer URL") + private String transferUrl; + + @SerializedName("phase") + @Param(description = "the transfer phase") + private String phase; + + @SerializedName("direction") + @Param(description = "the image transfer direction: upload / download") + private String direction; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the date created") + private Date created; + + public void setId(String id) { + this.id = id; + } + + public void setBackupId(String backupId) { + this.backupId = backupId; + } + + public void setVmId(String vmId) { + this.vmId = vmId; + } + + public void setDiskId(String diskId) { + this.diskId = diskId; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + + public void setPhase(String phase) { + this.phase = phase; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java index eba00da25cfe..8734e65dfa58 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicSecondaryIpResponse.java @@ -53,6 +53,10 @@ public class NicSecondaryIpResponse extends BaseResponse { @Param(description = "The ID of the Instance") private String vmId; + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the secondary IP address") + private String description; + @Override public String getObjectId() { return this.getId(); @@ -98,6 +102,14 @@ public void setId(String id) { this.id = id; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public List getSecondaryIpsList() { return secondaryIpsList; } diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java index 951af9180e7f..2d68f18b953f 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/Backup.java +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -30,8 +30,16 @@ public interface Backup extends ControlledEntity, InternalIdentity, Identity { + String getFromCheckpointId(); + + String getToCheckpointId(); + + Long getCheckpointCreateTime(); + + Long getHostId(); + enum Status { - Allocated, Queued, BackingUp, BackedUp, Error, Failed, Restoring, Removed, Expunged + Allocated, Queued, BackingUp, ReadyForImageTransfer, FinalizingImageTransfer, BackedUp, Error, Failed, Restoring, Removed, Expunged } class Metric { diff --git a/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java new file mode 100644 index 000000000000..e1153be3ae02 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/ImageTransfer.java @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.backup; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface ImageTransfer extends ControlledEntity, InternalIdentity { + long getDataCenterId(); + + public enum Direction { + upload, download + } + + public enum Format { + raw, + cow + } + + public enum Backend { + nbd, + file + } + + public enum Phase { + initializing, transferring, finished, failed + } + + String getUuid(); + + Long getBackupId(); + + long getVolumeId(); + + long getHostId(); + + String getTransferUrl(); + + Phase getPhase(); + + Direction getDirection(); + + Backend getBackend(); + + String getSignedTicketId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java b/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java new file mode 100644 index 000000000000..e1affb311ec6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/KVMBackupExportService.java @@ -0,0 +1,106 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import java.util.List; + +import org.apache.cloudstack.api.command.admin.backup.CreateImageTransferCmd; +import org.apache.cloudstack.api.command.admin.backup.DeleteVmCheckpointCmd; +import org.apache.cloudstack.api.command.admin.backup.FinalizeBackupCmd; +import org.apache.cloudstack.api.command.admin.backup.FinalizeImageTransferCmd; +import org.apache.cloudstack.api.command.admin.backup.ListImageTransfersCmd; +import org.apache.cloudstack.api.command.admin.backup.ListVmCheckpointsCmd; +import org.apache.cloudstack.api.command.admin.backup.StartBackupCmd; +import org.apache.cloudstack.api.response.CheckpointResponse; +import org.apache.cloudstack.api.response.ImageTransferResponse; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.utils.component.PluggableService; + +/** + * Service for Creating Backups and ImageTransfer sessions which will be consumed by an external orchestrator. + */ +public interface KVMBackupExportService extends Configurable, PluggableService { + + ConfigKey ImageTransferIdleTimeoutSeconds = new ConfigKey<>("Advanced", Integer.class, + "image.transfer.idle.timeout.seconds", + "600", + "Seconds since last completed HTTP request to an image transfer before the image server unregisters it (idle timeout).", + true, ConfigKey.Scope.Zone); + + ConfigKey ExposeKVMBackupExportServiceApis = new ConfigKey<>("Advanced", Boolean.class, + "expose.kvm.backup.export.service.apis", + "false", + "Enable to expose APIs for testing the KVM Backup Export Service.", + false, ConfigKey.Scope.Global); + /** + * Creates a backup session for a VM + */ + Backup createBackup(StartBackupCmd cmd); + + /** + * Start a backup session for a VM + * Creates a new checkpoint and starts NBD server for pull-mode backup + */ + Backup startBackup(StartBackupCmd cmd); + + /** + * Finalize a backup session + * Stops NBD server, updates checkpoint tracking, deletes old checkpoints + */ + Backup finalizeBackup(FinalizeBackupCmd cmd); + + /** + * Create an image transfer object for a disk + * Registers NBD endpoint with ImageIO (stubbed for POC) + */ + ImageTransferResponse createImageTransfer(CreateImageTransferCmd cmd); + + ImageTransfer createImageTransfer(long volumeId, Long backupId, ImageTransfer.Direction direction, ImageTransfer.Format format); + + boolean cancelImageTransfer(long imageTransferId); + + /** + * Finalize an image transfer + * Marks transfer as complete (NBD is closed globally in finalize backup) + */ + boolean finalizeImageTransfer(FinalizeImageTransferCmd cmd); + + boolean finalizeImageTransfer(long imageTransferId); + + /** + * List image transfers for a backup + */ + List listImageTransfers(ListImageTransfersCmd cmd); + + /** + * List checkpoints for a VM + */ + List listVmCheckpoints(ListVmCheckpointsCmd cmd); + + /** + * Delete a VM checkpoint (no-op for normal flow, kept for API parity) + */ + boolean deleteVmCheckpoint(DeleteVmCheckpointCmd cmd); + + /** + * List Compatible Data Center Ids for the service + */ + List listCompatibleDataCenterIds(); +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java b/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java index 21184de27a26..3e349cf0bd39 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java +++ b/api/src/main/java/org/apache/cloudstack/storage/sharedfs/SharedFSService.java @@ -23,6 +23,10 @@ import org.apache.cloudstack.api.command.user.storage.sharedfs.ChangeSharedFSServiceOfferingCmd; import org.apache.cloudstack.api.command.user.storage.sharedfs.CreateSharedFSCmd; import org.apache.cloudstack.api.command.user.storage.sharedfs.DestroySharedFSCmd; +import org.apache.cloudstack.api.command.user.storage.sharedfs.ListSharedFSCmd; +import org.apache.cloudstack.api.command.user.storage.sharedfs.UpdateSharedFSCmd; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.SharedFSResponse; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ManagementServerException; @@ -31,11 +35,6 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.VirtualMachineMigrationException; -import org.apache.cloudstack.api.command.user.storage.sharedfs.ListSharedFSCmd; -import org.apache.cloudstack.api.command.user.storage.sharedfs.UpdateSharedFSCmd; -import org.apache.cloudstack.api.response.SharedFSResponse; -import org.apache.cloudstack.api.response.ListResponse; - public interface SharedFSService { List getSharedFSProviders(); @@ -69,4 +68,10 @@ public interface SharedFSService { SharedFS recoverSharedFS(Long sharedFSId); void deleteSharedFS(Long sharedFSId); + + SharedFS getSharedFSByUuid(String uuid); + + SharedFS getSharedFSForVmId(long vmId); + + SharedFS updateSharedFSPostRestore(long sharedFsId, long volumeId); } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java new file mode 100644 index 000000000000..27bc4614e1b5 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/AssignVMCmdTest.java @@ -0,0 +1,55 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.vm; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +public class AssignVMCmdTest { + + @Test + public void test_setSkipNetwork_default() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + Object value = ReflectionTestUtils.getField(assignVMCmd, "skipNetwork"); + Assert.assertTrue(value instanceof Boolean); + Assert.assertFalse((Boolean) value); + } + + @Test + public void test_setSkipNetwork_set() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + assignVMCmd.setSkipNetwork(true); + Object value = ReflectionTestUtils.getField(assignVMCmd, "skipNetwork"); + Assert.assertTrue(value instanceof Boolean); + Assert.assertTrue((Boolean) value); + } + + @Test + public void test_isSkipNetwork_default() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + Assert.assertFalse(assignVMCmd.isSkipNetwork()); + } + + @Test + public void test_isSkipNetwork_set() { + AssignVMCmd assignVMCmd = new AssignVMCmd(); + ReflectionTestUtils.setField(assignVMCmd, "skipNetwork", true); + Assert.assertTrue(assignVMCmd.isSkipNetwork()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java new file mode 100644 index 000000000000..d82dc9f766f9 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdminTest.java @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.vm; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class DeployVMCmdByAdminTest { + + @InjectMocks + private DeployVMCmdByAdmin cmd; + + @Test + public void testIsBlankInstance_default() { + assertFalse(cmd.isBlankInstance()); + } + + @Test + public void testIsBlankInstance_true() { + ReflectionTestUtils.setField(cmd, "blankInstance", true); + assertTrue(cmd.isBlankInstance()); + } + + @Test + public void testIsBlankInstance_false() { + ReflectionTestUtils.setField(cmd, "blankInstance", false); + assertFalse(cmd.isBlankInstance()); + } + + @Test + public void testSetBlankInstance_default() { + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNull(obj); + } + + @Test + public void testSetBlankInstance_true() { + cmd.setBlankInstance(true); + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNotNull(obj); + assertTrue((boolean)obj); + } + + @Test + public void testSetBlankInstance_false() { + cmd.setBlankInstance(false); + Object obj = ReflectionTestUtils.getField(cmd, "blankInstance"); + assertNotNull(obj); + assertFalse((boolean)obj); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java b/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java index 9ea3a6446e8e..71c6d54c7a8c 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/test/AddIpToVmNicTest.java @@ -59,7 +59,7 @@ public void testCreateSuccess() throws ResourceAllocationException, ResourceUnav NicSecondaryIp secIp = Mockito.mock(NicSecondaryIp.class); Mockito.when( - networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any())) + networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any(), ArgumentMatchers.anyString())) .thenReturn(secIp); ipTonicCmd._networkService = networkService; @@ -79,7 +79,7 @@ public void testCreateFailure() throws ResourceAllocationException, ResourceUnav AddIpToVmNicCmd ipTonicCmd = Mockito.mock(AddIpToVmNicCmd.class); Mockito.when( - networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any())) + networkService.allocateSecondaryGuestIP(ArgumentMatchers.anyLong(), ArgumentMatchers.any(), ArgumentMatchers.anyString())) .thenReturn(null); ipTonicCmd._networkService = networkService; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java index f7e3e38d9c3f..09d396e40237 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmdTest.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.vm; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -41,6 +42,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.NetworkService; import com.cloud.utils.db.EntityManager; import com.cloud.vm.VmDetailConstants; @@ -480,4 +482,146 @@ public void testGetDataDiskTemplateToDiskOfferingMapInvalidTemplateId() { }); assertTrue(thrownException.getMessage().contains("Unable to translate and find entity with datadisktemplateid")); } + + @Test + public void testSetServiceOfferingId() { + cmd.setServiceOfferingId(101L); + assertEquals(Long.valueOf(101L), cmd.getServiceOfferingId()); + } + + @Test + public void testSetTemplateId() { + cmd.setTemplateId(102L); + assertEquals(Long.valueOf(102L), cmd.getTemplateId()); + } + + @Test + public void testSetVolumeId() { + cmd.setVolumeId(103L); + assertEquals(Long.valueOf(103L), cmd.getVolumeId()); + } + + @Test + public void testSetSnapshotId() { + cmd.setSnapshotId(104L); + assertEquals(Long.valueOf(104L), cmd.getSnapshotId()); + } + + @Test + public void testSetZoneId() { + cmd.setZoneId(105L); + assertEquals(Long.valueOf(105L), cmd.getZoneId()); + } + + @Test + public void testSetName() { + cmd.setName("vm-name"); + assertEquals("vm-name", cmd.getName()); + } + + @Test + public void testSetDisplayName() { + cmd.setDisplayName("vm-display-name"); + assertEquals("vm-display-name", cmd.getDisplayName()); + } + + @Test + public void testSetAccountName() { + cmd.setAccountName("account-name"); + assertEquals("account-name", cmd.getAccountName()); + } + + @Test + public void testSetDomainId() { + cmd.setDomainId(106L); + assertEquals(Long.valueOf(106L), cmd.getDomainId()); + } + + @Test + public void testSetNetworkIds() { + List networkIds = Arrays.asList(11L, 12L); + cmd.setNetworkIds(networkIds); + assertEquals(networkIds, cmd.getNetworkIds()); + } + + @Test + public void testSetBootType() { + cmd.setBootType("UEFI"); + assertEquals(BootType.UEFI, cmd.getBootType()); + } + + @Test + public void testSetBootMode() { + cmd.setBootType("UEFI"); + cmd.setBootMode("SECURE"); + assertEquals(BootMode.SECURE, cmd.getBootMode()); + } + + @Test + public void testSetHypervisor() { + cmd.setHypervisor("KVM"); + assertEquals(HypervisorType.KVM, cmd.getHypervisor()); + } + + @Test + public void testSetUserData() { + cmd.setUserData("dXNlci1kYXRh"); + assertEquals("dXNlci1kYXRh", cmd.getUserData()); + } + + @Test + public void testSetKeyboard() { + cmd.setKeyboard("us"); + assertEquals("us", cmd.getKeyboard()); + } + + @Test + public void testSetProjectId() { + cmd.setProjectId(107L); + assertEquals(Long.valueOf(107L), ReflectionTestUtils.getField(cmd, "projectId")); + } + + @Test + public void testSetDisplayVm() { + cmd.setDisplayVm(Boolean.FALSE); + assertEquals(Boolean.FALSE, cmd.isDisplayVm()); + } + + @Test + public void testSetUserDataId() { + cmd.setUserDataId(108L); + assertEquals(Long.valueOf(108L), cmd.getUserdataId()); + } + + @Test + public void testSetAffinityGroupIds() { + List affinityGroupIds = Arrays.asList(21L, 22L); + cmd.setAffinityGroupIds(affinityGroupIds); + assertEquals(affinityGroupIds, cmd.getAffinityGroupIdList()); + } + + @Test + public void testSetDetails() { + Map details = new HashMap<>(); + details.put("key", "value"); + cmd.setDetails(details); + assertEquals(details, ReflectionTestUtils.getField(cmd, "details")); + } + + @Test + public void testSetExtraConfig() { + cmd.setExtraConfig("cpu-mode=host-passthrough"); + assertEquals("cpu-mode=host-passthrough", cmd.getExtraConfig()); + } + + @Test + public void testSetDynamicScalingEnabled() { + cmd.setDynamicScalingEnabled(Boolean.FALSE); + assertFalse(cmd.isDynamicScalingEnabled()); + } + + @Test + public void testIsBlankInstance() { + assertFalse(cmd.isBlankInstance()); + } } diff --git a/client/pom.xml b/client/pom.xml index ba16cc5d34a2..7d37225dbbb3 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -612,6 +612,11 @@ cloud-plugin-backup-nas ${project.version} + + org.apache.cloudstack + cloud-plugin-integrations-veeam-control-service + ${project.version} + org.apache.cloudstack cloud-plugin-integrations-kubernetes-service diff --git a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java index 5ac4e9ae445e..7196247ffc23 100644 --- a/core/src/main/java/com/cloud/agent/api/MigrateCommand.java +++ b/core/src/main/java/com/cloud/agent/api/MigrateCommand.java @@ -26,6 +26,7 @@ import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.storage.Storage; public class MigrateCommand extends Command { private String vmName; @@ -42,6 +43,7 @@ public class MigrateCommand extends Command { private Map dpdkInterfaceMapping = new HashMap<>(); private int newVmCpuShares; + private boolean clvmCrossPoolMigration; Map vlanToPersistenceMap = new HashMap<>(); @@ -149,6 +151,14 @@ public void setNewVmCpuShares(int newVmCpuShares) { this.newVmCpuShares = newVmCpuShares; } + public boolean isClvmCrossPoolMigration() { + return clvmCrossPoolMigration; + } + + public void setClvmCrossPoolMigration(boolean clvmCrossPoolMigration) { + this.clvmCrossPoolMigration = clvmCrossPoolMigration; + } + public static class MigrateDiskInfo { public enum DiskType { FILE, BLOCK; @@ -184,6 +194,8 @@ public String toString() { private final String sourceText; private final String backingStoreText; private boolean isSourceDiskOnStorageFileSystem; + private Storage.StoragePoolType sourcePoolType; + private Storage.StoragePoolType destPoolType; public MigrateDiskInfo(final String serialNumber, final DiskType diskType, final DriverType driverType, final Source source, final String sourceText) { this.serialNumber = serialNumber; @@ -232,6 +244,22 @@ public boolean isSourceDiskOnStorageFileSystem() { public void setSourceDiskOnStorageFileSystem(boolean isDiskOnFileSystemStorage) { this.isSourceDiskOnStorageFileSystem = isDiskOnFileSystemStorage; } + + public Storage.StoragePoolType getSourcePoolType() { + return sourcePoolType; + } + + public void setSourcePoolType(Storage.StoragePoolType sourcePoolType) { + this.sourcePoolType = sourcePoolType; + } + + public Storage.StoragePoolType getDestPoolType() { + return destPoolType; + } + + public void setDestPoolType(Storage.StoragePoolType destPoolType) { + this.destPoolType = destPoolType; + } } @Override diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java new file mode 100644 index 000000000000..24fdf8402029 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationAnswer.java @@ -0,0 +1,42 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api; + +/** + * Answer for PostMigrationCommand. + * Indicates success or failure of post-migration operations on the destination host. + */ +public class PostMigrationAnswer extends Answer { + + protected PostMigrationAnswer() { + } + + public PostMigrationAnswer(PostMigrationCommand cmd, String detail) { + super(cmd, false, detail); + } + + public PostMigrationAnswer(PostMigrationCommand cmd, Exception ex) { + super(cmd, ex); + } + + public PostMigrationAnswer(PostMigrationCommand cmd) { + super(cmd, true, null); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java new file mode 100644 index 000000000000..938000c3593c --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PostMigrationCommand.java @@ -0,0 +1,59 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PostMigrationCommand is sent to the destination host after a successful VM migration. + * It performs post-migration tasks such as: + * - Claiming exclusive locks on CLVM volumes (converting from shared to exclusive mode) + * - Other post-migration cleanup operations + */ +public class PostMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PostMigrationCommand() { + } + + public PostMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java new file mode 100644 index 000000000000..3ff30391eaef --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PreMigrationCommand.java @@ -0,0 +1,61 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.api; + +import com.cloud.agent.api.to.VirtualMachineTO; + +/** + * PreMigrationCommand is sent to the source host before VM migration starts. + * It performs pre-migration tasks such as: + * - Converting CLVM volume exclusive locks to shared mode so destination host can access them + * - Other pre-migration preparation operations on the source host + * + * This command runs on the SOURCE host before PrepareForMigrationCommand runs on the DESTINATION host. + */ +public class PreMigrationCommand extends Command { + private VirtualMachineTO vm; + private String vmName; + + protected PreMigrationCommand() { + } + + public PreMigrationCommand(VirtualMachineTO vm, String vmName) { + this.vm = vm; + this.vmName = vmName; + } + + public VirtualMachineTO getVirtualMachine() { + return vm; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return true; + } + + @Override + public boolean isBypassHostMaintenance() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java new file mode 100644 index 000000000000..34cf6d4ca34c --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferAnswer.java @@ -0,0 +1,56 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class CreateImageTransferAnswer extends Answer { + private String imageTransferId; + private String transferUrl; + + public CreateImageTransferAnswer() { + } + + public CreateImageTransferAnswer(CreateImageTransferCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public CreateImageTransferAnswer(CreateImageTransferCommand cmd, boolean success, String details, + String imageTransferId, String transferUrl) { + super(cmd, success, details); + this.imageTransferId = imageTransferId; + this.transferUrl = transferUrl; + } + + public String getImageTransferId() { + return imageTransferId; + } + + public void setImageTransferId(String imageTransferId) { + this.imageTransferId = imageTransferId; + } + + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java new file mode 100644 index 000000000000..95b56c9a9c38 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java @@ -0,0 +1,94 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class CreateImageTransferCommand extends Command { + private String transferId; + private String exportName; + private String socket; + private String direction; + private String checkpointId; + private String file; + private ImageTransfer.Backend backend; + private int idleTimeoutSeconds; + + public CreateImageTransferCommand() { + } + + private CreateImageTransferCommand(String transferId, String direction, String socket, int idleTimeoutSeconds) { + this.transferId = transferId; + this.direction = direction; + this.socket = socket; + this.idleTimeoutSeconds = idleTimeoutSeconds; + } + + public CreateImageTransferCommand(String transferId, String direction, String exportName, String socket, String checkpointId, int idleTimeoutSeconds) { + this(transferId, direction, socket, idleTimeoutSeconds); + this.backend = ImageTransfer.Backend.nbd; + this.exportName = exportName; + this.checkpointId = checkpointId; + } + + public CreateImageTransferCommand(String transferId, String direction, String socket, String file, int idleTimeoutSeconds) { + this(transferId, direction, socket, idleTimeoutSeconds); + if (direction == ImageTransfer.Direction.download.toString()) { + throw new IllegalArgumentException("File backend is only supported for upload"); + } + this.backend = ImageTransfer.Backend.file; + this.file = file; + } + + public String getExportName() { + return exportName; + } + + public String getSocket() { + return socket; + } + + public String getFile() { + return file; + } + + public ImageTransfer.Backend getBackend() { + return backend; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getDirection() { + return direction; + } + + public String getCheckpointId() { + return checkpointId; + } + + public int getIdleTimeoutSeconds() { + return idleTimeoutSeconds; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java b/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java new file mode 100644 index 000000000000..81cf6c1abfcc --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/DeleteVmCheckpointCommand.java @@ -0,0 +1,60 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import java.util.Map; + +import com.cloud.agent.api.Command; + +public class DeleteVmCheckpointCommand extends Command { + private String vmName; + private String checkpointId; + private Map diskPathUuidMap; + private boolean stoppedVM; + + public DeleteVmCheckpointCommand() { + } + + public DeleteVmCheckpointCommand(String vmName, String checkpointId, Map diskPathUuidMap, boolean stoppedVM) { + this.vmName = vmName; + this.checkpointId = checkpointId; + this.diskPathUuidMap = diskPathUuidMap; + this.stoppedVM = stoppedVM; + } + + public String getVmName() { + return vmName; + } + + public String getCheckpointId() { + return checkpointId; + } + + public Map getDiskPathUuidMap() { + return diskPathUuidMap; + } + + public boolean isStoppedVM() { + return stoppedVM; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java new file mode 100644 index 000000000000..84d9b1ff8186 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/FinalizeImageTransferCommand.java @@ -0,0 +1,40 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class FinalizeImageTransferCommand extends Command { + private String transferId; + + public FinalizeImageTransferCommand() { + } + + public FinalizeImageTransferCommand(String transferId) { + this.transferId = transferId; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java new file mode 100644 index 000000000000..d7cbf097df90 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupAnswer.java @@ -0,0 +1,44 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StartBackupAnswer extends Answer { + private Long checkpointCreateTime; + + public StartBackupAnswer() { + } + + public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public StartBackupAnswer(StartBackupCommand cmd, boolean success, String details, Long checkpointCreateTime) { + super(cmd, success, details); + this.checkpointCreateTime = checkpointCreateTime; + } + + public Long getCheckpointCreateTime() { + return checkpointCreateTime; + } + + public void setCheckpointCreateTime(Long checkpointCreateTime) { + this.checkpointCreateTime = checkpointCreateTime; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java new file mode 100644 index 000000000000..6f1ed6834500 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartBackupCommand.java @@ -0,0 +1,91 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import java.util.Map; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; + +public class StartBackupCommand extends Command { + private String vmName; + private String toCheckpointId; + private String fromCheckpointId; + private Long fromCheckpointCreateTime; + private String socket; + private Map diskPathUuidMap; + private boolean stoppedVM; + @LogLevel(LogLevel.Log4jLevel.Off) + private Map diskPathPassphraseMap; + + public StartBackupCommand() { + } + + public StartBackupCommand(String vmName, String toCheckpointId, String fromCheckpointId, Long fromCheckpointCreateTime, + String socket, Map diskPathUuidMap, Map passphrases, boolean stoppedVM) { + this.vmName = vmName; + this.toCheckpointId = toCheckpointId; + this.fromCheckpointId = fromCheckpointId; + this.fromCheckpointCreateTime = fromCheckpointCreateTime; + this.socket = socket; + this.diskPathUuidMap = diskPathUuidMap; + this.diskPathPassphraseMap = passphrases; + this.stoppedVM = stoppedVM; + } + + public String getVmName() { + return vmName; + } + + public String getToCheckpointId() { + return toCheckpointId; + } + + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public Long getFromCheckpointCreateTime() { + return fromCheckpointCreateTime; + } + + public String getSocket() { + return socket; + } + + public Map getDiskPathUuidMap() { + return diskPathUuidMap; + } + + public boolean isIncremental() { + return fromCheckpointId != null && !fromCheckpointId.isEmpty(); + } + + public boolean isStoppedVM() { + return stoppedVM; + } + + public Map getDiskPathPassphraseMap() { + return diskPathPassphraseMap; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java new file mode 100644 index 000000000000..d8c78d3c8807 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerAnswer.java @@ -0,0 +1,56 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StartNBDServerAnswer extends Answer { + private String imageTransferId; + private String transferUrl; + + public StartNBDServerAnswer() { + } + + public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details) { + super(cmd, success, details); + } + + public StartNBDServerAnswer(StartNBDServerCommand cmd, boolean success, String details, + String imageTransferId, String transferUrl) { + super(cmd, success, details); + this.imageTransferId = imageTransferId; + this.transferUrl = transferUrl; + } + + public String getImageTransferId() { + return imageTransferId; + } + + public void setImageTransferId(String imageTransferId) { + this.imageTransferId = imageTransferId; + } + + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java new file mode 100644 index 000000000000..07e50f11fd37 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StartNBDServerCommand.java @@ -0,0 +1,78 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; + +public class StartNBDServerCommand extends Command { + private String transferId; + private String exportName; + private String volumePath; + private String socket; + private String direction; + private String fromCheckpointId; + @LogLevel(LogLevel.Log4jLevel.Off) + private byte[] passphrase; + + public StartNBDServerCommand() { + } + + protected StartNBDServerCommand(String transferId, String exportName, String volumePath, String socket, String direction, String fromCheckpointId, byte[] passphrase) { + this.transferId = transferId; + this.socket = socket; + this.exportName = exportName; + this.volumePath = volumePath; + this.direction = direction; + this.fromCheckpointId = fromCheckpointId; + this.passphrase = passphrase; + } + + public String getExportName() { + return exportName; + } + + public String getSocket() { + return socket; + } + + public String getTransferId() { + return transferId; + } + + @Override + public boolean executeInSequence() { + return true; + } + + public String getVolumePath() { + return volumePath; + } + + public String getDirection() { + return direction; + } + + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public byte[] getPassphrase() { + return passphrase; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java b/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java new file mode 100644 index 000000000000..ce977f31e005 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopBackupAnswer.java @@ -0,0 +1,30 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Answer; + +public class StopBackupAnswer extends Answer { + + public StopBackupAnswer() { + } + + public StopBackupAnswer(StopBackupCommand cmd, boolean success, String details) { + super(cmd, success, details); + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java new file mode 100644 index 000000000000..d3055021e9de --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopBackupCommand.java @@ -0,0 +1,52 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class StopBackupCommand extends Command { + private String vmName; + private Long vmId; + private Long backupId; + + public StopBackupCommand() { + } + + public StopBackupCommand(String vmName, Long vmId, Long backupId) { + this.vmName = vmName; + this.vmId = vmId; + this.backupId = backupId; + } + + public String getVmName() { + return vmName; + } + + public Long getVmId() { + return vmId; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java new file mode 100644 index 000000000000..d75168a22eb2 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/backup/StopNBDServerCommand.java @@ -0,0 +1,46 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import com.cloud.agent.api.Command; + +public class StopNBDServerCommand extends Command { + private String transferId; + private String direction; + + public StopNBDServerCommand() { + } + + public StopNBDServerCommand(String transferId, String direction) { + this.transferId = transferId; + this.direction = direction; + } + + public String getTransferId() { + return transferId; + } + + public String getDirection() { + return direction; + } + + @Override + public boolean executeInSequence() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java new file mode 100644 index 000000000000..f3c43c400b2b --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferAnswer.java @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.storage.clvm.command; + +import com.cloud.agent.api.Answer; + +/** + * Answer for ClvmLockTransferCommand, containing lock state information. + * This answer includes the current lock holder information when querying lock state. + */ +public class ClvmLockTransferAnswer extends Answer { + + private String currentLockHostname; + private boolean isActive; + private boolean isOpen; + private String lvAttributes; + + public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + + public ClvmLockTransferAnswer(ClvmLockTransferCommand cmd, boolean result, String details, + String currentLockHostname, boolean isActive, boolean isOpen, + String lvAttributes) { + super(cmd, result, details); + this.currentLockHostname = currentLockHostname; + this.isActive = isActive; + this.isOpen = isOpen; + this.lvAttributes = lvAttributes; + } + + /** + * Get the hostname from lv_host. Retained for diagnostics only — + * do NOT use this to determine lock holder identity. + */ + public String getCurrentLockHostname() { + return currentLockHostname; + } + + public void setCurrentLockHostname(String currentLockHostname) { + this.currentLockHostname = currentLockHostname; + } + + /** + * Whether the LV is locally active on the queried host (lv_attr[4]=='a'). + * This is the authoritative signal for lock holder discovery via fan-out. + */ + public boolean isActive() { + return isActive; + } + + public void setActive(boolean active) { + isActive = active; + } + + /** + * Whether a process has the device file open on the queried host (lv_attr[5]=='o'). + * true means a VM is actively doing I/O on this host right now — do NOT deactivate. + */ + public boolean isOpen() { + return isOpen; + } + + public void setOpen(boolean open) { + isOpen = open; + } + + public String getLvAttributes() { + return lvAttributes; + } + + public void setLvAttributes(String lvAttributes) { + this.lvAttributes = lvAttributes; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java new file mode 100644 index 000000000000..218fb798869d --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/storage/clvm/command/ClvmLockTransferCommand.java @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.storage.clvm.command; + +import com.cloud.agent.api.Command; + +/** + * Command to transfer CLVM (Clustered LVM) exclusive lock between hosts. + * This enables lightweight volume migration for CLVM storage pools where volumes + * reside in the same Volume Group (VG) but need to be accessed from different hosts. + * + *

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

This is significantly faster (10-100x) than traditional migration and uses no network bandwidth. + */ +public class ClvmLockTransferCommand extends Command { + + /** + * Operation to perform on the CLVM volume. + * Maps to lvchange flags for LVM operations. + */ + public enum Operation { + /** Deactivate the volume on this host (-an) */ + DEACTIVATE("-an", "deactivate"), + + /** Activate the volume exclusively on this host (-aey) */ + ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"), + + /** Activate the volume in shared mode on this host (-asy) */ + ACTIVATE_SHARED("-asy", "activate in shared mode"), + + /** Query the current lock state (lvs -o lv_attr,lv_host) */ + QUERY_LOCK_STATE("query", "query lock state"); + + private final String lvchangeFlag; + private final String description; + + Operation(String lvchangeFlag, String description) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + } + + private String lvPath; + private Operation operation; + private String volumeUuid; + + public ClvmLockTransferCommand() { + // For serialization + } + + public ClvmLockTransferCommand(Operation operation, String lvPath, String volumeUuid) { + this.operation = operation; + this.lvPath = lvPath; + this.volumeUuid = volumeUuid; + setWait(65); + } + + public String getLvPath() { + return lvPath; + } + + public Operation getOperation() { + return operation; + } + + public String getVolumeUuid() { + return volumeUuid; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/debian/control b/debian/control index 2b8ce929c639..cdf663ef8906 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,7 @@ Description: CloudStack server library Package: cloudstack-agent Architecture: all -Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat +Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat, python3-libnbd, socat Recommends: init-system-helpers Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts Description: CloudStack agent diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index e871bd8672ff..5eca02f33d51 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -59,6 +59,8 @@ */ public interface VirtualMachineManager extends Manager { + String KVM_BLANK_VM_TEMPLATE_NAME = "kvm-blank-vm-template"; + ConfigKey ExecuteInSequence = new ConfigKey<>("Advanced", Boolean.class, "execute.in.sequence.hypervisor.commands", "false", "If set to true, start, stop, reboot, copy and migrate commands will be serialized on the agent side. If set to false the commands are executed in parallel. Default value is false.", false); @@ -312,4 +314,8 @@ void checkDeploymentPlan(VirtualMachine virtualMachine, VirtualMachineTemplate t ServiceOffering serviceOffering, Account systemAccount, DeploymentPlan plan) throws InsufficientServerCapacityException; + boolean isBlankInstanceDefaultTemplate(VirtualMachineTemplate template); + + boolean isBlankInstance(VirtualMachineTemplate template); + } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java index 8b0171870765..4937edd33d1f 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeInfo.java @@ -103,4 +103,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume { List getCheckpointPaths(); Set getCheckpointImageStoreUrls(); + + /** + * Gets the destination host ID hint for CLVM volume creation. + * This is used to route volume creation commands to the specific host where the VM will be deployed. + * Only applicable for CLVM storage pools to avoid shared mode activation. + * + * @return The host ID where the volume should be created, or null if not set + */ + Long getDestinationHostId(); + + /** + * Sets the destination host ID hint for CLVM volume creation. + * This should be set before volume creation when the destination host is known. + * + * @param hostId The host ID where the volume should be created + */ + void setDestinationHostId(Long hostId); } diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java index 682473ec94fc..a7d82d0b9628 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/VolumeService.java @@ -30,6 +30,7 @@ import com.cloud.host.Host; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.offering.DiskOffering; +import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -123,4 +124,71 @@ boolean copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(ObjectInD void checkAndRepairVolumeBasedOnConfig(DataObject dataObject, Host host); void validateChangeDiskOfferingEncryptionType(long existingDiskOfferingId, long newDiskOfferingId); + + /** + * Transfers exclusive lock for a volume on cluster-based storage (e.g., CLVM/CLVM_NG) from one host to another. + * This is used for storage that requires host-level lock management for volumes on shared storage pools. + * For non-CLVM pool types, this method returns false without taking action. + * + * @param volume The volume to transfer lock for + * @param sourceHostId Host currently holding the exclusive lock + * @param destHostId Host to receive the exclusive lock + * @return true if lock transfer succeeded or was not needed, false if it failed + */ + boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId); + + /** + * Finds which host currently has the exclusive lock on a CLVM volume. + * Checks in order: explicit lock tracking, attached VM's host, or first available cluster host. + * + * @param volume The CLVM volume + * @return Host ID that has the exclusive lock, or null if cannot be determined + */ + Long findVolumeLockHost(VolumeInfo volume); + + /** + * Performs lightweight CLVM lock migration for a volume to a target host. + * This transfers the LVM exclusive lock without copying data (CLVM volumes are on shared cluster storage). + * If the volume already has the lock on the destination host, no action is taken. + * + * @param volume The volume to migrate lock for + * @param destHostId Destination host ID + * @return Updated VolumeInfo after lock migration + */ + VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId); + + /** + * Checks if both storage pools are CLVM type (CLVM or CLVM_NG). + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @return true if both pools are CLVM type (CLVM or CLVM_NG) + */ + boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType); + + /** + * Determines if CLVM lock transfer is required when a volume is already on the correct storage pool. + * + * @param volumeToAttach The volume being attached + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM's existing volume + * @param volumePoolId Storage pool ID for the volume + * @param vmPoolId Storage pool ID for the VM's existing volume + * @param vmHostId VM's current host ID (or last host ID if stopped) + * @return true if CLVM lock transfer is needed + */ + boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId); + + /** + * Determines if lightweight CLVM migration is needed instead of full data copy. + * + * @param volumePoolType Storage pool type for the volume + * @param vmPoolType Storage pool type for the VM + * @param volumePoolPath Storage pool path for the volume + * @param vmPoolPath Storage pool path for the VM + * @return true if lightweight migration should be used + */ + boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath); } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index ead990b42b86..c27240823b16 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -50,6 +50,8 @@ import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.hypervisor.KVMGuru; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; @@ -136,6 +138,7 @@ import com.cloud.agent.api.PrepareExternalProvisioningCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; +import com.cloud.agent.api.PreMigrationCommand; import com.cloud.agent.api.RebootAnswer; import com.cloud.agent.api.RebootCommand; import com.cloud.agent.api.RecreateCheckpointsCommand; @@ -267,6 +270,7 @@ import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.storage.snapshot.SnapshotManager; import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; @@ -303,8 +307,8 @@ import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; @@ -361,6 +365,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private VolumeDao _volsDao; @Inject + private VolumeDetailsDao _volsDetailsDao; + @Inject private HighAvailabilityManager _haMgr; @Inject private HostPodDao _podDao; @@ -463,6 +469,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac ExtensionsManager extensionsManager; @Inject ExtensionDetailsDao extensionDetailsDao; + @Inject + ClvmPoolManager clvmPoolManager; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -576,7 +584,13 @@ public void allocate(final String vmInstanceName, final VirtualMachineTemplate t logger.debug("Allocating disks for {}", persistedVm); - allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot); + if (isBlankInstance(template)) { + logger.debug("Template is a dummy template for hypervisor {}, skipping volume allocation", hyperType); + return; + } else { + allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot); + } + // Create new Volume context and inject event resource type, id and details to generate VOLUME.CREATE event for the ROOT disk. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); @@ -3144,6 +3158,9 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy updateOverCommitRatioForVmProfile(profile, dest.getHost().getClusterId()); final VirtualMachineTO to = toVmTO(profile); + + executePreMigrationCommand(vm, to, srcHostId); + final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); setVmNetworkDetails(vm, to); @@ -3275,6 +3292,7 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy logger.warn("Error while checking the vm {} on host {}", vm, dest.getHost(), e); } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -3314,6 +3332,30 @@ protected void migrate(final VMInstanceVO vm, final long srcHostId, final Deploy } } + private void executePostMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long dstHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String dstHostUuid = _hostDao.findById(dstHostId).getUuid(); + try { + logger.info("Executing post-migration tasks for VM {} with CLVM volumes on destination host {}", vm.getInstanceName(), dstHostUuid); + final PostMigrationCommand postMigrationCommand = new PostMigrationCommand(to, vm.getInstanceName()); + final Answer postMigrationAnswer = _agentMgr.send(dstHostId, postMigrationCommand); + + if (postMigrationAnswer == null || !postMigrationAnswer.getResult()) { + final String details = postMigrationAnswer != null ? postMigrationAnswer.getDetails() : "null answer returned"; + logger.warn("Post-migration tasks failed for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, details); + } else { + logger.info("Successfully completed post-migration tasks for VM {} on destination host {}", vm.getInstanceName(), dstHostUuid); + } + } catch (Exception e) { + logger.warn("Exception during post-migration tasks for VM {} on destination host {}: {}. Migration completed but some cleanup may be needed.", + vm.getInstanceName(), dstHostUuid, e.getMessage(), e); + } + updateClvmLockHostForVmVolumes(vm.getId(), dstHostId); + } + /** * Create and set parameters for the {@link MigrateCommand} used in the migration and scaling of VMs. */ @@ -3360,6 +3402,27 @@ private void updateVmPod(VMInstanceVO vm, long dstHostId) { _vmDao.persist(newVm); } + /** + * Updates CLVM_LOCK_HOST_ID for all CLVM volumes attached to a VM after VM migration. + * This ensures that subsequent operations on CLVM volumes are routed to the correct host. + * + * @param vmId The ID of the VM that was migrated + * @param destHostId The destination host ID where the VM now resides + */ + private void updateClvmLockHostForVmVolumes(long vmId, long destHostId) { + List volumes = _volsDao.findByInstance(vmId); + if (CollectionUtils.isEmpty(volumes)) { + return; + } + + for (VolumeVO volume : volumes) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } + } + } + /** * We create the mapping of volumes and storage pool to migrate the VMs according to the information sent by the user. * If the user did not enter a complete mapping, the volumes that were left behind will be auto mapped using {@link #createStoragePoolMappingsForVolumes(VirtualMachineProfile, DataCenterDeployment, Map, List)} @@ -4916,6 +4979,12 @@ private void orchestrateMigrateForScale(final String vmUuid, final long srcHostI volumeMgr.prepareForMigration(profile, dest); final VirtualMachineTO to = toVmTO(profile); + + // Step 1: Send PreMigrationCommand to source host to convert CLVM volumes to shared mode + // This must happen BEFORE PrepareForMigrationCommand on destination to avoid lock conflicts + executePreMigrationCommand(vm, to, srcHostId); + + // Step 2: Send PrepareForMigrationCommand to destination host final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to); ItWorkVO work = new ItWorkVO(UUID.randomUUID().toString(), _nodeId, State.Migrating, vm.getType(), vm.getId()); @@ -5000,6 +5069,7 @@ private void orchestrateMigrateForScale(final String vmUuid, final long srcHostI } migrated = true; + executePostMigrationCommand(vm, to, dstHostId); } finally { if (!migrated) { logger.info("Migration was unsuccessful. Cleaning up: {}", vm); @@ -6435,6 +6505,37 @@ private Pair findClusterAndHostIdForVm(VirtualMachine vm) { return findClusterAndHostIdForVm(vm, false); } + private boolean hasClvmVolumes(long vmId) { + List volumes = _volsDao.findByInstance(vmId); + return volumes.stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(pool -> pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())); + } + + private void executePreMigrationCommand(VMInstanceVO vm, VirtualMachineTO to, long srcHostId) { + if (!(vm.getHypervisorType() == HypervisorType.KVM && hasClvmVolumes(vm.getId()))) { + return; + } + final String vmInstanceName = vm.getInstanceName(); + final String srcHostUuid = _hostDao.findById(srcHostId).getUuid(); + logger.info("Sending PreMigrationCommand to source host {} for VM {} with CLVM volumes", srcHostUuid, vmInstanceName); + final PreMigrationCommand preMigCmd = new PreMigrationCommand(to, vmInstanceName); + Answer preMigAnswer = null; + try { + preMigAnswer = _agentMgr.send(srcHostId, preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + final String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + final String msg = "Failed to prepare source host for migration: " + details; + logger.error("Failed to prepare source host {} for migration of VM {}: {}", srcHostUuid, vmInstanceName, details); + throw new CloudRuntimeException(msg); + } + logger.info("Successfully prepared source host {} for migration of VM {}", srcHostUuid, vmInstanceName); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + logger.error("Failed to send PreMigrationCommand to source host {}: {}", srcHostUuid, e.getMessage(), e); + throw new CloudRuntimeException("Failed to prepare source host for migration: " + e.getMessage(), e); + } + } + @Override public Pair findClusterAndHostIdForVm(long vmId) { VMInstanceVO vm = _vmDao.findById(vmId); @@ -6708,4 +6809,18 @@ public void checkDeploymentPlan(VirtualMachine virtualMachine, VirtualMachineTem vmProfile), DataCenter.class, plan.getDataCenterId(), areAffinityGroupsAssociated(vmProfile)); } } + + @Override + public boolean isBlankInstanceDefaultTemplate(VirtualMachineTemplate template) { + return KVM_BLANK_VM_TEMPLATE_NAME.equals(template.getUniqueName()); + } + + @Override + public boolean isBlankInstance(VirtualMachineTemplate template) { + if (isBlankInstanceDefaultTemplate(template)) { + return true; + } + return Boolean.TRUE.equals( + MapUtils.getBoolean(CallContext.current().getContextParameters(), ApiConstants.BLANK_INSTANCE)); + } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java index 8639f006383f..964265cb873e 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.engine.orchestration; import com.cloud.storage.Snapshot; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.template.VirtualMachineTemplate; import java.net.URL; @@ -292,9 +293,11 @@ public VirtualMachineEntity createVirtualMachineFromScratch(String id, String ow ServiceOfferingVO computeOffering = _serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId()); + VMTemplateVO iso = _templateDao.findByIdIncludingRemoved(Long.valueOf(isoId)); + DiskOfferingInfo rootDiskOfferingInfo = new DiskOfferingInfo(); - if (diskOfferingId == null) { + if (diskOfferingId == null && !_itMgr.isBlankInstance(iso)) { throw new InvalidParameterValueException("Installing from ISO requires a disk offering to be specified for the root disk."); } DiskOfferingVO diskOffering = _diskOfferingDao.findById(diskOfferingId); @@ -345,7 +348,7 @@ public VirtualMachineEntity createVirtualMachineFromScratch(String id, String ow HypervisorType hypervisorType = HypervisorType.valueOf(hypervisor); - _itMgr.allocate(vm.getInstanceName(), _templateDao.findByIdIncludingRemoved(new Long(isoId)), computeOffering, rootDiskOfferingInfo, dataDiskOfferings, dataDiskDeviceIds, + _itMgr.allocate(vm.getInstanceName(), iso, computeOffering, rootDiskOfferingInfo, dataDiskOfferings, dataDiskDeviceIds, networkIpMap, plan, hypervisorType, extraDhcpOptionMap, null, volume, snapshot); return vmEntity; diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index bf3985d3ce77..c4a6a9dd6843 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -38,8 +38,10 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.agent.AgentManager; import com.cloud.deploy.DeploymentClusterPlanner; import com.cloud.exception.ResourceAllocationException; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.resourcelimit.ReservationHelper; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.VMTemplateVO; @@ -275,6 +277,10 @@ public enum UserVmCloneType { ConfigurationDao configurationDao; @Inject VMInstanceDao vmInstanceDao; + @Inject + ClvmPoolManager clvmPoolManager; + @Inject + AgentManager _agentMgr; @Inject protected SnapshotHelper snapshotHelper; @@ -747,6 +753,17 @@ public VolumeInfo createVolume(VolumeInfo volumeInfo, VirtualMachine vm, Virtual logger.debug("Trying to create volume [{}] on storage pool [{}].", volumeToString, poolToString); DataStore store = dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary); + + // For CLVM pools, set the lock host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + if (ClvmPoolManager.isClvmPoolType(pool.getPoolType()) && hostId != null) { + logger.info("CLVM pool detected. Setting lock host {} for volume {} to route creation to correct host", + hostId, volumeInfo.getUuid()); + volumeInfo.setDestinationHostId(hostId); + + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + } + for (int i = 0; i < 2; i++) { // retry one more time in case of template reload is required for Vmware case AsyncCallFuture future = null; @@ -788,6 +805,122 @@ private String getVolumeIdentificationInfos(Volume volume) { return String.format("uuid: %s, name: %s", volume.getUuid(), volume.getName()); } + /** + * Updates the CLVM_LOCK_HOST_ID for a migrated volume if applicable. + * For CLVM volumes that are attached to a VM, this updates the lock host tracking + * to point to the VM's current host after volume migration. + * + * @param migratedVolume The volume that was migrated + * @param destPool The destination storage pool + * @param operationType Description of the operation (e.g., "migrated", "live-migrated") for logging + */ + private void updateClvmLockHostAfterMigration(Volume migratedVolume, StoragePool destPool, String operationType) { + if (migratedVolume == null || destPool == null) { + return; + } + + StoragePoolVO pool = _storagePoolDao.findById(destPool.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return; + } + + if (migratedVolume.getInstanceId() == null) { + return; + } + + VMInstanceVO vm = vmInstanceDao.findById(migratedVolume.getInstanceId()); + if (vm == null || vm.getHostId() == null) { + return; + } + + clvmPoolManager.setClvmLockHostId(migratedVolume.getId(), vm.getHostId()); + logger.debug("Updated CLVM_LOCK_HOST_ID for {} volume {} to host {} where VM {} is running", + operationType, migratedVolume.getUuid(), vm.getHostId(), vm.getInstanceName()); + } + + /** + * Retrieves the CLVM lock host ID from any existing volume of the specified VM. + * This is useful when attaching a new volume to a stopped VM - we want to maintain + * consistency by using the same host that manages the VM's other CLVM volumes. + * + * @param vmId The ID of the VM + * @return The host ID if found, null otherwise + */ + private Long getClvmLockHostFromVmVolumes(Long vmId) { + if (vmId == null) { + return null; + } + + List vmVolumes = _volsDao.findByInstance(vmId); + if (vmVolumes == null || vmVolumes.isEmpty()) { + return null; + } + + for (VolumeVO volume : vmVolumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + if (lockHostId != null) { + logger.debug("Found actual CLVM lock host {} from volume {} of VM {} via LVM query", + lockHostId, volume.getUuid(), vmId); + return lockHostId; + } + } + + return null; + } + + private void transferClvmLocksForVmStart(List volumes, Long destHostId, VMInstanceVO vm) { + if (volumes == null || volumes.isEmpty() || destHostId == null) { + return; + } + + for (VolumeVO volume : volumes) { + if (volume.getPoolId() == null) { + continue; + } + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + continue; + } + + Long currentLockHost = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (currentLockHost == null) { + clvmPoolManager.setClvmLockHostId(volume.getId(), destHostId); + } else if (!currentLockHost.equals(destHostId)) { + logger.info("CLVM volume {} is locked on host {} but VM {} starting on host {}. Transferring lock.", + volume.getUuid(), currentLockHost, vm.getInstanceName(), destHostId); + + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), pool, currentLockHost, destHostId)) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volume.getUuid(), currentLockHost, destHostId)); + } + } + } + } + public String getRandomVolumeName() { return UUID.randomUUID().toString(); } @@ -1206,10 +1339,22 @@ public VolumeInfo createVolumeOnPrimaryStorage(VirtualMachine vm, VolumeInfo vol Long clusterId = storagePool.getClusterId(); logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); Long hostId = vm.getHostId(); - if (hostId == null && storagePool.isLocal()) { - List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); - if (poolHosts.size() > 0) { - hostId = poolHosts.get(0).getHostId(); + if (hostId == null && (storagePool.isLocal() || ClvmPoolManager.isClvmPoolType(storagePool.getPoolType()))) { + if (ClvmPoolManager.isClvmPoolType(storagePool.getPoolType())) { + hostId = getClvmLockHostFromVmVolumes(vm.getId()); + if (hostId != null) { + logger.debug("Using CLVM lock host {} from VM {}'s existing volumes for new volume creation", + hostId, vm.getUuid()); + } + } + + if (hostId == null) { + List poolHosts = storagePoolHostDao.listByPoolId(storagePool.getId()); + if (!poolHosts.isEmpty()) { + hostId = poolHosts.get(0).getHostId(); + logger.debug("Selected host {} from storage pool {} for stopped VM {} volume creation", + hostId, storagePool.getUuid(), vm.getUuid()); + } } } @@ -1454,6 +1599,9 @@ public Volume migrateVolume(Volume volume, StoragePool destPool) throws StorageU _snapshotDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); _snapshotDataStoreDao.updateVolumeIds(vol.getId(), result.getVolume().getId()); } + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "migrated"); } return result.getVolume(); } catch (InterruptedException | ExecutionException e) { @@ -1479,6 +1627,10 @@ public Volume liveMigrateVolume(Volume volume, StoragePool destPool) { logger.error("Volume [{}] migration failed due to [{}].", volToString, result.getResult()); return null; } + + // For CLVM volumes attached to a VM, update the CLVM_LOCK_HOST_ID after live migration + updateClvmLockHostAfterMigration(result.getVolume(), destPool, "live-migrated"); + return result.getVolume(); } catch (InterruptedException | ExecutionException e) { logger.error("Volume [{}] migration failed due to [{}].", volToString, e.getMessage()); @@ -1521,6 +1673,22 @@ public void migrateVolumes(VirtualMachine vm, VirtualMachineTO vmTo, Host srcHos logger.error(msg); throw new CloudRuntimeException(msg); } + for (Map.Entry entry : volumeToPool.entrySet()) { + Volume volume = entry.getKey(); + StoragePool destPool = entry.getValue(); + StoragePoolVO srcPool = _storagePoolDao.findById(volume.getPoolId()); + if (srcPool != null && srcPool.getId() == destPool.getId() && + ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + if (!clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), + volume.getPath(), srcPool, srcHost.getId(), destHost.getId())) { + throw new CloudRuntimeException(String.format( + "Failed to transfer CLVM lock for volume [%s] to destination host [%s].", + volume.getUuid(), destHost.getId())); + } + } else { + updateClvmLockHostAfterMigration(volume, destPool, "vm-migrated"); + } + } } catch (InterruptedException | ExecutionException e) { logger.error("Failed to migrate VM [{}] along with its volumes due to [{}].", vm, e.getMessage()); logger.debug("Exception: ", e); @@ -1853,6 +2021,19 @@ private Pair recreateVolume(VolumeVO vol, VirtualMachinePro future = volService.createManagedStorageVolumeFromTemplateAsync(volume, destPool.getId(), templ, hostId); } else { + // For CLVM pools, set the destination host hint so volume is created on the correct host + // This avoids the need for shared mode activation and improves performance + StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId()); + if (poolVO != null && ClvmPoolManager.isClvmPoolType(poolVO.getPoolType())) { + Long hostId = vm.getVirtualMachine().getHostId(); + if (hostId != null) { + volume.setDestinationHostId(hostId); + clvmPoolManager.setClvmLockHostId(volume.getId(), hostId); + logger.info("CLVM pool detected during volume creation from template. Setting lock host {} for volume {} (persisted to DB) to route creation to correct host", + hostId, volume.getUuid()); + } + } + future = volService.createVolumeFromTemplateAsync(volume, destPool.getId(), templ); } } @@ -1976,13 +2157,18 @@ public void prepare(VirtualMachineProfile vm, DeployDestination dest) throws Sto throw new CloudRuntimeException(msg); } - // don't allow to start vm that doesn't have a root volume if (_volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT).isEmpty()) { throw new CloudRuntimeException(String.format("ROOT volume is missing, unable to prepare volumes for the VM [%s].", vm.getVirtualMachine())); } List vols = _volsDao.findUsableVolumesForInstance(vm.getId()); + VirtualMachine vmInstance = vm.getVirtualMachine(); + VMInstanceVO vmInstanceVO = vmInstanceDao.findById(vmInstance.getId()); + if (vmInstance.getState() == State.Starting && dest.getHost() != null) { + transferClvmLocksForVmStart(vols, dest.getHost().getId(), vmInstanceVO); + } + List tasks = getTasks(vols, dest.getStorageForDisks(), vm); Volume vol = null; PrimaryDataStore store; diff --git a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml index 17c5002c718b..8f93ae5b35a3 100644 --- a/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml +++ b/engine/orchestration/src/main/resources/META-INF/cloudstack/core/spring-engine-orchestration-core-context.xml @@ -44,6 +44,8 @@ value="#{storagePoolAllocatorsRegistry.registered}" /> + + + diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index a07870d09af2..c656e1a282af 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -60,6 +61,7 @@ import com.cloud.network.Network; import com.cloud.network.NetworkModel; import com.cloud.resource.ResourceManager; +import com.cloud.storage.clvm.ClvmPoolManager; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; @@ -336,6 +338,19 @@ public void cleanup() { } } + private ClvmPoolManager injectMockedClvmPoolManager() { + ClvmPoolManager clvmPoolManagerMock = mock(ClvmPoolManager.class); + ReflectionTestUtils.setField(virtualMachineManagerImpl, "clvmPoolManager", clvmPoolManagerMock); + return clvmPoolManagerMock; + } + + private Method getUpdateClvmLockHostForVmVolumesMethod() throws NoSuchMethodException { + Method method = VirtualMachineManagerImpl.class.getDeclaredMethod( + "updateClvmLockHostForVmVolumes", long.class, long.class); + method.setAccessible(true); + return method; + } + @Test public void testaddHostIpToCertDetailsIfConfigAllows() { Host vmHost = mock(Host.class); @@ -1954,4 +1969,181 @@ public void testUnmanageSuccessKvm() throws Exception { } } + @Test + public void testUpdateClvmLockHostForVmVolumes_WithClvmVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO clvmVolume1 = mock(VolumeVO.class); + VolumeVO clvmVolume2 = mock(VolumeVO.class); + + when(clvmVolume1.getId()).thenReturn(1L); + when(clvmVolume1.getPoolId()).thenReturn(poolId); + when(clvmVolume2.getId()).thenReturn(2L); + when(clvmVolume2.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = mock(StoragePoolVO.class); + when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume1, clvmVolume2)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(clvmPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNonClvmVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO nfsVolume = mock(VolumeVO.class); + when(nfsVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO nfsPool = mock(StoragePoolVO.class); + when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(nfsVolume)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(nfsPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithMixedVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long clvmPoolId = 10L; + long nfsPoolId = 20L; + + VolumeVO clvmVolume = mock(VolumeVO.class); + VolumeVO nfsVolume = mock(VolumeVO.class); + + when(clvmVolume.getId()).thenReturn(1L); + when(clvmVolume.getPoolId()).thenReturn(clvmPoolId); + when(nfsVolume.getPoolId()).thenReturn(nfsPoolId); + + StoragePoolVO clvmPool = mock(StoragePoolVO.class); + when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO nfsPool = mock(StoragePoolVO.class); + when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(clvmVolume, nfsVolume)); + when(storagePoolDaoMock.findById(clvmPoolId)).thenReturn(clvmPool); + when(storagePoolDaoMock.findById(nfsPoolId)).thenReturn(nfsPool); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, never()).setClvmLockHostId(2L, destHostId); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNoVolumes() throws Exception { + long vmId = 100L; + long destHostId = 2L; + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Collections.emptyList()); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNullPoolId() throws Exception { + long vmId = 100L; + long destHostId = 2L; + + VolumeVO volumeWithoutPool = mock(VolumeVO.class); + when(volumeWithoutPool.getPoolId()).thenReturn(null); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volumeWithoutPool)); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(storagePoolDaoMock, never()).findById(anyLong()); + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_WithNullPool() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long poolId = 10L; + + VolumeVO volume = mock(VolumeVO.class); + when(volume.getPoolId()).thenReturn(poolId); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume)); + when(storagePoolDaoMock.findById(poolId)).thenReturn(null); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, never()).setClvmLockHostId(anyLong(), anyLong()); + } + + @Test + public void testUpdateClvmLockHostForVmVolumes_MultipleClvmPools() throws Exception { + long vmId = 100L; + long destHostId = 2L; + long pool1Id = 10L; + long pool2Id = 20L; + + VolumeVO volume1 = mock(VolumeVO.class); + VolumeVO volume2 = mock(VolumeVO.class); + VolumeVO volume3 = mock(VolumeVO.class); + + when(volume1.getId()).thenReturn(1L); + when(volume1.getPoolId()).thenReturn(pool1Id); + when(volume2.getId()).thenReturn(2L); + when(volume2.getPoolId()).thenReturn(pool2Id); + when(volume3.getId()).thenReturn(3L); + when(volume3.getPoolId()).thenReturn(pool1Id); + + StoragePoolVO clvmPool1 = mock(StoragePoolVO.class); + when(clvmPool1.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO clvmPool2 = mock(StoragePoolVO.class); + when(clvmPool2.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + when(volumeDaoMock.findByInstance(vmId)).thenReturn(Arrays.asList(volume1, volume2, volume3)); + when(storagePoolDaoMock.findById(pool1Id)).thenReturn(clvmPool1); + when(storagePoolDaoMock.findById(pool2Id)).thenReturn(clvmPool2); + + ClvmPoolManager clvmPoolManagerMock = injectMockedClvmPoolManager(); + + Method method = getUpdateClvmLockHostForVmVolumesMethod(); + method.invoke(virtualMachineManagerImpl, vmId, destHostId); + + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(1L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(2L, destHostId); + verify(clvmPoolManagerMock, times(1)).setClvmLockHostId(3L, destHostId); + } + } diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java index b4a26c17e2e5..a184b4540a96 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.engine.orchestration; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -30,6 +32,7 @@ import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.offering.DiskOffering; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.ScopeType; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; @@ -42,6 +45,7 @@ import com.cloud.uservm.UserVm; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.utils.Pair; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; @@ -67,6 +71,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedConstruction; @@ -640,4 +645,305 @@ public void getVolumeCheckpointPathsAndImageStoreUrlsTestReturnCheckpointIfKVMAn Assert.assertEquals(1, result.second().size()); } + @Test + public void testTransferClvmLocksForVmStart_WithClvmVolumes() throws Exception { + Long destHostId = 2L; + Long currentHostId = 1L; + Long poolId = 10L; + + VolumeVO clvmVolume1 = Mockito.mock(VolumeVO.class); + VolumeVO clvmVolume2 = Mockito.mock(VolumeVO.class); + + Mockito.when(clvmVolume1.getId()).thenReturn(101L); + Mockito.when(clvmVolume1.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume1.getUuid()).thenReturn("vol-uuid-1"); + Mockito.when(clvmVolume1.getPath()).thenReturn("vol-path-1"); + + Mockito.when(clvmVolume2.getId()).thenReturn(102L); + Mockito.when(clvmVolume2.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume2.getUuid()).thenReturn("vol-uuid-2"); + Mockito.when(clvmVolume2.getPath()).thenReturn("vol-path-2"); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(102L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume1, clvmVolume2), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("vol-uuid-1"), Mockito.eq(101L), Mockito.eq("vol-path-1"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("vol-uuid-2"), Mockito.eq(102L), Mockito.eq("vol-path-2"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + } + + @Test + public void testTransferClvmLocksForVmStart_WithNonClvmVolumes() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO nfsVolume = Mockito.mock(VolumeVO.class); + Mockito.when(nfsVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(nfsPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(nfsVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_NoLockTransferNeeded() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class), + ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(destHostId); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + java.lang.reflect.Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_EmptyVolumeList() throws Exception { + Long destHostId = 2L; + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, new ArrayList(), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.never()).getClvmLockHostId(Mockito.anyLong(), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.anyBoolean()); + } + + @Test + public void testTransferClvmLocksForVmStart_NullPoolId() throws Exception { + Long destHostId = 2L; + + VolumeVO volumeWithoutPool = Mockito.mock(VolumeVO.class); + Mockito.when(volumeWithoutPool.getPoolId()).thenReturn(null); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(volumeWithoutPool), destHostId, vmInstance); + + Mockito.verify(storagePoolDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_SetInitialLockHost() throws Exception { + Long destHostId = 2L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), ArgumentMatchers.nullable(String.class), + ArgumentMatchers.nullable(String.class), Mockito.any(), Mockito.eq(true))).thenReturn(null); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).setClvmLockHostId(101L, destHostId); + Mockito.verify(clvmPoolManager, Mockito.never()).transferClvmVolumeLock( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.anyLong(), Mockito.anyLong()); + } + + @Test + public void testTransferClvmLocksForVmStart_MixedVolumes() throws Exception { + Long destHostId = 2L; + Long currentHostId = 1L; + Long clvmPoolId = 10L; + Long nfsPoolId = 20L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(clvmPoolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("clvm-vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("clvm-vol-path"); + + VolumeVO nfsVolume = Mockito.mock(VolumeVO.class); + Mockito.when(nfsVolume.getPoolId()).thenReturn(nfsPoolId); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + StoragePoolVO nfsPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(nfsPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(true); + + Mockito.when(storagePoolDao.findById(clvmPoolId)).thenReturn(clvmPool); + Mockito.when(storagePoolDao.findById(nfsPoolId)).thenReturn(nfsPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + method.invoke(volumeOrchestrator, List.of(clvmVolume, nfsVolume), destHostId, vmInstance); + + Mockito.verify(clvmPoolManager, Mockito.times(1)).transferClvmVolumeLock( + Mockito.eq("clvm-vol-uuid"), Mockito.eq(101L), Mockito.eq("clvm-vol-path"), + Mockito.eq(clvmPool), Mockito.eq(currentHostId), Mockito.eq(destHostId)); + } + + @Test(expected = CloudRuntimeException.class) + public void testTransferClvmLocksForVmStart_TransferFails() throws Throwable { + Long destHostId = 2L; + Long currentHostId = 1L; + Long poolId = 10L; + + VolumeVO clvmVolume = Mockito.mock(VolumeVO.class); + Mockito.when(clvmVolume.getId()).thenReturn(101L); + Mockito.when(clvmVolume.getPoolId()).thenReturn(poolId); + Mockito.when(clvmVolume.getUuid()).thenReturn("vol-uuid"); + Mockito.when(clvmVolume.getPath()).thenReturn("vol-path"); + + StoragePoolVO clvmPool = Mockito.mock(StoragePoolVO.class); + Mockito.when(clvmPool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + + VMInstanceVO vmInstance = Mockito.mock(VMInstanceVO.class); + Mockito.when(vmInstance.getInstanceName()).thenReturn(MOCK_VM_NAME); + + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(101L), Mockito.anyString(), + Mockito.anyString(), Mockito.any(), Mockito.eq(true))).thenReturn(currentHostId); + Mockito.when(clvmPoolManager.transferClvmVolumeLock(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())).thenReturn(false); + + Mockito.when(storagePoolDao.findById(poolId)).thenReturn(clvmPool); + + setField(volumeOrchestrator, "clvmPoolManager", clvmPoolManager); + setField(volumeOrchestrator, "_storagePoolDao", storagePoolDao); + + Method method = VolumeOrchestrator.class.getDeclaredMethod( + "transferClvmLocksForVmStart", List.class, Long.class, VMInstanceVO.class); + method.setAccessible(true); + + try { + method.invoke(volumeOrchestrator, List.of(clvmVolume), destHostId, vmInstance); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = findField(target.getClass(), fieldName); + if (field == null) { + throw new NoSuchFieldException("Field " + fieldName + " not found in " + target.getClass()); + } + field.setAccessible(true); + field.set(target, value); + } + + private Field findField(Class clazz, String fieldName) { + Class current = clazz; + while (current != null && current != Object.class) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } + } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java index 6cfd2608f5de..3d04f6679def 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java @@ -23,6 +23,7 @@ import com.cloud.dc.ClusterVO; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface ClusterDao extends GenericDao { @@ -61,4 +62,6 @@ public interface ClusterDao extends GenericDao { List listDistinctStorageAccessGroups(String name, String keyword); List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch); + + List listByZonesAndHypervisorType(List zoneIds, HypervisorType hypervisorType, Filter filter); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java index c63af0a237ba..9b6f0f7ac3a6 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java @@ -20,6 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.cpu.CPU; @@ -38,6 +40,7 @@ import com.cloud.org.Grouping; import com.cloud.org.Managed; import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.JoinBuilder; @@ -413,4 +416,19 @@ public List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, Hypervi } return customSearch(sc, null); } + + @Override + public List listByZonesAndHypervisorType(List zoneIds, HypervisorType hypervisorType, Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + sb.and("hypervisorType", sb.entity().getHypervisorType(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + sc.setParameters("hypervisorType", hypervisorType.toString()); + return listBy(sc, filter); + } } diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index d51b4eca0577..468caf1e2271 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java @@ -45,7 +45,7 @@ import com.cloud.cpu.CPU; import org.apache.cloudstack.util.CPUArchConverter; import org.apache.cloudstack.util.HypervisorTypeConverter; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; @@ -856,7 +856,8 @@ public boolean checkHostServiceOfferingTags(ServiceOffering serviceOffering) { } if (BooleanUtils.isTrue(this.getIsTagARule())) { - return TagAsRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value()); + return GenericRuleHelper.interpretTagAsRule(this.getHostTags().get(0), serviceOffering.getHostTag(), HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key()); } if (StringUtils.isEmpty(serviceOffering.getHostTag())) { diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 5f5b2affee08..5c0d04fb2be8 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java @@ -32,12 +32,17 @@ import com.cloud.utils.db.GenericDao; import com.cloud.utils.fsm.StateDao; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.framework.config.ConfigKey; /** * Data Access Object for server * */ public interface HostDao extends GenericDao, StateDao { + + ConfigKey guestOsRuleExecutionTimeout = new ConfigKey<>("Advanced", Long.class, "guest.os.rule.execution.timeout", "3000", "The maximum runtime, in milliseconds, " + + "to execute a guest OS rule; if it is reached, a timeout will happen.", true); + long countBy(long clusterId, ResourceState... states); Integer countAllByType(final Host.Type type); @@ -220,6 +225,8 @@ int countHostsByMsResourceStateTypeAndHypervisorType(long msId, List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags); + List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName); + List findClustersThatMatchHostTagRule(String computeOfferingTags); List listSsvmHostsWithPendingMigrateJobsOrderedByJobCount(); diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 99c9a979c3bf..cd4423dfa269 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java @@ -37,7 +37,9 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.utils.jsinterpreter.GenericRuleHelper; import org.apache.commons.collections.CollectionUtils; import com.cloud.agent.api.VgpuTypesInfo; @@ -84,7 +86,7 @@ @DB @TableGenerator(name = "host_req_sq", table = "op_host", pkColumnName = "id", valueColumnName = "sequence", allocationSize = 1) -public class HostDaoImpl extends GenericDaoBase implements HostDao { //FIXME: , ExternalIdDao { +public class HostDaoImpl extends GenericDaoBase implements HostDao, Configurable { private static final String LIST_HOST_IDS_BY_HOST_TAGS = "SELECT filtered.host_id, COUNT(filtered.tag) AS tag_count " + "FROM (SELECT host_id, tag, is_tag_a_rule FROM host_tags GROUP BY host_id,tag,is_tag_a_rule) AS filtered " @@ -1520,11 +1522,13 @@ private List findHostIdsByHostTags(String hostTags){ } } + @Override public List findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) { List hostTagVOList = _hostTagsDao.findHostRuleTags(); List result = new ArrayList<>(); for (HostTagVO rule: hostTagVOList) { - if (TagAsRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value())) { + if (GenericRuleHelper.interpretTagAsRule(rule.getTag(), computeOfferingTags, HostTagsDao.hostTagRuleExecutionTimeout.value(), + HostTagsDao.hostTagRuleExecutionTimeout.key())) { result.add(findById(rule.getHostId())); } } @@ -1532,6 +1536,23 @@ public List findHostsWithTagRuleThatMatchComputeOfferingTags(String comp return result; } + @Override + public List findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName) { + List hostIdsWithGuestOsRule = _detailsDao.findByName(Host.GUEST_OS_RULE); + List hostsWithIncompatibleRules = new ArrayList<>(); + for (DetailVO guestOsRule : hostIdsWithGuestOsRule) { + if (!GenericRuleHelper.interpretGuestOsRule(guestOsRule.getValue(), templateGuestOSName, HostDao.guestOsRuleExecutionTimeout.value(), + HostDao.guestOsRuleExecutionTimeout.key())) { + logger.trace("The guest OS rule [{}] of the host with ID [{}] is incompatible with the OS of the VM.", + guestOsRule.getHostId(), guestOsRule.getValue()); + hostsWithIncompatibleRules.add(findById(guestOsRule.getHostId())); + } + } + logger.trace("The hosts with the following IDs [{}] are incompatible with the VM considering their guest OS rule.", + hostsWithIncompatibleRules); + return hostsWithIncompatibleRules; + } + public List findClustersThatMatchHostTagRule(String computeOfferingTags) { Set result = new HashSet<>(); List hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags); @@ -1984,4 +2005,14 @@ public List listDistinctStorageAccessGroups(String name, String keyword) return customSearch(sc, null); } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {guestOsRuleExecutionTimeout}; + } + + @Override + public String getConfigComponentName() { + return HostDaoImpl.class.getSimpleName(); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java index fdca6e43f00f..8406f80ed09d 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java @@ -24,6 +24,7 @@ import com.cloud.network.Network.GuestType; import com.cloud.network.Network.State; import com.cloud.network.Networks.TrafficType; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.fsm.StateDao; @@ -96,8 +97,13 @@ public interface NetworkDao extends GenericDao, StateDao serviceProviderMap); + List listByZoneAndTrafficType(long zoneId, TrafficType trafficType, Filter filter); + List listByZoneAndTrafficType(long zoneId, TrafficType trafficType); + List listByZonesTrafficTypeAndOwners(List zoneIds, final TrafficType trafficType, + List accountIds, List domainIds, Filter filter); + void setCheckForGc(long networkId); int getNetworkCountByNetworkOffId(long networkOfferingId); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java index 9f7ffabac930..f3ece4879040 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java @@ -19,6 +19,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -29,9 +30,9 @@ import javax.inject.Inject; import javax.persistence.TableGenerator; -import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.network.Network; @@ -63,6 +64,7 @@ import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.SequenceFetcher; import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; @Component @@ -632,12 +634,46 @@ public List listBy(final long accountId, final long dataCenterId, fin } @Override - public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType) { + public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType, Filter filter) { final SearchCriteria sc = AllFieldsSearch.create(); sc.setParameters("datacenter", zoneId); sc.setParameters("trafficType", trafficType); - return listBy(sc, null); + return listBy(sc, filter); + } + + @Override + public List listByZoneAndTrafficType(final long zoneId, final TrafficType trafficType) { + return listByZoneAndTrafficType(zoneId, trafficType, null); + } + + @Override + public List listByZonesTrafficTypeAndOwners(List zoneIds, final TrafficType trafficType, + List accountIds, List domainIds, Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + sb.and("trafficType", sb.entity().getTrafficType(), Op.EQ); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + sc.setParameters("trafficType", trafficType); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + return listBy(sc, filter); } @Override diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java index 793334731095..2b18bcbd87bd 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDao.java @@ -31,4 +31,6 @@ public interface SecurityGroupDao extends GenericDao { List findByAccountAndNames(Long accountId, String... names); int removeByAccountId(long accountId); + + List listByIds(List ids); } diff --git a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java index 019cf5fec462..ffb45629b625 100644 --- a/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/security/dao/SecurityGroupDaoImpl.java @@ -129,4 +129,14 @@ public boolean expunge(Long id) { txn.commit(); return result; } + + @Override + public List listByIds(List ids) { + SearchBuilder idsSearch = createSearchBuilder(); + idsSearch.and("ids", idsSearch.entity().getId(), SearchCriteria.Op.IN); + idsSearch.done(); + SearchCriteria sc = idsSearch.create(); + sc.setParameters("ids", ids.toArray()); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java index 9beea0037444..ea97f663768f 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java @@ -30,7 +30,7 @@ public interface DiskOfferingDao extends GenericDao { List listAllBySizeAndProvisioningType(long size, Storage.ProvisioningType provisioningType); - List findCustomDiskOfferings(); + List listCustomDiskOfferings(); List listByStorageTag(String tag); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java index 4ca3fe9f12ac..44e6551da17d 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.storage.dao; +import static org.apache.cloudstack.query.QueryService.SortKeyAscending; + import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -33,6 +35,7 @@ import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.Storage; import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -138,13 +141,14 @@ public List listAllBySizeAndProvisioningType(long size, Storage. } @Override - public List findCustomDiskOfferings() { + public List listCustomDiskOfferings() { SearchBuilder sb = createSearchBuilder(); sb.and("customized", sb.entity().isCustomized(), SearchCriteria.Op.EQ); sb.done(); SearchCriteria sc = sb.create(); sc.setParameters("customized", true); - return listBy(sc); + Filter searchFilter = new Filter(DiskOfferingVO.class, "sortKey", SortKeyAscending.value()); + return listBy(sc, searchFilter); } @Override diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java index 4c9f906b68a9..aec06d6d0003 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java @@ -106,4 +106,6 @@ public interface VMTemplateDao extends GenericDao, StateDao< VMTemplateVO findActiveSystemTemplateByHypervisorArchAndUrlPath(HypervisorType hypervisorType, CPU.CPUArch arch, String urlPathSuffix); + + VMTemplateVO findByAccountAndName(Long accountId, String templateName); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index 9b5d0edc599d..8c6e3fe0983f 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -945,4 +945,12 @@ public boolean updateState( } return rows > 0; } + + @Override + public VMTemplateVO findByAccountAndName(Long accountId, String templateName) { + SearchCriteria sc = NameAccountIdSearch.create(); + sc.setParameters("name", templateName); + sc.setParameters("accountId", accountId); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 717e3e782f2f..7d566a1ea01e 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -46,6 +46,8 @@ public interface VolumeDao extends GenericDao, StateDao findByInstanceAndType(long id, Volume.Type vType); + List findByInstanceAndNotStates(long id, Volume.State...states); + List findIncludingRemovedByInstanceAndType(long id, Volume.Type vType); List findNonDestroyedVolumesByInstanceIdAndPoolId(long instanceId, long poolId); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index fce4d1f7233d..91f1c7f5eb69 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -208,6 +208,17 @@ public List findByInstanceAndType(long id, Type vType) { return listBy(sc); } + @Override + public List findByInstanceAndNotStates(long id, Volume.State...states) { + SearchBuilder sb = createSearchBuilder(); + sb.and("instanceId", sb.entity().getInstanceId(), Op.EQ); + sb.and("state", sb.entity().getState(), Op.NIN); + SearchCriteria sc = sb.create(); + sc.setParameters("instanceId", id); + sc.setParameters("state", (Object[]) states); + return listBy(sc); + } + @Override public List findIncludingRemovedByInstanceAndType(long id, Type vType) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java index bacb09b98793..5efaea40a943 100644 --- a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java +++ b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagDao.java @@ -20,11 +20,13 @@ import java.util.Map; import java.util.Set; +import org.apache.cloudstack.api.response.ResourceTagResponse; + import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.tags.ResourceTagVO; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; -import org.apache.cloudstack.api.response.ResourceTagResponse; public interface ResourceTagDao extends GenericDao { @@ -60,4 +62,13 @@ public interface ResourceTagDao extends GenericDao { void removeByResourceIdAndKey(long resourceId, ResourceObjectType resourceType, String key); List listByResourceUuid(String resourceUuid); + + List listByResourceTypeKeyPrefixAndOwners(ResourceObjectType resourceType, String key, + List accountIds, List domainIds, + Filter filter); + + ResourceTagVO findByResourceTypeKeyPrefixAndValue(ResourceObjectType resourceType, String key, String value); + + List listByResourceTypeIdAndKeyPrefix(ResourceObjectType resourceType, long resourceId, String key); + } diff --git a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java index cc9d99e6ab16..22c7b7b2ee5b 100644 --- a/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/tags/dao/ResourceTagsDaoImpl.java @@ -16,19 +16,22 @@ // under the License. package com.cloud.tags.dao; -import java.util.List; -import java.util.Set; -import java.util.Map; import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.cloudstack.api.response.ResourceTagResponse; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.tags.ResourceTagVO; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; @@ -120,4 +123,62 @@ public List listByResourceUuid(String resourceUuid) { sc.setParameters("resourceUuid", resourceUuid); return listBy(sc); } + + @Override + public List listByResourceTypeKeyPrefixAndOwners(ResourceObjectType resourceType, String key, + List accountIds, List domainIds, + Filter filter) { + GenericSearchBuilder sb = createSearchBuilder(String.class); + sb.select(null, SearchCriteria.Func.DISTINCT, sb.entity().getValue()); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("key", key + "%"); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + return customSearch(sc, filter); + } + + @Override + public ResourceTagVO findByResourceTypeKeyPrefixAndValue(ResourceObjectType resourceType, String key, + String value) { + SearchBuilder sb = createSearchBuilder(); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + sb.and("value", sb.entity().getValue(), Op.EQ); + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("key", key + "%"); + sc.setParameters("value", value); + return findOneBy(sc); + } + + @Override + public List listByResourceTypeIdAndKeyPrefix(ResourceObjectType resourceType, long resourceId, + String key) { + SearchBuilder sb = createSearchBuilder(); + sb.and("resourceType", sb.entity().getResourceType(), Op.EQ); + sb.and("resourceId", sb.entity().getResourceId(), Op.EQ); + sb.and("key", sb.entity().getKey(), Op.LIKE); + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("resourceType", resourceType); + sc.setParameters("resourceId", resourceId); + sc.setParameters("key", key + "%"); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index 96a1b6e3bd16..7767b7512b91 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -26,6 +26,8 @@ public interface NicDao extends GenericDao { List listByVmId(long instanceId); + int countByVmId(long instanceId); + List listByVmIdOrderByDeviceId(long instanceId); List listIpAddressInNetwork(long networkConfigId); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index 78966a09e97c..6202dabf730e 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -126,6 +126,13 @@ public List listByVmId(long instanceId) { return listBy(sc); } + @Override + public int countByVmId(long instanceId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("instance", instanceId); + return getCount(sc); + } + @Override public List listByVmIdOrderByDeviceId(long instanceId) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java index 4c8208b4be84..baa331d81f67 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicSecondaryIpVO.java @@ -43,7 +43,7 @@ public NicSecondaryIpVO(long nicId, String ipaddr, long vmId, long accountId, lo this.networkId = networkId; } - public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long vmId, long accountId, long domainId, long networkId) { + public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long vmId, long accountId, long domainId, long networkId, String description) { this.nicId = nicId; this.vmId = vmId; this.ip4Address = ip4Address; @@ -51,6 +51,7 @@ public NicSecondaryIpVO(long nicId, String ip4Address, String ip6Address, long v this.accountId = accountId; this.domainId = domainId; this.networkId = networkId; + this.description = description; } protected NicSecondaryIpVO() { @@ -88,6 +89,18 @@ protected NicSecondaryIpVO() { @Column(name = "vmId") long vmId; + @Column(name = "description") + String description; + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + @Override public String toString() { return String.format("NicSecondaryIp %s", diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index 4fd3e729e0d2..1a5b8cedd9ea 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -197,4 +197,6 @@ List searchRemovedByRemoveDate(final Date startDate, final Date en List listDeleteProtectedVmsByAccountId(long accountId); List listDeleteProtectedVmsByDomainIds(Set domainIds); + + List listIdsByHostIdForVolumeStats(long hostIds); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 019d152ce5c3..ae1e838649ba 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -1336,4 +1336,20 @@ public List listDeleteProtectedVmsByDomainIds(Set domainIds) Filter filter = new Filter(VMInstanceVO.class, null, false, 0L, 10L); return listBy(sc, filter); } + + @Override + public List listIdsByHostIdForVolumeStats(long hostId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getId()); + sb.and().op("host", sb.entity().getHostId(), SearchCriteria.Op.EQ); + sb.or().op("hostNull", sb.entity().getHostId(), Op.NULL); + sb.and("lastHost", sb.entity().getLastHostId(), SearchCriteria.Op.EQ); + sb.cp(); + sb.cp(); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("host", hostId); + sc.setParameters("lastHost", hostId); + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java index eb38b08f6151..7a6bd7a3b140 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/acl/ApiKeyPairVO.java @@ -70,7 +70,7 @@ public class ApiKeyPairVO implements ApiKeyPair { @Temporal(value = TemporalType.TIMESTAMP) private Date created = Date.from(Instant.now()); - @Column(name = "description") + @Column(name = "description", length = 1024) private String description = ""; @Column(name = "api_key", nullable = false) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java index 0f8a10fb7be6..d589f9e6bef8 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@ -103,6 +103,18 @@ public class BackupVO implements Backup { @Column(name = "backup_schedule_id") private Long backupScheduleId; + @Column(name = "from_checkpoint_id") + private String fromCheckpointId; + + @Column(name = "to_checkpoint_id") + private String toCheckpointId; + + @Column(name = "checkpoint_create_time") + private Long checkpointCreateTime; + + @Column(name = "host_id") + private Long hostId; + @Transient Map details; @@ -288,4 +300,40 @@ public Long getBackupScheduleId() { public void setBackupScheduleId(Long backupScheduleId) { this.backupScheduleId = backupScheduleId; } + + @Override + public String getFromCheckpointId() { + return fromCheckpointId; + } + + public void setFromCheckpointId(String fromCheckpointId) { + this.fromCheckpointId = fromCheckpointId; + } + + @Override + public String getToCheckpointId() { + return toCheckpointId; + } + + public void setToCheckpointId(String toCheckpointId) { + this.toCheckpointId = toCheckpointId; + } + + @Override + public Long getCheckpointCreateTime() { + return checkpointCreateTime; + } + + public void setCheckpointCreateTime(Long checkpointCreateTime) { + this.checkpointCreateTime = checkpointCreateTime; + } + + @Override + public Long getHostId() { + return hostId; + } + + public void setHostId(Long hostId) { + this.hostId = hostId; + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java new file mode 100644 index 000000000000..ec9b927b63e4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/ImageTransferVO.java @@ -0,0 +1,242 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "image_transfer") +public class ImageTransferVO implements ImageTransfer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "backup_id") + private Long backupId; + + @Column(name = "volume_id") + private long volumeId; + + @Column(name = "host_id") + private long hostId; + + @Column(name = "socket") + private String socket; + + @Column(name = "file") + private String file; + + @Column(name = "transfer_url") + private String transferUrl; + + @Enumerated(value = EnumType.STRING) + @Column(name = "phase") + private Phase phase; + + @Enumerated(value = EnumType.STRING) + @Column(name = "direction") + private Direction direction; + + @Enumerated(value = EnumType.STRING) + @Column(name = "backend") + private Backend backend; + + @Column(name = "signed_ticket_id") + private String signedTicketId; + + @Column(name = "account_id") + Long accountId; + + @Column(name = "domain_id") + Long domainId; + + @Column(name = "data_center_id") + Long dataCenterId; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public ImageTransferVO() { + } + + private ImageTransferVO(String uuid, long volumeId, long hostId, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this.uuid = uuid; + this.volumeId = volumeId; + this.hostId = hostId; + this.phase = phase; + this.direction = direction; + this.accountId = accountId; + this.domainId = domainId; + this.dataCenterId = dataCenterId; + this.created = new Date(); + } + + public ImageTransferVO(String uuid, Long backupId, long volumeId, long hostId, String socket, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this(uuid, volumeId, hostId, phase, direction, accountId, domainId, dataCenterId); + this.backupId = backupId; + this.socket = socket; + this.backend = Backend.nbd; + } + + public ImageTransferVO(String uuid, long volumeId, long hostId, String file, Phase phase, Direction direction, Long accountId, Long domainId, Long dataCenterId) { + this(uuid, volumeId, hostId, phase, direction, accountId, domainId, dataCenterId); + this.file = file; + this.backend = Backend.file; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public Long getBackupId() { + return backupId; + } + + public void setBackupId(long backupId) { + this.backupId = backupId; + } + + @Override + public long getVolumeId() { + return volumeId; + } + + public void setVolumeId(long volumeId) { + this.volumeId = volumeId; + } + + @Override + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + public void setSocket(String socket) { + this.socket = socket; + } + + @Override + public String getTransferUrl() { + return transferUrl; + } + + public void setTransferUrl(String transferUrl) { + this.transferUrl = transferUrl; + } + + @Override + public Phase getPhase() { + return phase; + } + + public void setPhase(Phase phase) { + this.phase = phase; + this.updated = new Date(); + } + + @Override + public Direction getDirection() { + return direction; + } + + public void setDirection(Direction direction) { + this.direction = direction; + } + + @Override + public Backend getBackend() { + return backend; + } + + @Override + public String getSignedTicketId() { + return signedTicketId; + } + + public void setSignedTicketId(String signedTicketId) { + this.signedTicketId = signedTicketId; + } + + @Override + public Class getEntityType() { + return ImageTransfer.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public long getDataCenterId() { + return dataCenterId; + } + + public Date getCreated() { + return created; + } + + public Date getUpdated() { + return updated; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java new file mode 100644 index 000000000000..9a9fbeb8814e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDao.java @@ -0,0 +1,36 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup.dao; + +import java.util.List; + +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.ImageTransferVO; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +public interface ImageTransferDao extends GenericDao { + List listByBackupId(Long backupId); + ImageTransferVO findByUuid(String uuid); + ImageTransferVO findByVolume(Long volumeId); + ImageTransferVO findUnfinishedByVolume(Long volumeId); + List listByPhaseAndDirection(ImageTransfer.Phase phase, ImageTransfer.Direction direction); + List listByZonesAndOwners(List zoneIds, List accountIds, List domainIds, + Filter filter); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java new file mode 100644 index 000000000000..2e36f924e4ef --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/ImageTransferDaoImpl.java @@ -0,0 +1,136 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.backup.dao; + +import java.util.Collections; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.backup.ImageTransferVO; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class ImageTransferDaoImpl extends GenericDaoBase implements ImageTransferDao { + + private SearchBuilder backupIdSearch; + private SearchBuilder uuidSearch; + private SearchBuilder volumeSearch; + private SearchBuilder volumeUnfinishedSearch; + private SearchBuilder phaseDirectionSearch; + + public ImageTransferDaoImpl() { + } + + @PostConstruct + protected void init() { + backupIdSearch = createSearchBuilder(); + backupIdSearch.and("backupId", backupIdSearch.entity().getBackupId(), SearchCriteria.Op.EQ); + backupIdSearch.done(); + + uuidSearch = createSearchBuilder(); + uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ); + uuidSearch.done(); + + volumeSearch = createSearchBuilder(); + volumeSearch.and("volumeId", volumeSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + volumeSearch.done(); + + volumeUnfinishedSearch = createSearchBuilder(); + volumeUnfinishedSearch.and("volumeId", volumeUnfinishedSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + volumeUnfinishedSearch.and("phase", volumeUnfinishedSearch.entity().getPhase(), SearchCriteria.Op.NEQ); + volumeUnfinishedSearch.done(); + + phaseDirectionSearch = createSearchBuilder(); + phaseDirectionSearch.and("phase", phaseDirectionSearch.entity().getPhase(), SearchCriteria.Op.EQ); + phaseDirectionSearch.and("direction", phaseDirectionSearch.entity().getDirection(), SearchCriteria.Op.EQ); + phaseDirectionSearch.done(); + } + + @Override + public List listByBackupId(Long backupId) { + SearchCriteria sc = backupIdSearch.create(); + sc.setParameters("backupId", backupId); + return listBy(sc); + } + + @Override + public ImageTransferVO findByUuid(String uuid) { + SearchCriteria sc = uuidSearch.create(); + sc.setParameters("uuid", uuid); + return findOneBy(sc); + } + + @Override + public ImageTransferVO findByVolume(Long volumeId) { + SearchCriteria sc = volumeSearch.create(); + sc.setParameters("volumeId", volumeId); + return findOneBy(sc); + } + + @Override + public ImageTransferVO findUnfinishedByVolume(Long volumeId) { + SearchCriteria sc = volumeUnfinishedSearch.create(); + sc.setParameters("volumeId", volumeId); + sc.setParameters("phase", ImageTransferVO.Phase.finished.toString()); + return findOneBy(sc); + } + + @Override + public List listByPhaseAndDirection(ImageTransfer.Phase phase, ImageTransfer.Direction direction) { + SearchCriteria sc = phaseDirectionSearch.create(); + sc.setParameters("phase", phase); + sc.setParameters("direction", direction); + return listBy(sc); + } + + @Override + public List listByZonesAndOwners(List zoneIds, List accountIds, List domainIds, + Filter filter) { + if (CollectionUtils.isEmpty(zoneIds)) { + return Collections.emptyList(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("dataCenterId", sb.entity().getDataCenterId(), SearchCriteria.Op.IN); + boolean accountIdsNotEmpty = CollectionUtils.isNotEmpty(accountIds); + boolean domainIdsNotEmpty = CollectionUtils.isNotEmpty(domainIds); + if (accountIdsNotEmpty || domainIdsNotEmpty) { + sb.and().op("account", sb.entity().getAccountId(), SearchCriteria.Op.IN); + sb.or("domain", sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.done(); + final SearchCriteria sc = sb.create(); + sc.setParameters("dataCenterId", zoneIds.toArray()); + if (accountIdsNotEmpty) { + sc.setParameters("account", accountIds.toArray()); + } + if (domainIdsNotEmpty) { + sc.setParameters("domain", domainIds.toArray()); + } + + return listBy(sc, filter); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java index 4735202a7623..82ba9445b25c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDao.java @@ -29,4 +29,6 @@ public interface SharedFSDao extends GenericDao, StateDao listSharedFSToBeDestroyed(Date date); SharedFSVO findSharedFSByNameAccountDomain(String name, Long accountId, Long domainId); + + SharedFSVO findByVm(long vmId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java index da6220716715..dd23787f982f 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/sharedfs/dao/SharedFSDaoImpl.java @@ -114,4 +114,14 @@ public SharedFSVO findSharedFSByNameAccountDomain(String name, Long accountId, L sc.setParameters("domainId", domainId); return findOneBy(sc); } + + @Override + public SharedFSVO findByVm(long vmId) { + SearchBuilder sb = createSearchBuilder(); + sb.and("vmId", sb.entity().getVmId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("vmId", vmId); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index ad3722577c27..26181d3fce0d 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -116,6 +116,7 @@ + @@ -271,6 +272,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index 858c46a7c1ee..fbb2fd079f9c 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -19,7 +19,6 @@ -- Schema upgrade from 4.21.0.0 to 4.22.0.0 --; - -- health check status as enum CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('router_health_check', 'check_result', 'check_result', 'varchar(16) NOT NULL COMMENT "check executions result: SUCCESS, FAILURE, WARNING, UNKNOWN"'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index dfa5fa8f3a14..bd5ecbab21ca 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -73,7 +73,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` ( `user_id` bigint(20) unsigned NOT NULL, `start_date` datetime, `end_date` datetime, - `description` varchar(100), + `description` varchar(1024), `api_key` varchar(255) NOT NULL, `secret_key` varchar(255) NOT NULL, `created` datetime NOT NULL, @@ -107,17 +107,25 @@ WHERE user.api_key IS NOT NULL AND user.secret_key IS NOT NULL; -- Drop API keys from user table ALTER TABLE `cloud`.`user` DROP COLUMN api_key, DROP COLUMN secret_key; --- Grant access to the "deleteUserKeys" API to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API +-- Grant access to the "deleteUserKeys" and "listUserKeyRules" APIs to the "User", "Domain Admin" and "Resource Admin" roles, similarly to the "registerUserKeys" API CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'deleteUserKeys', 'ALLOW'); CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'deleteUserKeys', 'ALLOW'); CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKeys', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('User', 'listUserKeyRules', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Domain Admin', 'listUserKeyRules', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'listUserKeyRules', 'ALLOW'); + -- Add conserve mode for VPC offerings CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' '); --- Disable/enable NICs CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' '); +--- Add URLs for OAuth provider +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','authorize_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Authorize URL for OAuth initialization'' '); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','token_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Token URL for OAuth finalization'' '); + --- Quota tariff/usage mapping CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, @@ -149,3 +157,65 @@ SELECT 'Advanced', 'DEFAULT', 'CapacityManager', 'kvm.cpu.dynamic.scaling.capaci FROM `cloud`.`configuration` `cfg` WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.cpu.dynamic.scaling.capacity') AND `cfg`.`name` = 'vm.serviceoffering.cpu.cores.max'; + +-- Remove stale realhostip.com default values; domain has been dead since ~2015. +UPDATE `cloud`.`configuration` + SET value = NULL + WHERE name IN ('consoleproxy.url.domain', 'secstorage.ssl.cert.domain') + AND value IN ('realhostip.com', '*.realhostip.com'); + +-- Add management_server_details table to allow ManagementServer scope configs +CREATE TABLE IF NOT EXISTS `management_server_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `management_server_id` bigint unsigned NOT NULL COMMENT 'management server the detail is related to', + `name` varchar(255) NOT NULL COMMENT 'name of the detail', + `value` varchar(255) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_management_server_details__management_server_id` FOREIGN KEY `fk_management_server_details__management_server_id`(`management_server_id`) REFERENCES `mshost`(`id`) ON DELETE CASCADE, + KEY `i_management_server_details__name__value` (`name`(128),`value`(128)) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Add checkpoint tracking fields to backups table for incremental backup support +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'from_checkpoint_id', 'VARCHAR(255) DEFAULT NULL COMMENT "Previous active checkpoint id for incremental backups"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'to_checkpoint_id', 'VARCHAR(255) DEFAULT NULL COMMENT "New checkpoint id created for the next incremental backup"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'checkpoint_create_time', 'BIGINT DEFAULT NULL COMMENT "Checkpoint creation timestamp from libvirt"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backups', 'host_id', 'BIGINT UNSIGNED DEFAULT NULL COMMENT "Host where backup is running"'); + +-- Create image_transfer table for per-disk image transfers +CREATE TABLE IF NOT EXISTS `cloud`.`image_transfer`( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'uuid', + `account_id` bigint unsigned NOT NULL COMMENT 'Account ID', + `domain_id` bigint unsigned NOT NULL COMMENT 'Domain ID', + `data_center_id` bigint unsigned NOT NULL COMMENT 'Data Center ID', + `backup_id` bigint unsigned COMMENT 'Backup ID', + `volume_id` bigint unsigned NOT NULL COMMENT 'Volume ID', + `host_id` bigint unsigned NOT NULL COMMENT 'Host ID', + `transfer_url` varchar(255) COMMENT 'ImageIO transfer URL', + `file` varchar(255) COMMENT 'File for the file backend', + `phase` varchar(20) NOT NULL COMMENT 'Transfer phase: initializing, transferring, finished, failed', + `socket` varchar(255) COMMENT 'Unix socket for nbd backend', + `direction` varchar(20) NOT NULL COMMENT 'Direction: upload, download', + `backend` varchar(20) NOT NULL COMMENT 'Backend: nbd, file', + `progress` int COMMENT 'Transfer progress percentage (0-100)', + `signed_ticket_id` varchar(255) COMMENT 'Signed ticket ID from ImageIO', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + UNIQUE KEY `uuid` (`uuid`), + CONSTRAINT `fk_image_transfer__backup_id` FOREIGN KEY (`backup_id`) REFERENCES `backups`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_image_transfer__volume_id` FOREIGN KEY (`volume_id`) REFERENCES `volumes`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_image_transfer__host_id` FOREIGN KEY (`host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + INDEX `i_image_transfer__backup_id`(`backup_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +--- Quota resource statement +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'quotaResourceStatement', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'quotaStatement' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaResourceStatement'); + +-- Add description for secondary IP addresses +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nic_secondary_ips', 'description', 'VARCHAR(2048) DEFAULT NULL'); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql index d9f4e2671595..255ce866c427 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.host_view.sql @@ -64,6 +64,7 @@ SELECT guest_os_category.id guest_os_category_id, guest_os_category.uuid guest_os_category_uuid, guest_os_category.name guest_os_category_name, + (SELECT `value` FROM `cloud`.`host_details` `hd` WHERE `hd`.`host_id` = `cloud`.`host`.`id` AND `hd`.`name` = 'guest.os.rule') AS `guest_os_rule`, mem_caps.used_capacity memory_used_capacity, mem_caps.reserved_capacity memory_reserved_capacity, cpu_caps.used_capacity cpu_used_capacity, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index c169cbce2170..d77ba27689be 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -56,6 +56,7 @@ SELECT `vm_instance`.`display_vm` AS `display_vm`, `vm_instance`.`delete_protection` AS `delete_protection`, `guest_os`.`uuid` AS `guest_os_uuid`, + `guest_os`.`display_name` AS `guest_os_display_name`, `vm_instance`.`pod_id` AS `pod_id`, `host_pod_ref`.`uuid` AS `pod_uuid`, `vm_instance`.`private_ip_address` AS `private_ip_address`, diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java index 9445efeb089c..6f153727ab76 100644 --- a/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VolumeDaoImplTest.java @@ -41,6 +41,7 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; @@ -113,6 +114,69 @@ public void testListPoolIdsByVolumeCount_without_cluster_details() throws SQLExc verify(preparedStatementMock, times(1)).executeQuery(); } + @Test + public void findByInstanceAndNotState_queriesWithInstanceIdAndExcludedStates() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + VolumeVO mockedVO = Mockito.mock(VolumeVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + + volumeDao.findByInstanceAndNotStates(42L, Volume.State.Ready); + + Mockito.verify(sc).setParameters("instanceId", 42L); + Mockito.verify(sc).setParameters("state", (Object[]) new Volume.State[]{Volume.State.Ready}); + } + + @Test + public void findByInstanceAndNotStates_withMultipleExcludedStates_passesAllStatesToCriteria() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + VolumeVO mockedVO = Mockito.mock(VolumeVO.class); + Mockito.when(sb.entity()).thenReturn(mockedVO); + + volumeDao.findByInstanceAndNotStates(7L, Volume.State.Destroy, Volume.State.Expunged); + + Mockito.verify(sc).setParameters("instanceId", 7L); + Mockito.verify(sc).setParameters("state", + (Object[]) new Volume.State[]{Volume.State.Destroy, Volume.State.Expunged}); + } + + @Test + public void findByInstanceAndNotStates_returnsResultFromDao() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + VolumeVO vol = Mockito.mock(VolumeVO.class); + Mockito.doReturn(List.of(vol)).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(VolumeVO.class)); + + List result = volumeDao.findByInstanceAndNotStates(1L, Volume.State.Ready); + + Assert.assertEquals(1, result.size()); + Assert.assertSame(vol, result.get(0)); + } + + @Test + public void findByInstanceAndNotStates_noMatchingVolumes_returnsEmptyList() { + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.doReturn(new ArrayList<>()).when(volumeDao).listBy(sc); + Mockito.when(volumeDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(VolumeVO.class)); + + List result = volumeDao.findByInstanceAndNotStates(99L, Volume.State.Ready); + + Assert.assertTrue(result.isEmpty()); + } + @Test public void testSearchRemovedByVmsNoVms() { Assert.assertTrue(CollectionUtils.isEmpty(volumeDao.searchRemovedByVms( @@ -141,5 +205,4 @@ public void testSearchRemovedByVms() { Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), Mockito.eq(null), Mockito.eq(false)); } - } diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java index 8145158dfa40..95362f44b138 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategy.java @@ -27,6 +27,7 @@ import javax.inject.Inject; import com.cloud.agent.api.to.DiskTO; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -75,6 +76,7 @@ import com.cloud.storage.Snapshot.Type; import com.cloud.storage.SnapshotVO; import com.cloud.storage.StorageManager; +import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StoragePool; import com.cloud.storage.VolumeVO; @@ -108,6 +110,8 @@ public class AncientDataMotionStrategy implements DataMotionStrategy { StorageCacheManager cacheMgr; @Inject VolumeDataStoreDao volumeDataStoreDao; + @Inject + ClvmPoolManager clvmPoolManager; @Inject StorageManager storageManager; @@ -309,6 +313,8 @@ protected Answer copyVolumeFromSnapshot(DataObject snapObj, DataObject volObj) { ep = selector.select(srcData, volObj); } + updateLockHostForVolume(ep, volObj); + CopyCommand cmd = new CopyCommand(srcData.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volObj.getTO()), _createVolumeFromSnapshotWait, VirtualMachineManager.ExecuteInSequence.value()); Answer answer = null; @@ -331,6 +337,29 @@ protected Answer copyVolumeFromSnapshot(DataObject snapObj, DataObject volObj) { } } + private void updateLockHostForVolume(EndPoint ep, DataObject volObj) { + if (ep == null || !(volObj instanceof VolumeInfo)) { + return; + } + VolumeInfo volumeInfo = (VolumeInfo) volObj; + StoragePool destPool = (StoragePool) volObj.getDataStore(); + if (destPool == null || !ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) { + return; + } + Long hostId = ep.getId(); + Long existingHostId = clvmPoolManager.getClvmLockHostId( + volumeInfo.getId(), + volumeInfo.getUuid(), + volumeInfo.getPath(), + destPool, + true + ); + if (existingHostId == null) { + clvmPoolManager.setClvmLockHostId(volumeInfo.getId(), hostId); + logger.debug("Set lock host ID {} for CLVM volume {} being created from snapshot", hostId, volumeInfo.getId()); + } + } + protected Answer cloneVolume(DataObject template, DataObject volume) { CopyCommand cmd = new CopyCommand(template.getTO(), addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(volume.getTO()), 0, VirtualMachineManager.ExecuteInSequence.value()); try { @@ -581,6 +610,9 @@ protected Answer migrateVolumeToPool(DataObject srcData, DataObject destData) { volumeVo.setPoolId(destPool.getId()); volumeVo.setPoolType(destPool.getPoolType()); volumeVo.setLastPoolId(oldPoolId); + if (destPool.getPoolType() == StoragePoolType.CLVM) { + volumeVo.setFormat(ImageFormat.RAW); + } // For SMB, pool credentials are also stored in the uri query string. We trim the query string // part here to make sure the credentials do not get stored in the db unencrypted. String folder = destPool.getPath(); diff --git a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java index 947b4af8f690..867470dac040 100644 --- a/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java +++ b/engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java @@ -144,12 +144,16 @@ protected boolean isDestinationNfsPrimaryStorageClusterWide(Map volumeDataStoreMap, VirtualMach String errMsg = null; boolean success = false; Map srcVolumeInfoToDestVolumeInfo = new HashMap<>(); + List samePoolClvmVolumes = new ArrayList<>(); try { if (srcHost.getHypervisorType() != HypervisorType.KVM) { @@ -2052,6 +2058,13 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach continue; } + if (sourceStoragePool.getId() == destStoragePool.getId() && + ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + logger.info("Same-pool CLVM migration for volume [{}]: skipping data copy.", srcVolumeInfo.getUuid()); + samePoolClvmVolumes.add(srcVolumeInfo); + continue; + } + if (!shouldMigrateVolume(sourceStoragePool, destHost, destStoragePool)) { continue; } @@ -2071,6 +2084,13 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach setVolumeMigrationOptions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost, destStoragePool, migrationType); + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + destVolumeInfo.setDestinationHostId(destHost.getId()); + clvmPoolManager.setClvmLockHostId(destVolume.getId(), destHost.getId()); + logger.info("Set CLVM lock host {} for volume {} during migration to ensure creation on destination host", + destHost.getId(), destVolumeInfo.getUuid()); + } + // create a volume on the destination storage destDataStore.getDriver().createAsync(destDataStore, destVolumeInfo, null); @@ -2096,7 +2116,7 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach MigrateCommand.MigrateDiskInfo migrateDiskInfo; - boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; + boolean isNonManagedToNfs = supportStoragePoolType(sourceStoragePool.getPoolType(), StoragePoolType.Filesystem, StoragePoolType.CLVM, StoragePoolType.CLVM_NG) && destStoragePool.getPoolType() == StoragePoolType.NetworkFilesystem && !managedStorageDestination; if (isNonManagedToNfs) { migrateDiskInfo = new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, @@ -2106,9 +2126,12 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } else { String backingPath = generateBackingPath(destStoragePool, destVolumeInfo); migrateDiskInfo = configureMigrateDiskInfo(srcVolumeInfo, destPath, backingPath); + migrateDiskInfo = updateMigrateDiskInfoForBlockDevice(migrateDiskInfo, destStoragePool); migrateDiskInfo.setSourceDiskOnStorageFileSystem(isStoragePoolTypeOfFile(sourceStoragePool)); migrateDiskInfoList.add(migrateDiskInfo); } + migrateDiskInfo.setSourcePoolType(sourceStoragePool.getPoolType()); + migrateDiskInfo.setDestPoolType(destVolumeInfo.getStoragePoolType()); prepareDiskWithSecretConsumerDetail(vmTO, srcVolumeInfo, destVolumeInfo.getPath()); migrateStorage.put(srcVolumeInfo.getPath(), migrateDiskInfo); @@ -2116,6 +2139,8 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo); } + prepareDisksForMigrationForClvm(vmTO, volumeDataStoreMap, srcHost); + PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); Answer pfma; @@ -2132,6 +2157,25 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach throw new AgentUnavailableException("Operation timed out", destHost.getId()); } + for (VolumeInfo vol : samePoolClvmVolumes) { + StoragePoolVO samePoolClvmPool = _storagePoolDao.findById(vol.getPoolId()); + String vgName = samePoolClvmPool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, vol.getPath()); + logger.info("Activating CLVM volume [{}] in shared mode on dest host [{}] for same-pool migration.", + vol.getUuid(), destHost.getId()); + Answer activateAnswer = agentManager.send(destHost.getId(), + new ClvmLockTransferCommand(ClvmLockTransferCommand.Operation.ACTIVATE_SHARED, lvPath, vol.getUuid())); + if (activateAnswer == null || !activateAnswer.getResult()) { + throw new CloudRuntimeException(String.format( + "Failed to activate CLVM volume [%s] in shared mode on dest host [%s]: %s", + vol.getUuid(), destHost.getId(), + activateAnswer != null ? activateAnswer.getDetails() : "null answer")); + } + } + VMInstanceVO vm = _vmDao.findById(vmTO.getId()); boolean isWindows = _guestOsCategoryDao.findById(_guestOsDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); @@ -2141,6 +2185,9 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach migrateCommand.setMigrateDiskInfoList(migrateDiskInfoList); migrateCommand.setMigrateStorageManaged(managedStorageDestination); migrateCommand.setMigrateNonSharedInc(migrateNonSharedInc); + boolean hasClvmCrossPoolVolume = migrateStorage.values().stream() + .anyMatch(info -> ClvmPoolManager.isClvmPoolType(info.getSourcePoolType())); + migrateCommand.setClvmCrossPoolMigration(hasClvmCrossPoolVolume); Integer newVmCpuShares = ((PrepareForMigrationAnswer) pfma).getNewVmCpuShares(); if (newVmCpuShares != null) { @@ -2171,7 +2218,7 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } } - handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost); + handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, srcHost, destHost); if (!success) { if (migrateAnswer == null) { @@ -2211,10 +2258,43 @@ public void copyAsync(Map volumeDataStoreMap, VirtualMach } } + private void prepareDisksForMigrationForClvm(VirtualMachineTO vmTO, Map volumeDataStoreMap, Host srcHost) { + // For CLVM/CLVM_NG source pools, convert volumes from exclusive to shared mode + // on the source host BEFORE PrepareForMigrationCommand on the destination. + boolean hasClvmSource = volumeDataStoreMap.keySet().stream() + .map(v -> _storagePoolDao.findById(v.getPoolId())) + .anyMatch(p -> p != null && (p.getPoolType() == StoragePoolType.CLVM || p.getPoolType() == StoragePoolType.CLVM_NG)); + if (hasClvmSource && srcHost.getHypervisorType() == HypervisorType.KVM) { + logger.info("CLVM/CLVM_NG source pool detected for VM [{}], sending PreMigrationCommand to source host [{}] to convert volumes to shared mode.", vmTO.getName(), srcHost.getId()); + PreMigrationCommand preMigCmd = new PreMigrationCommand(vmTO, vmTO.getName()); + try { + Answer preMigAnswer = agentManager.send(srcHost.getId(), preMigCmd); + if (preMigAnswer == null || !preMigAnswer.getResult()) { + String details = preMigAnswer != null ? preMigAnswer.getDetails() : "null answer returned"; + logger.warn("PreMigrationCommand failed for CLVM/CLVM_NG VM [{}] on source host [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", vmTO.getName(), srcHost.getId(), details); + } else { + logger.info("Successfully converted CLVM/CLVM_NG volumes to shared mode on source host [{}] for VM [{}].", srcHost.getId(), vmTO.getName()); + } + } catch (Exception e) { + logger.warn("Failed to send PreMigrationCommand to source host [{}] for VM [{}]: {}. Migration will continue but may fail if volumes are exclusively locked.", srcHost.getId(), vmTO.getName(), e.getMessage()); + } + } else if (hasClvmSource) { + logger.debug("Skipping PreMigrationCommand for non-KVM hypervisor type: {} on host [{}]", srcHost.getHypervisorType(), srcHost.getId()); + } + } + private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host destHost, VMInstanceVO vmInstance, VolumeInfo srcVolumeInfo, StoragePoolVO sourceStoragePool, StoragePoolVO destStoragePool, DataStore destDataStore) { VMTemplateVO vmTemplate = _vmTemplateDao.findById(vmInstance.getTemplateId()); String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo); + + // Check if source is CLVM/CLVM_NG (block device storage) + // LinkedClone (VIR_MIGRATE_NON_SHARED_INC) only works for file → file migrations + // For block device sources, use FullClone (VIR_MIGRATE_NON_SHARED_DISK) + boolean sourceIsBlockDevice = sourceStoragePool.getPoolType() == StoragePoolType.CLVM || + sourceStoragePool.getPoolType() == StoragePoolType.CLVM_NG; + if (StringUtils.isNotBlank(srcVolumeBackingFile) && supportStoragePoolType(destStoragePool.getPoolType(), StoragePoolType.Filesystem) && + !sourceIsBlockDevice && srcVolumeInfo.getTemplateId() != null && Objects.nonNull(vmTemplate) && !Arrays.asList(KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME).contains(vmTemplate.getName())) { @@ -2222,8 +2302,12 @@ private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host de copyTemplateToTargetFilesystemStorageIfNeeded(srcVolumeInfo, sourceStoragePool, destDataStore, destStoragePool, destHost); return MigrationOptions.Type.LinkedClone; } - logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a " + - "template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + + if (sourceIsBlockDevice) { + logger.debug(String.format("Source storage pool [%s] is block device (CLVM/CLVM_NG). Using FullClone migration for volume [%s] to target storage pool [%s]. Template copy skipped as entire volume will be copied.", sourceStoragePool.getId(), srcVolumeInfo.getId(), destStoragePool.getId())); + } else { + logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId())); + } return MigrationOptions.Type.FullClone; } @@ -2289,6 +2373,39 @@ protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo src MigrateCommand.MigrateDiskInfo.Source.DEV, destPath, backingPath); } + /** + * UpdatesMigrateDiskInfo for CLVM/CLVM_NG block devices by returning a new instance with corrected disk type, driver type, and source. + * For CLVM/CLVM_NG destinations, returns a new MigrateDiskInfo with BLOCK disk type, DEV source, and appropriate driver type (QCOW2 for CLVM_NG, RAW for CLVM). + * For other storage types, returns the original MigrateDiskInfo unchanged. + * + * @param migrateDiskInfo The original MigrateDiskInfo object + * @param destStoragePool The destination storage pool + * @return A new MigrateDiskInfo with updated values for CLVM/CLVM_NG, or the original for other storage types + */ + protected MigrateCommand.MigrateDiskInfo updateMigrateDiskInfoForBlockDevice(MigrateCommand.MigrateDiskInfo migrateDiskInfo, + StoragePoolVO destStoragePool) { + if (ClvmPoolManager.isClvmPoolType(destStoragePool.getPoolType())) { + + MigrateCommand.MigrateDiskInfo.DriverType driverType = + (destStoragePool.getPoolType() == StoragePoolType.CLVM_NG) ? + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2 : + MigrateCommand.MigrateDiskInfo.DriverType.RAW; + + logger.debug("Updating MigrateDiskInfo for {} destination: setting BLOCK disk type, DEV source, and {} driver type", + destStoragePool.getPoolType(), driverType); + + return new MigrateCommand.MigrateDiskInfo( + migrateDiskInfo.getSerialNumber(), + MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, + driverType, + MigrateCommand.MigrateDiskInfo.Source.DEV, + migrateDiskInfo.getSourceText(), + migrateDiskInfo.getBackingStoreText()); + } + + return migrateDiskInfo; + } + /** * Sets the volume path as the iScsi name in case of a configured iScsi. */ @@ -2320,7 +2437,26 @@ String getVolumeBackingFile(VolumeInfo srcVolumeInfo) { return null; } - private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host destHost) { + private void sendClvmLockCommand(long hostId, StoragePoolVO pool, VolumeInfo volumeInfo, + ClvmLockTransferCommand.Operation operation) { + String vgName = pool.getPath(); + if (vgName.startsWith("/")) { + vgName = vgName.substring(1); + } + String lvPath = String.format("/dev/%s/%s", vgName, volumeInfo.getPath()); + try { + Answer answer = agentManager.send(hostId, + new ClvmLockTransferCommand(operation, lvPath, volumeInfo.getUuid())); + if (answer == null || !answer.getResult()) { + String details = answer != null ? answer.getDetails() : "null answer"; + logger.warn("CLVM lock command [{}] failed for LV [{}] on host [{}]: {}", operation, lvPath, hostId, details); + } + } catch (AgentUnavailableException | OperationTimedoutException e) { + logger.warn("Exception sending CLVM lock command [{}] for LV [{}] on host [{}]: {}", operation, lvPath, hostId, e.getMessage()); + } + } + + private void handlePostMigration(boolean success, Map srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host srcHost, Host destHost) { if (!success) { try { PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO); @@ -2339,6 +2475,17 @@ private void handlePostMigration(boolean success, Map sr catch (Exception e) { logger.debug("Failed to disconnect one or more (original) dest volumes", e); } + + if (srcHost != null && srcHost.getHypervisorType() == HypervisorType.KVM) { + for (VolumeInfo srcVolumeInfo : srcVolumeInfoToDestVolumeInfo.keySet()) { + StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + if (srcPool == null || !ClvmPoolManager.isClvmPoolType(srcPool.getPoolType())) { + continue; + } + sendClvmLockCommand(srcHost.getId(), srcPool, srcVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + } + } } for (Map.Entry entry : srcVolumeInfoToDestVolumeInfo.entrySet()) { @@ -2349,12 +2496,13 @@ private void handlePostMigration(boolean success, Map sr if (success) { VolumeVO volumeVO = _volumeDao.findById(destVolumeInfo.getId()); - volumeVO.setFormat(ImageFormat.QCOW2); + StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); + StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); + volumeVO.setFormat(destPoolVO != null && destPoolVO.getPoolType() == StoragePoolType.CLVM + ? ImageFormat.RAW : ImageFormat.QCOW2); volumeVO.setLastId(srcVolumeInfo.getId()); if (Objects.equals(srcVolumeInfo.getDiskOfferingId(), destVolumeInfo.getDiskOfferingId())) { - StoragePoolVO srcPoolVO = _storagePoolDao.findById(srcVolumeInfo.getPoolId()); - StoragePoolVO destPoolVO = _storagePoolDao.findById(destVolumeInfo.getPoolId()); if (srcPoolVO != null && destPoolVO != null && ((srcPoolVO.isShared() && destPoolVO.isLocal()) || (srcPoolVO.isLocal() && destPoolVO.isShared()))) { Long offeringId = getSuitableDiskOfferingForVolumeOnPool(volumeVO, destPoolVO); @@ -2365,6 +2513,12 @@ private void handlePostMigration(boolean success, Map sr } _volumeDao.update(volumeVO.getId(), volumeVO); + if (destPoolVO != null && ClvmPoolManager.isClvmPoolType(destPoolVO.getPoolType()) + && (srcPoolVO == null || srcPoolVO.getId() != destPoolVO.getId())) { + sendClvmLockCommand(destHost.getId(), destPoolVO, destVolumeInfo, + ClvmLockTransferCommand.Operation.ACTIVATE_EXCLUSIVE); + clvmPoolManager.setClvmLockHostId(destVolumeInfo.getId(), destHost.getId()); + } _volumeService.copyPoliciesBetweenVolumesAndDestroySourceVolumeAfterMigration(Event.OperationSucceeded, null, srcVolumeInfo, destVolumeInfo, false); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java index e167cc0a9653..e84163656b10 100755 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/AncientDataMotionStrategyTest.java @@ -28,14 +28,18 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.any; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StorageCacheManager; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -95,6 +99,14 @@ private void overrideDefaultConfigValue(final ConfigKey configKey, final String f.set(configKey, value); } + private ClvmPoolManager injectMockedClvmPoolManager() throws Exception { + ClvmPoolManager clvmPoolManager = Mockito.mock(ClvmPoolManager.class); + Field clvmPoolManagerField = AncientDataMotionStrategy.class.getDeclaredField("clvmPoolManager"); + clvmPoolManagerField.setAccessible(true); + clvmPoolManagerField.set(strategy, clvmPoolManager); + return clvmPoolManager; + } + @Test public void testAddFullCloneFlagOnVMwareDest(){ strategy.addFullCloneAndDiskprovisiongStrictnessFlagOnVMwareDest(dataTO); @@ -288,4 +300,185 @@ public void testCanBypassSecondaryStorageWithClusterWideNFSAndZoneWideNFSPoolsIn canBypassSecondaryStorage = (boolean) method.invoke(strategy, destVolumeInfo, srcVolumeInfo); Assert.assertTrue(canBypassSecondaryStorage); } + + @Test + public void testUpdateLockHostForVolume_CLVMPool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 123L; + Long volumeId = 456L; + String volumeUuid = "test-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("test-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_CLVM_NG_Pool_SetsLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 789L; + Long volumeId = 101L; + String volumeUuid = "test-clvm-ng-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("test-clvm-ng-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM_NG); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(null); + + try { + method.invoke(strategy, endPoint, volumeInfo); + } catch (InvocationTargetException e) { + e.getCause().printStackTrace(); + throw e; + } + + Mockito.verify(clvmPoolManager).setClvmLockHostId(volumeId, hostId); + } + + @Test + public void testUpdateLockHostForVolume_NonCLVMPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + // Create mock that implements both DataStore and StoragePool interfaces + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_ExistingLockHost_DoesNotOverwrite() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + DataStore dataStore = Mockito.mock(DataStore.class, Mockito.withSettings().extraInterfaces(StoragePool.class)); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + Long hostId = 555L; + Long existingHostId = 666L; + Long volumeId = 777L; + String volumeUuid = "existing-lock-volume-uuid"; + + Mockito.when(endPoint.getId()).thenReturn(hostId); + Mockito.when(volumeInfo.getDataStore()).thenReturn(dataStore); + Mockito.when(volumeInfo.getId()).thenReturn(volumeId); + Mockito.when(volumeInfo.getUuid()).thenReturn(volumeUuid); + Mockito.when(volumeInfo.getPath()).thenReturn("existing-lock-volume-path"); + Mockito.when(((StoragePool) dataStore).getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(clvmPoolManager.getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true))).thenReturn(existingHostId); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager).getClvmLockHostId(Mockito.eq(volumeId), Mockito.eq(volumeUuid), + Mockito.anyString(), Mockito.any(StoragePool.class), Mockito.eq(true)); + } + + @Test + public void testUpdateLockHostForVolume_NullEndPoint_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, null, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NonVolumeDataObject_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + SnapshotInfo snapshotInfo = Mockito.mock(SnapshotInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, snapshotInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } + + @Test + public void testUpdateLockHostForVolume_NullPool_DoesNotSetLockHost() throws Exception { + Method method = AncientDataMotionStrategy.class.getDeclaredMethod( + "updateLockHostForVolume", + EndPoint.class, + DataObject.class); + method.setAccessible(true); + + EndPoint endPoint = Mockito.mock(EndPoint.class); + VolumeInfo volumeInfo = Mockito.mock(VolumeInfo.class); + ClvmPoolManager clvmPoolManager = injectMockedClvmPoolManager(); + + method.invoke(strategy, endPoint, volumeInfo); + + Mockito.verify(clvmPoolManager, never()).setClvmLockHostId(any(Long.class), any(Long.class)); + Mockito.verify(clvmPoolManager, never()).getClvmLockHostId(any(Long.class), any(String.class), + any(String.class), any(StoragePool.class), Mockito.anyBoolean()); + } } diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java index 808c319b40f2..6f0776b27c8c 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageSystemDataMotionTest.java @@ -168,6 +168,8 @@ public Boolean supportStoragePoolType(StoragePoolType storagePoolType) { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); return supportedTypes.contains(storagePoolType); } @@ -505,6 +507,8 @@ public void validateSupportStoragePoolType() { supportedTypes.add(StoragePoolType.Filesystem); supportedTypes.add(StoragePoolType.NetworkFilesystem); supportedTypes.add(StoragePoolType.SharedMountPoint); + supportedTypes.add(StoragePoolType.CLVM); + supportedTypes.add(StoragePoolType.CLVM_NG); for (StoragePoolType poolType : StoragePoolType.values()) { boolean isSupported = kvmNonManagedStorageDataMotionStrategy.supportStoragePoolType(poolType); diff --git a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java index 45357fa64b2a..fc51eeba7b6e 100644 --- a/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java +++ b/engine/storage/datamotion/src/test/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategyTest.java @@ -369,4 +369,286 @@ public void validateIsStoragePoolTypeInListReturnsFalse() { assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.SharedMountPoint, listTypes)); } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM destination pool + * Should set driver type to RAW for CLVM + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial123", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial123", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with CLVM_NG destination pool + * Should set driver type to QCOW2 for CLVM_NG + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_ClvmNgDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial456", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.RAW, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + "/backing/path" + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.CLVM_NG); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, updatedDiskInfo.getSource()); + Assert.assertEquals("serial456", updatedDiskInfo.getSerialNumber()); + Assert.assertEquals("/source/path", updatedDiskInfo.getSourceText()); + Assert.assertEquals("/backing/path", updatedDiskInfo.getBackingStoreText()); + } + + /** + * Test updateMigrateDiskInfoForBlockDevice with non-CLVM destination pool + * Should return original DiskInfo unchanged + */ + @Test + public void testUpdateMigrateDiskInfoForBlockDevice_NonClvmDestination() { + MigrateCommand.MigrateDiskInfo originalDiskInfo = new MigrateCommand.MigrateDiskInfo( + "serial789", + MigrateCommand.MigrateDiskInfo.DiskType.FILE, + MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, + MigrateCommand.MigrateDiskInfo.Source.FILE, + "/source/path", + null + ); + + StoragePoolVO destStoragePool = new StoragePoolVO(); + destStoragePool.setPoolType(StoragePoolType.NetworkFilesystem); + + MigrateCommand.MigrateDiskInfo updatedDiskInfo = strategy.updateMigrateDiskInfoForBlockDevice( + originalDiskInfo, destStoragePool); + + Assert.assertSame(originalDiskInfo, updatedDiskInfo); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.FILE, updatedDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.QCOW2, updatedDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.FILE, updatedDiskInfo.getSource()); + } + + /** + * Test supportStoragePoolType with CLVM and CLVM_NG types + */ + @Test + public void testSupportStoragePoolType_ClvmTypes() { + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM)); + assertFalse(strategy.supportStoragePoolType(StoragePoolType.CLVM_NG)); + } + + /** + * Test configureMigrateDiskInfo with CLVM destination + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvm() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", null); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test configureMigrateDiskInfo with CLVM_NG destination and backing file + */ + @Test + public void testConfigureMigrateDiskInfo_ForClvmNgWithBacking() { + VolumeObject srcVolumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn("/dev/vg/volume-path").when(srcVolumeInfo).getPath(); + + MigrateCommand.MigrateDiskInfo migrateDiskInfo = strategy.configureMigrateDiskInfo( + srcVolumeInfo, "/dev/vg/dest-path", "/dev/vg/backing-template"); + + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DiskType.BLOCK, migrateDiskInfo.getDiskType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.DriverType.RAW, migrateDiskInfo.getDriverType()); + Assert.assertEquals(MigrateCommand.MigrateDiskInfo.Source.DEV, migrateDiskInfo.getSource()); + Assert.assertEquals("/dev/vg/dest-path", migrateDiskInfo.getSourceText()); + Assert.assertEquals("/dev/vg/backing-template", migrateDiskInfo.getBackingStoreText()); + Assert.assertEquals("/dev/vg/volume-path", migrateDiskInfo.getSerialNumber()); + } + + /** + * Test isStoragePoolTypeInList with CLVM types + */ + @Test + public void testIsStoragePoolTypeInList_WithClvmTypes() { + StoragePoolType[] clvmTypes = new StoragePoolType[] { + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.Filesystem + }; + + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.CLVM_NG, clvmTypes)); + assertTrue(strategy.isStoragePoolTypeInList(StoragePoolType.Filesystem, clvmTypes)); + assertFalse(strategy.isStoragePoolTypeInList(StoragePoolType.NetworkFilesystem, clvmTypes)); + } + + /** + * Test supportStoragePoolType with mixed CLVM and NFS types + */ + @Test + public void testSupportStoragePoolType_MixedClvmAndNfs() { + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.CLVM_NG, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG, + StoragePoolType.NetworkFilesystem + )); + + assertTrue(strategy.supportStoragePoolType( + StoragePoolType.NetworkFilesystem, + StoragePoolType.CLVM, + StoragePoolType.CLVM_NG + )); + } + + /** + * Test internalCanHandle with CLVM source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG source and managed destination + */ + @Test + public void testInternalCanHandle_ClvmNgSourceManagedDestination() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with both CLVM source and CLVM_NG destination + */ + @Test + public void testInternalCanHandle_ClvmToClvmNg() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } + + /** + * Test internalCanHandle with CLVM_NG to CLVM migration + */ + @Test + public void testInternalCanHandle_ClvmNgToClvm() { + VolumeObject volumeInfo = Mockito.spy(new VolumeObject()); + Mockito.doReturn(0L).when(volumeInfo).getPoolId(); + + DataStore ds = Mockito.spy(new PrimaryDataStoreImpl()); + + Map volumeMap = new HashMap<>(); + volumeMap.put(volumeInfo, ds); + + StoragePoolVO sourcePool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM_NG).when(sourcePool).getPoolType(); + Mockito.doReturn(true).when(sourcePool).isManaged(); + + StoragePoolVO destPool = Mockito.spy(new StoragePoolVO()); + Mockito.lenient().doReturn(StoragePoolType.CLVM).when(destPool).getPoolType(); + + Mockito.doReturn(sourcePool).when(primaryDataStoreDao).findById(0L); + + StrategyPriority result = strategy.internalCanHandle( + volumeMap, new HostVO("srcHostUuid"), new HostVO("destHostUuid")); + + Assert.assertEquals(StrategyPriority.HIGHEST, result); + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index 88f479c09045..d85e862bfb67 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -60,6 +60,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.CreateSnapshotPayload; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Snapshot; @@ -643,6 +644,10 @@ public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperat return StrategyPriority.DEFAULT; } + if (isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)) { + return StrategyPriority.DEFAULT; + } + return StrategyPriority.CANT_HANDLE; } if (zoneId != null && SnapshotOperation.DELETE.equals(op)) { @@ -691,4 +696,32 @@ protected boolean isSnapshotStoredOnSameZoneStoreForQCOW2Volume(Snapshot snapsho dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); } + /** + * Checks if a CLVM volume snapshot is stored on secondary storage in the same zone. + * CLVM snapshots are backed up to secondary storage and removed from primary storage. + */ + protected boolean isSnapshotStoredOnSecondaryForCLVMVolume(Snapshot snapshot, VolumeVO volumeVO) { + if (volumeVO == null) { + return false; + } + + Long poolId = volumeVO.getPoolId(); + if (poolId == null) { + return false; + } + + StoragePool pool = (StoragePool) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return false; + } + + List snapshotStores = snapshotStoreDao.listReadyBySnapshot(snapshot.getId(), DataStoreRole.Image); + if (CollectionUtils.isEmpty(snapshotStores)) { + return false; + } + + return snapshotStores.stream().anyMatch(s -> Objects.equals( + dataStoreMgr.getStoreZoneId(s.getDataStoreId(), s.getRole()), volumeVO.getDataCenterId())); + } + } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index b71d6cf3afac..665c3a4659ca 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -27,6 +27,7 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Snapshot; +import com.cloud.storage.Storage; import com.cloud.storage.dao.SnapshotDao; import com.cloud.vm.snapshot.VMSnapshotDetailsVO; import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; @@ -468,6 +469,13 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { @Override public StrategyPriority canHandle(VMSnapshot vmSnapshot) { + UserVmVO vm = userVmDao.findById(vmSnapshot.getVmId()); + String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm); + + if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + return StrategyPriority.DEFAULT; } @@ -493,10 +501,31 @@ public boolean deleteVMSnapshotFromDB(VMSnapshot vmSnapshot, boolean unmanage) { return vmSnapshotDao.remove(vmSnapshot.getId()); } + protected boolean isRunningVMVolumeOnCLVMStorage(UserVmVO vm, String cantHandleLog) { + Long vmId = vm.getId(); + if (State.Running.equals(vm.getState())) { + List volumes = volumeDao.findByInstance(vmId); + for (VolumeVO volume : volumes) { + StoragePool pool = primaryDataStoreDao.findById(volume.getPoolId()); + if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + logger.warn("Rejecting VM snapshot request: {} - VM is running on CLVM storage (pool: {}, poolType: CLVM)", + cantHandleLog, pool.getName()); + return true; + } + } + } + return false; + } + @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { UserVmVO vm = userVmDao.findById(vmId); String cantHandleLog = String.format("Default VM snapshot cannot handle VM snapshot for [%s]", vm); + + if (isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if (State.Running.equals(vm.getState()) && !snapshotMemory) { logger.debug("{} as it is running and its memory will not be affected.", cantHandleLog, vm); return StrategyPriority.CANT_HANDLE; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index 31b13fc279e3..4ae6e26fbd96 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -345,6 +345,13 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { } } + Long vmId = vmSnapshot.getVmId(); + UserVmVO vm = userVmDao.findById(vmId); + String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm); + if (vm != null && isRunningVMVolumeOnCLVMStorage(vm, cantHandleLog)) { + return StrategyPriority.CANT_HANDLE; + } + if ( SnapshotManager.VmStorageSnapshotKvm.value() && userVm.getHypervisorType() == Hypervisor.HypervisorType.KVM && vmSnapshot.getType() == VMSnapshot.Type.Disk) { return StrategyPriority.HYPERVISOR; diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java index 41bfaa6f0c77..c27d4e13fa4d 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java @@ -21,6 +21,7 @@ import java.util.List; import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.StoragePool; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; @@ -322,4 +323,236 @@ public void testIsSnapshotStoredOnSameZoneStoreForQCOW2VolumeHasRef() { prepareMocksForIsSnapshotStoredOnSameZoneStoreForQCOW2VolumeTest(100L); Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSameZoneStoreForQCOW2Volume(snapshot, volumeVO)); } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullVolume() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, null)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullPoolId() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NullPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_NonCLVMPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_RBDPool() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolNoSnapshotStores() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)).thenReturn(new ArrayList<>()); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInDifferentZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(112L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolSnapshotInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsOneMatches() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore2.getDataStoreId()).thenReturn(202L); + Mockito.when(snapshotStore2.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore3 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2, snapshotStore3)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(111L); + Mockito.when(dataStoreManager.getStoreZoneId(202L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolNullZoneIds() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(null); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolVolumeNullDataCenter() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(1L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertFalse(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } + + @Test + public void testIsSnapshotStoredOnSecondaryForCLVMVolume_CLVMPoolMultipleSnapshotsAllInSameZone() { + Snapshot snapshot = Mockito.mock(Snapshot.class); + Mockito.when(snapshot.getId()).thenReturn(1L); + + VolumeVO volumeVO = Mockito.mock(VolumeVO.class); + Mockito.when(volumeVO.getPoolId()).thenReturn(10L); + Mockito.when(volumeVO.getDataCenterId()).thenReturn(100L); + + StoragePool pool = Mockito.mock(StoragePool.class, Mockito.withSettings().extraInterfaces(DataStore.class)); + Mockito.when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.CLVM); + Mockito.when(dataStoreManager.getDataStore(10L, DataStoreRole.Primary)).thenReturn((DataStore) pool); + + SnapshotDataStoreVO snapshotStore1 = Mockito.mock(SnapshotDataStoreVO.class); + Mockito.when(snapshotStore1.getDataStoreId()).thenReturn(201L); + Mockito.when(snapshotStore1.getRole()).thenReturn(DataStoreRole.Image); + + SnapshotDataStoreVO snapshotStore2 = Mockito.mock(SnapshotDataStoreVO.class); + + Mockito.when(snapshotDataStoreDao.listReadyBySnapshot(1L, DataStoreRole.Image)) + .thenReturn(List.of(snapshotStore1, snapshotStore2)); + + Mockito.when(dataStoreManager.getStoreZoneId(201L, DataStoreRole.Image)).thenReturn(100L); + + Assert.assertTrue(defaultSnapshotStrategySpy.isSnapshotStoredOnSecondaryForCLVMVolume(snapshot, volumeVO)); + } } diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java index da377f96ec32..365ba3d4eb31 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategyTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.UUID; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -39,6 +40,10 @@ import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.VolumeDao; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshot; @RunWith(MockitoJUnitRunner.class) public class DefaultVMSnapshotStrategyTest { @@ -46,6 +51,8 @@ public class DefaultVMSnapshotStrategyTest { VolumeDao volumeDao; @Mock PrimaryDataStoreDao primaryDataStoreDao; + @Mock + UserVmDao userVmDao; @Spy @InjectMocks @@ -85,7 +92,7 @@ public void testUpdateVolumePath() { Mockito.when(vol2.getChainInfo()).thenReturn(newVolChain); Mockito.when(vol2.getSize()).thenReturn(vmSnapshotChainSize); Mockito.when(vol2.getId()).thenReturn(volumeId); - VolumeVO volumeVO = new VolumeVO("name", 0l, 0l, 0l, 0l, 0l, "folder", "path", Storage.ProvisioningType.THIN, 0l, Volume.Type.ROOT); + VolumeVO volumeVO = new VolumeVO("name", 0L, 0L, 0L, 0L, 0L, "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); volumeVO.setPoolId(oldPoolId); volumeVO.setChainInfo(oldVolChain); volumeVO.setPath(oldVolPath); @@ -103,4 +110,110 @@ public void testUpdateVolumePath() { Assert.assertEquals(vmSnapshotChainSize, persistedVolume.getVmSnapshotChainSize()); Assert.assertEquals(newVolChain, persistedVolume.getChainInfo()); } + + @Test + public void testCanHandleRunningVMOnClvmStorageCantHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + List volumes = List.of(volumeOnClvm); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE for running VM on CLVM storage", + StrategyPriority.CANT_HANDLE, result); + } + + @Test + public void testCanHandleStoppedVMOnClvmStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Stopped); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + Assert.assertEquals("Should return DEFAULT for stopped VM on CLVM storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMOnNfsStorageCanHandle() { + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnNfs = createVolume(vmId, 1L); + List volumes = List.of(volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(nfsPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return DEFAULT for running VM on NFS storage", + StrategyPriority.DEFAULT, result); + } + + @Test + public void testCanHandleRunningVMWithMixedStorageClvmAndNfsCantHandle() { + // Arrange - VM has volumes on both CLVM and NFS + Long vmId = 1L; + VMSnapshot vmSnapshot = Mockito.mock(VMSnapshot.class); + Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId); + + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(vmId); + Mockito.when(vm.getState()).thenReturn(State.Running); + Mockito.when(userVmDao.findById(vmId)).thenReturn(vm); + + VolumeVO volumeOnClvm = createVolume(vmId, 1L); + VolumeVO volumeOnNfs = createVolume(vmId, 2L); + List volumes = List.of(volumeOnClvm, volumeOnNfs); + Mockito.when(volumeDao.findByInstance(vmId)).thenReturn(volumes); + + StoragePoolVO clvmPool = createStoragePool("clvm-pool", Storage.StoragePoolType.CLVM); + StoragePoolVO nfsPool = createStoragePool("nfs-pool", Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDao.findById(1L)).thenReturn(clvmPool); + + StrategyPriority result = defaultVMSnapshotStrategy.canHandle(vmSnapshot); + + Assert.assertEquals("Should return CANT_HANDLE if any volume is on CLVM storage for running VM", + StrategyPriority.CANT_HANDLE, result); + } + + private VolumeVO createVolume(Long vmId, Long poolId) { + VolumeVO volume = new VolumeVO("volume", 0L, 0L, 0L, 0L, 0L, + "folder", "path", Storage.ProvisioningType.THIN, 0L, Volume.Type.ROOT); + volume.setInstanceId(vmId); + volume.setPoolId(poolId); + return volume; + } + + private StoragePoolVO createStoragePool(String name, Storage.StoragePoolType poolType) { + StoragePoolVO pool = Mockito.mock(StoragePoolVO.class); + Mockito.when(pool.getName()).thenReturn(name); + Mockito.when(pool.getPoolType()).thenReturn(poolType); + return pool; + } } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java index 061d18dc3769..7eee32b9f1b5 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java @@ -32,8 +32,12 @@ import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; +import com.cloud.storage.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.user.Account; import com.cloud.utils.Pair; +import com.cloud.utils.db.QueryBuilder; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -46,6 +50,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.storage.LocalHostEndpoint; import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import org.springframework.stereotype.Component; @@ -59,8 +64,8 @@ import com.cloud.storage.DataStoreRole; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage.TemplateType; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import com.cloud.utils.db.DB; -import com.cloud.utils.db.QueryBuilder; import com.cloud.utils.db.SearchCriteria.Op; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; @@ -75,6 +80,12 @@ public class DefaultEndPointSelector implements EndPointSelector { private HostDao hostDao; @Inject private DedicatedResourceDao dedicatedResourceDao; + @Inject + private PrimaryDataStoreDao _storagePoolDao; + @Inject + private VolumeDetailsDao _volDetailsDao; + @Inject + private ClvmPoolManager clvmPoolManager; private static final String VOL_ENCRYPT_COLUMN_NAME = "volume_encryption_support"; private final String findOneHostOnPrimaryStorage = "select t.id from " @@ -264,6 +275,27 @@ public EndPoint select(DataObject srcData, DataObject destData) { @Override public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) { + if (destData instanceof VolumeInfo) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy"); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + + // Check if SOURCE is a CLVM volume with active lock (for operations copying FROM CLVM to secondary storage) + if (srcData instanceof VolumeInfo) { + VolumeInfo srcVolume = (VolumeInfo) srcData; + DataStore srcStore = srcVolume.getDataStore(); + if (srcStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(srcVolume, pool, "copy operation"); + if (clvmEp != null) { + return clvmEp; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set DataStore srcStore = srcData.getDataStore(); DataStore destStore = destData.getDataStore(); if (moveBetweenPrimaryImage(srcStore, destStore)) { @@ -305,7 +337,6 @@ public EndPoint select(DataObject srcData, DataObject destData, StorageAction ac @Override public EndPoint select(DataObject srcData, DataObject destData, StorageAction action, boolean encryptionRequired) { - logger.error("IR24 select BACKUPSNAPSHOT from primary to secondary {} dest={}", srcData, destData); if (action == StorageAction.BACKUPSNAPSHOT && srcData.getDataStore().getRole() == DataStoreRole.Primary) { SnapshotInfo srcSnapshot = (SnapshotInfo)srcData; VolumeInfo volumeInfo = srcSnapshot.getBaseVolume(); @@ -314,6 +345,17 @@ public EndPoint select(DataObject srcData, DataObject destData, StorageAction ac if (vm != null && vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); } + // For CLVM pools, the snapshot LVM device only exists on the lock-holder host. + // Route the backup CopyCommand to that same host regardless of VM state. + DataStore srcStore = volumeInfo.getDataStore(); + if (srcStore != null && srcStore.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(srcStore.getId()); + logger.debug("Checking if CLVM store and lock-holder routing applicable for snapshot {}", srcSnapshot.getUuid()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volumeInfo, pool, "snapshot backup"); + if (clvmEp != null) { + return clvmEp; + } + } } if (srcSnapshot.getHypervisorType() == Hypervisor.HypervisorType.VMware) { if (vm != null) { @@ -388,18 +430,103 @@ private List listUpAndConnectingSecondaryStorageVmHost(Long dcId) { return sc.list(); } + /** + * Selects endpoint for CLVM volumes with destination host hint. + * This ensures volumes are created on the correct host with exclusive locks. + * + * @param volume The volume to check for CLVM routing + * @param operation Description of the operation (for logging) + * @return EndPoint for the destination host if CLVM routing applies, null otherwise + */ + private EndPoint selectClvmEndpointIfApplicable(VolumeInfo volume, String operation) { + DataStore store = volume.getDataStore(); + + if (store.getRole() != DataStoreRole.Primary) { + return null; + } + + + // Check if this is a CLVM pool + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + if (Volume.State.Allocated == volume.getState()) { + // Check if destination host hint is set + Long destHostId = volume.getDestinationHostId(); + if (destHostId == null) { + return null; + } + + logger.info("CLVM {}: routing volume {} to destination host {} for optimal exclusive lock placement", + operation, volume.getUuid(), destHostId); + + EndPoint ep = getEndPointFromHostId(destHostId); + if (ep != null) { + return ep; + } + } + + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + return null; + } + logger.info("CLVM {}: routing existing volume {} to live lock-holder host {}", + operation, volume.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for lock host {}, falling back to default selection", lockHostId); + return null; + } + @Override public EndPoint select(DataObject object, boolean encryptionSupportRequired) { DataStore store = object.getDataStore(); + + // This ensures volumes are created on the correct host with exclusive locks + String operation = ""; + if (DataStoreRole.Primary == store.getRole()) { + VolumeInfo volume = null; + if (object instanceof VolumeInfo) { + volume = (VolumeInfo) object; + operation = "volume creation"; + } else if (object instanceof SnapshotInfo) { + volume = ((SnapshotInfo) object).getBaseVolume(); + operation = "snapshot creation"; + } + + if (volume != null) { + EndPoint clvmEndpoint = selectClvmEndpointIfApplicable(volume, operation); + if (clvmEndpoint != null) { + return clvmEndpoint; + } + } + } + + // Default behavior for non-CLVM or when no destination host is set if (store.getRole() == DataStoreRole.Primary) { return findEndPointInScope(store.getScope(), findOneHostOnPrimaryStorage, store.getId(), encryptionSupportRequired); } throw new CloudRuntimeException(String.format("Storage role %s doesn't support encryption", store.getRole())); } + @Override public EndPoint select(DataObject object) { DataStore store = object.getDataStore(); + + // For CLVM volumes, check if there's a lock host ID to route to + if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) { + VolumeInfo volume = (VolumeInfo) object; + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "operation"); + if (clvmEp != null) { + return clvmEp; + } + } + EndPoint ep = select(store); if (ep != null) { return ep; @@ -493,6 +620,19 @@ public EndPoint select(DataObject object, StorageAction action, boolean encrypti } case DELETEVOLUME: { VolumeInfo volume = (VolumeInfo) object; + + // For CLVM volumes, route to the host holding the exclusive lock + if (volume.getHypervisorType() == Hypervisor.HypervisorType.KVM) { + DataStore store = volume.getDataStore(); + if (store.getRole() == DataStoreRole.Primary) { + StoragePoolVO pool = _storagePoolDao.findById(store.getId()); + EndPoint clvmEp = tryRouteToClvmLockHolder(volume, pool, "deletion"); + if (clvmEp != null) { + return clvmEp; + } + } + } + if (volume.getHypervisorType() == Hypervisor.HypervisorType.VMware) { VirtualMachine vm = volume.getAttachedVM(); if (vm != null) { @@ -540,6 +680,14 @@ protected EndPoint getEndPointForSnapshotOperationsInKvm(SnapshotInfo snapshotIn if (vm.getState() == VirtualMachine.State.Running) { return getEndPointFromHostId(vm.getHostId()); + } else if (vm.getState() == VirtualMachine.State.Stopped) { + StoragePoolVO pool = _storagePoolDao.findById(volumeInfo.getPoolId()); + if (pool != null && ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + EndPoint ep = getApplicableEndpointForClvm(snapshotInfo, volumeInfo); + if (ep != null) { + return ep; + } + } } Long hostId = vm.getLastHostId(); @@ -552,6 +700,20 @@ protected EndPoint getEndPointForSnapshotOperationsInKvm(SnapshotInfo snapshotIn return select(snapshotInfo, encryptionRequired); } + private EndPoint getApplicableEndpointForClvm(SnapshotInfo snapshotInfo, VolumeInfo volumeInfo) { + Long lockHostId = getClvmLockHostId(volumeInfo); + if (lockHostId != null) { + logger.debug("CLVM snapshot operation: routing snapshot [{}] to lock-holder host [{}]", + snapshotInfo.getUuid(), lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for CLVM lock host {}, falling back", lockHostId); + } + return null; + } + @Override public EndPoint select(Scope scope, Long storeId) { return findEndPointInScope(scope, findOneHostOnPrimaryStorage, storeId); @@ -589,4 +751,46 @@ public List selectAll(DataStore store) { } return endPoints; } + + protected EndPoint tryRouteToClvmLockHolder(VolumeInfo volume, StoragePoolVO pool, String operation) { + if (pool == null || !ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + return null; + } + Long lockHostId = getClvmLockHostId(volume); + if (lockHostId == null) { + logger.debug("No CLVM lock host tracked for volume {}, using default endpoint selection", volume.getUuid()); + return null; + } + logger.info("Routing CLVM volume {} {} to lock holder host {}", volume.getUuid(), operation, lockHostId); + EndPoint ep = getEndPointFromHostId(lockHostId); + if (ep != null) { + return ep; + } + logger.warn("Could not get endpoint for CLVM lock host {}, falling back to default selection", lockHostId); + return null; + } + + /** + * Gets the CLVM lock host ID for a volume by querying actual LVM state. + * + * @param volume The CLVM volume + * @return Host ID holding the lock, or null if not found + */ + protected Long getClvmLockHostId(VolumeInfo volume) { + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (lockHostId != null) { + logger.debug("Found actual lock host {} for volume {} via LVM query", lockHostId, volume.getUuid()); + } + + return lockHostId; + } } diff --git a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java index 9f01ab162ba7..ca124ec30f04 100644 --- a/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java +++ b/engine/storage/src/test/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelectorTest.java @@ -17,20 +17,41 @@ package org.apache.cloudstack.storage.endpoint; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Volume; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.VolumeDetailVO; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.StorageAction; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.RemoteHostEndPoint; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mockStatic; + @RunWith(MockitoJUnitRunner.class) public class DefaultEndPointSelectorTest { @@ -46,12 +67,55 @@ public class DefaultEndPointSelectorTest { @Mock private DataStore datastoreMock; - @Spy - private DefaultEndPointSelector defaultEndPointSelectorSpy; + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private PrimaryDataStoreDao _storagePoolDao; + + @Mock + private VolumeDetailsDao _volDetailsDao; + + @Mock + private VolumeDetailVO volumeDetailVOMock; + + @Mock + private EndPoint endPointMock; + + @Mock + ClvmPoolManager clvmPoolManager; + + @Mock + HostDao hostDao; + + static MockedStatic remoteHostEndPointMock; + + @InjectMocks + private DefaultEndPointSelector defaultEndPointSelectorSpy = Mockito.spy(new DefaultEndPointSelector()); + + private static final Long VOLUME_ID = 1L; + private static final Long HOST_ID = 10L; + private static final Long DEST_HOST_ID = 20L; + private static final Long STORE_ID = 100L; + private static final String VOLUME_UUID = "test-volume-uuid"; + + @BeforeClass + public static void init() { + remoteHostEndPointMock = mockStatic(RemoteHostEndPoint.class); + } + + @AfterClass + public static void close() { + remoteHostEndPointMock.close(); + } @Before public void setup() { Mockito.doReturn(volumeInfoMock).when(snapshotInfoMock).getBaseVolume(); + + // Common volume mock setup + Mockito.when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + Mockito.when(volumeInfoMock.getUuid()).thenReturn(VOLUME_UUID); } @Test @@ -197,4 +261,293 @@ public void getEndPointForSnapshotOperationsInKvmTestVolumeAttachedToStoppedVmAn Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(snapshotInfoMock, false); } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(DEST_HOST_ID); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithDestinationHost_CLVM_NG() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_VolumeWithoutDestinationHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.when(volumeInfoMock.getDestinationHostId()).thenReturn(null); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_NonCLVMPool() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.when(datastoreMock.getScope()).thenReturn(Mockito.mock(Scope.class)); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointInScope( + Mockito.any(), Mockito.anyString(), Mockito.eq(STORE_ID), Mockito.eq(false)); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.never()).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectClvmEndpoint_SnapshotWithBaseVolumeDestHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(snapshotInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(snapshotInfoMock.getBaseVolume()).thenReturn(volumeInfoMock); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(snapshotInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVM_NG_WithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectWithAction_DeleteVolume_CLVMWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(volumeInfoMock, false); + } + + @Test + public void testSelectWithAction_DeleteVolume_NonCLVM() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(volumeInfoMock.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.NetworkFilesystem); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(volumeInfoMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock, StorageAction.DELETEVOLUME, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(_volDetailsDao, Mockito.never()).findDetail(Mockito.anyLong(), Mockito.anyString()); + } + + @Test + public void testSelectObject_CLVMVolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVM_NG_VolumeWithLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(HOST_ID); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(HOST_ID); + } + + @Test + public void testSelectObject_CLVMVolumeWithoutLockHost() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + RemoteHostEndPoint ep = Mockito.mock(RemoteHostEndPoint.class); + Host lockHost = Mockito.mock(Host.class); + remoteHostEndPointMock.when(() -> RemoteHostEndPoint.getHypervisorHostEndPoint(lockHost)).thenReturn(ep); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithInvalidLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectObject_CLVMVolumeWithEmptyLockHostId() { + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).select(datastoreMock); + + EndPoint result = defaultEndPointSelectorSpy.select(volumeInfoMock); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).select(datastoreMock); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM_NG); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).getEndPointFromHostId(DEST_HOST_ID); + Mockito.doReturn(DEST_HOST_ID).when(defaultEndPointSelectorSpy).getClvmLockHostId(volumeInfoMock); + Mockito.when(volumeInfoMock.getState()).thenReturn(Volume.State.Creating); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + assertEquals(endPointMock, result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).getEndPointFromHostId(DEST_HOST_ID); + } + + @Test + public void testSelectTwoObjects_TemplateToVolume_CLVMWithoutDestHost() { + DataObject srcDataMock = Mockito.mock(DataObject.class); + DataStore srcStoreMock = Mockito.mock(DataStore.class); + + Mockito.when(srcDataMock.getDataStore()).thenReturn(srcStoreMock); + Mockito.when(srcStoreMock.getRole()).thenReturn(DataStoreRole.Image); + + Mockito.when(volumeInfoMock.getDataStore()).thenReturn(datastoreMock); + Mockito.when(datastoreMock.getRole()).thenReturn(DataStoreRole.Primary); + Mockito.when(datastoreMock.getId()).thenReturn(STORE_ID); + Mockito.when(_storagePoolDao.findById(STORE_ID)).thenReturn(storagePoolVOMock); + Mockito.when(storagePoolVOMock.getPoolType()).thenReturn(StoragePoolType.CLVM); + Mockito.doReturn(endPointMock).when(defaultEndPointSelectorSpy).findEndPointForImageMove( + srcStoreMock, datastoreMock, false); + + EndPoint result = defaultEndPointSelectorSpy.select(srcDataMock, volumeInfoMock, false); + + assertNotNull(result); + Mockito.verify(defaultEndPointSelectorSpy, Mockito.times(1)).findEndPointForImageMove(srcStoreMock, datastoreMock, false); + } + } diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java index 7de9000782ec..7644d4688f7e 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java @@ -37,6 +37,7 @@ import com.cloud.network.dao.NetworkVO; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -139,6 +140,18 @@ public boolean hostConnect(long hostId, long poolId) throws StorageConflictExcep Map nfsMountOpts = storageManager.getStoragePoolNFSMountOpts(pool, null).first(); Optional.ofNullable(nfsMountOpts).ifPresent(detailsMap::putAll); + + // Propagate CLVM secure zero-fill setting to the host + // Note: This is done during host connection (agent start, MS restart, host reconnection) + // so the setting is non-dynamic. Changes require host reconnection to take effect. + if (ClvmPoolManager.isClvmPoolType(pool.getPoolType())) { + Boolean clvmSecureZeroFill = ClvmPoolManager.CLVMSecureZeroFill.valueIn(poolId); + if (clvmSecureZeroFill != null) { + detailsMap.put("clvmsecurezerofill", String.valueOf(clvmSecureZeroFill)); + logger.debug("Added CLVM secure zero-fill setting: {} for storage pool: {}", clvmSecureZeroFill, pool); + } + } + ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, pool, detailsMap); cmd.setWait(modifyStoragePoolCommandWait); HostVO host = hostDao.findById(hostId); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java index 43218b3f6a02..20d677f90137 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java @@ -24,6 +24,7 @@ import com.cloud.dc.VsphereStoragePolicyVO; import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.storage.StorageManager; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.utils.Pair; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallbackNoReturn; @@ -126,6 +127,7 @@ public class VolumeObject implements VolumeInfo { private boolean directDownload; private String vSphereStoragePolicyId; private boolean followRedirects; + private Long destinationHostId; // For CLVM: hints where volume should be created private List checkpointPaths; private Set checkpointImageStoreUrls; @@ -361,6 +363,30 @@ public void setDirectDownload(boolean directDownload) { this.directDownload = directDownload; } + @Override + public Long getDestinationHostId() { + // If not in memory, try to load from the database (volume_details table) + // For CLVM volumes, this uses the CLVM_LOCK_HOST_ID, which serves a dual purpose: + // 1. During creation: hints where to create the volume + // 2. After creation: tracks which host holds the exclusive lock + if (destinationHostId == null && volumeVO != null) { + VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeVO.getId(), ClvmPoolManager.CLVM_LOCK_HOST_ID); + if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) { + try { + destinationHostId = Long.parseLong(detail.getValue()); + } catch (NumberFormatException e) { + logger.warn("Invalid CLVM lock host ID value in volume_details for volume {}: {}", volumeVO.getUuid(), detail.getValue()); + } + } + } + return destinationHostId; + } + + @Override + public void setDestinationHostId(Long hostId) { + this.destinationHostId = hostId; + } + public void update() { volumeDao.update(volumeVO.getId(), volumeVO); volumeVO = volumeDao.findById(volumeVO.getId()); diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 8731e8791ddd..6fe4c2708c59 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -32,6 +32,8 @@ import javax.inject.Inject; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -221,6 +223,8 @@ public class VolumeServiceImpl implements VolumeService { private PassphraseDao passphraseDao; @Inject protected DiskOfferingDao diskOfferingDao; + @Inject + ClvmPoolManager clvmPoolManager; public VolumeServiceImpl() { } @@ -2970,4 +2974,173 @@ public void moveVolumeOnSecondaryStorageToAnotherAccount(Volume volume, Account protected String buildVolumePath(long accountId, long volumeId) { return String.format("%s/%s/%s", TemplateConstants.DEFAULT_VOLUME_ROOT_DIR, accountId, volumeId); } + + @Override + public boolean transferVolumeLock(VolumeInfo volume, Long sourceHostId, Long destHostId) { + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + if (pool == null) { + logger.error("Cannot transfer volume lock for volume {}: storage pool not found", volume.getUuid()); + return false; + } + + logger.info("Transferring CLVM lock for volume {} (pool: {}) from host {} to host {}", + volume.getUuid(), pool.getName(), sourceHostId, destHostId); + + return clvmPoolManager.transferClvmVolumeLock(volume.getUuid(), volume.getId(), volume.getPath(), + pool, sourceHostId, destHostId); + } + + @Override + public Long findVolumeLockHost(VolumeInfo volume) { + if (volume == null) { + logger.warn("Cannot find volume lock host: volume is null"); + return null; + } + + StoragePoolVO pool = storagePoolDao.findById(volume.getPoolId()); + + Long lockHostId = clvmPoolManager.getClvmLockHostId( + volume.getId(), + volume.getUuid(), + volume.getPath(), + pool, + true + ); + + if (lockHostId != null) { + logger.debug("Found actual lock host {} for volume {}", lockHostId, volume.getUuid()); + return lockHostId; + } + + Long instanceId = volume.getInstanceId(); + if (instanceId != null) { + VMInstanceVO vmInstance = vmDao.findById(instanceId); + if (vmInstance != null && vmInstance.getHostId() != null) { + logger.debug("Volume {} is attached to VM {} on host {}", + volume.getUuid(), vmInstance.getUuid(), vmInstance.getHostId()); + return vmInstance.getHostId(); + } + } + + if (pool != null && pool.getClusterId() != null) { + List hosts = _hostDao.findByClusterId(pool.getClusterId()); + if (hosts != null && !hosts.isEmpty()) { + for (HostVO host : hosts) { + if (host.getStatus() == com.cloud.host.Status.Up) { + logger.debug("Using fallback: first UP host {} in cluster {} for volume {}", + host.getId(), pool.getClusterId(), volume.getUuid()); + return host.getId(); + } + } + } + } + + logger.warn("Could not determine lock host for volume {}", volume.getUuid()); + return null; + } + + @Override + public VolumeInfo performLockMigration(VolumeInfo volume, Long destHostId) { + if (volume == null) { + throw new CloudRuntimeException("Cannot perform CLVM lock migration: volume is null"); + } + + String volumeUuid = volume.getUuid(); + logger.info("Starting CLVM lock migration for volume {} (id: {}) to host {}", + volumeUuid, volume.getUuid(), destHostId); + + Long sourceHostId = findVolumeLockHost(volume); + if (sourceHostId == null) { + logger.warn("Could not determine source host for CLVM volume {} lock, assuming volume is not exclusively locked", + volumeUuid); + sourceHostId = destHostId; + } + + if (sourceHostId.equals(destHostId)) { + logger.info("CLVM volume {} already has lock on destination host {}, no migration needed", + volumeUuid, destHostId); + return volume; + } + + logger.info("Migrating CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + boolean success = transferVolumeLock(volume, sourceHostId, destHostId); + if (!success) { + throw new CloudRuntimeException( + String.format("Failed to transfer CLVM lock for volume %s from host %s to host %s", + volumeUuid, sourceHostId, destHostId)); + } + + logger.info("Successfully migrated CLVM volume {} lock from host {} to host {}", + volumeUuid, sourceHostId, destHostId); + + return volFactory.getVolume(volume.getId()); + } + + @Override + public boolean areBothPoolsClvmType(StoragePoolType volumePoolType, StoragePoolType vmPoolType) { + if (volumePoolType == null || vmPoolType == null) { + logger.debug("Cannot check if both pools are CLVM type: one or both pool types are null"); + return false; + } + return ClvmPoolManager.isClvmPoolType(volumePoolType) && + ClvmPoolManager.isClvmPoolType(vmPoolType); + } + + @Override + public boolean isLockTransferRequired(VolumeInfo volumeToAttach, StoragePoolType volumePoolType, StoragePoolType vmPoolType, + Long volumePoolId, Long vmPoolId, Long vmHostId) { + if (volumePoolType != null && !ClvmPoolManager.isClvmPoolType(volumePoolType)) { + return false; + } + + if (volumePoolId == null || !volumePoolId.equals(vmPoolId)) { + return false; + } + + Long volumeLockHostId = findVolumeLockHost(volumeToAttach); + + if (volumeLockHostId == null) { + VolumeVO volumeVO = _volumeDao.findById(volumeToAttach.getId()); + if (volumeVO != null && volumeVO.getState() == Volume.State.Ready && volumeVO.getInstanceId() == null) { + logger.debug("CLVM volume {} is detached on same pool, lock transfer may be needed", + volumeToAttach.getUuid()); + return true; + } + } + + if (volumeLockHostId != null && vmHostId != null && !volumeLockHostId.equals(vmHostId)) { + logger.info("CLVM lock transfer required: Volume {} lock is on host {} but VM is on host {}", + volumeToAttach.getUuid(), volumeLockHostId, vmHostId); + return true; + } + + return false; + } + + @Override + public boolean isLightweightMigrationNeeded(StoragePoolType volumePoolType, StoragePoolType vmPoolType, + String volumePoolPath, String vmPoolPath) { + if (!areBothPoolsClvmType(volumePoolType, vmPoolType)) { + return false; + } + + String volumeVgName = extractVgNameFromPath(volumePoolPath); + String vmVgName = extractVgNameFromPath(vmPoolPath); + + if (volumeVgName != null && volumeVgName.equals(vmVgName)) { + logger.info("CLVM lightweight migration detected: Volume is in same VG ({}), only lock transfer needed (no data copy)", volumeVgName); + return true; + } + + return false; + } + + private String extractVgNameFromPath(String poolPath) { + if (poolPath == null) { + return null; + } + return poolPath.startsWith("/") ? poolPath.substring(1) : poolPath; + } } diff --git a/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java new file mode 100644 index 000000000000..38af2a7550b3 --- /dev/null +++ b/engine/storage/volume/src/test/java/org/apache/cloudstack/storage/volume/VolumeServiceImplClvmTest.java @@ -0,0 +1,520 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.storage.volume; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.eq; + +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.clvm.ClvmPoolManager; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; + +/** + * Tests for CLVM lock management methods in VolumeServiceImpl. + */ +@RunWith(MockitoJUnitRunner.class) +public class VolumeServiceImplClvmTest { + + @Spy + @InjectMocks + private VolumeServiceImpl volumeService; + + @Mock + private VolumeDao volumeDao; + + @Mock + private PrimaryDataStoreDao storagePoolDao; + + @Mock + private HostDao _hostDao; + + @Mock + private VMInstanceDao vmDao; + + @Mock + private VolumeDataFactory volFactory; + + @Mock + private VolumeInfo volumeInfoMock; + + @Mock + private VolumeVO volumeVOMock; + + @Mock + private StoragePoolVO storagePoolVOMock; + + @Mock + private HostVO hostVOMock; + + @Mock + private VMInstanceVO vmInstanceVOMock; + + @Mock + private ClvmPoolManager clvmPoolManager; + + private static final Long VOLUME_ID = 1L; + private static final Long POOL_ID_1 = 100L; + private static final Long POOL_ID_2 = 200L; + private static final Long HOST_ID_1 = 10L; + private static final Long HOST_ID_2 = 20L; + private static final String POOL_PATH_VG1 = "/vg1"; + + @Before + public void setup() { + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getUuid()).thenReturn("test-volume-uuid"); + when(volumeInfoMock.getPath()).thenReturn("test-volume-path"); + + volumeService.storagePoolDao = storagePoolDao; + volumeService._hostDao = _hostDao; + volumeService.vmDao = vmDao; + volumeService.volFactory = volFactory; + volumeService._volumeDao = volumeDao; + volumeService.clvmPoolManager = clvmPoolManager; + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_BothCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_MixedCLVMAndCLVM_NG() { + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.CLVM_NG)); + assertTrue(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVMOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_OneCLVM_NGOneNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM_NG, StoragePoolType.NetworkFilesystem)); + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM_NG)); + } + + @Test + public void testAreBothPoolsClvmType_BothNFS() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem)); + } + + @Test + public void testAreBothPoolsClvmType_NullVolumePoolType() { + assertFalse(volumeService.areBothPoolsClvmType(null, StoragePoolType.CLVM)); + } + + @Test + public void testAreBothPoolsClvmType_NullVmPoolType() { + assertFalse(volumeService.areBothPoolsClvmType(StoragePoolType.CLVM, null)); + } + + @Test + public void testAreBothPoolsClvmType_BothNull() { + assertFalse(volumeService.areBothPoolsClvmType(null, null)); + } + + + @Test + public void testIsLockTransferRequired_NonCLVMPool() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.NetworkFilesystem, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentPools() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_2, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullPoolIds() { + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + null, POOL_ID_1, HOST_ID_1)); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, null, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Ready); + when(volumeVOMock.getInstanceId()).thenReturn(null); // Detached + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DetachedVolumeNotReady() { + when(volumeDao.findById(VOLUME_ID)).thenReturn(volumeVOMock); + when(volumeVOMock.getState()).thenReturn(Volume.State.Allocated); + + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(null); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLockTransferRequired_SameHost() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, HOST_ID_1)); + } + + @Test + public void testIsLockTransferRequired_NullVmHostId() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertFalse(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM, StoragePoolType.CLVM, + POOL_ID_1, POOL_ID_1, null)); + } + + @Test + public void testIsLockTransferRequired_CLVM_NG_DifferentHosts() { + when(volumeService.findVolumeLockHost(volumeInfoMock)).thenReturn(HOST_ID_1); + + assertTrue(volumeService.isLockTransferRequired( + volumeInfoMock, StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + POOL_ID_1, POOL_ID_1, HOST_ID_2)); + } + + @Test + public void testIsLightweightMigrationNeeded_NonCLVMPools() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.NetworkFilesystem, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_OneCLVMOneNFS() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.NetworkFilesystem, + POOL_PATH_VG1, POOL_PATH_VG1)); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_NoSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_SameVG_MixedSlash() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_CLVM_NG_DifferentVG() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM_NG, + "/vg1", "/vg2")); + } + + @Test + public void testIsLightweightMigrationNeeded_MixedCLVM_CLVM_NG_SameVG() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM_NG, + "/vg1", "/vg1")); + + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM_NG, StoragePoolType.CLVM, + "/vg1", "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVolumePath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, "/vg1")); + } + + @Test + public void testIsLightweightMigrationNeeded_NullVmPath() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/vg1", null)); + } + + @Test + public void testIsLightweightMigrationNeeded_BothPathsNull() { + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + null, null)); + } + + @Test + public void testIsLightweightMigrationNeeded_ComplexVGNames() { + assertTrue(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-01")); + + assertFalse(volumeService.isLightweightMigrationNeeded( + StoragePoolType.CLVM, StoragePoolType.CLVM, + "/cloudstack-vg-01", "/cloudstack-vg-02")); + } + + @Test + public void testTransferVolumeLock_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + + assertTrue(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_Failure() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testTransferVolumeLock_PoolNotFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(null); + + assertFalse(volumeService.transferVolumeLock(volumeInfoMock, HOST_ID_1, HOST_ID_2)); + } + + @Test + public void testFindVolumeLockHost_NullVolume() { + Long result = volumeService.findVolumeLockHost(null); + assertNull(result); + } + + @Test + public void testFindVolumeLockHost_ExplicitLockFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FromAttachedVM() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(100L); + when(vmDao.findById(100L)).thenReturn(vmInstanceVOMock); + when(vmInstanceVOMock.getUuid()).thenReturn("vm-uuid"); + when(vmInstanceVOMock.getHostId()).thenReturn(HOST_ID_1); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_FallbackToClusterHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(10L); + when(hostVOMock.getId()).thenReturn(HOST_ID_1); + when(hostVOMock.getStatus()).thenReturn(com.cloud.host.Status.Up); + when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.singletonList(hostVOMock)); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertEquals(HOST_ID_1, result); + } + + @Test + public void testFindVolumeLockHost_NoHostFound() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(10L); + when(_hostDao.findByClusterId(10L)).thenReturn(java.util.Collections.emptyList()); + + Long result = volumeService.findVolumeLockHost(volumeInfoMock); + assertNull(result); + } + + @Test + public void testPerformLockMigration_Success() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(true); + when(volFactory.getVolume(VOLUME_ID)).thenReturn(volumeInfoMock); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + assertNotNull(result); + } + + @Test + public void testPerformLockMigration_SameHost() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_1); + assertEquals(volumeInfoMock, result); + } + + @Test + public void testPerformLockMigration_SourceHostNull() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("test-volume-path"), eq(storagePoolVOMock), eq(true))) + .thenReturn(null); + when(volumeInfoMock.getInstanceId()).thenReturn(null); + when(storagePoolVOMock.getClusterId()).thenReturn(null); + + VolumeInfo result = volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + assertNotNull(result); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_NullVolume() { + volumeService.performLockMigration(null, HOST_ID_2); + } + + @Test(expected = com.cloud.utils.exception.CloudRuntimeException.class) + public void testPerformLockMigration_TransferFails() { + when(volumeInfoMock.getPoolId()).thenReturn(POOL_ID_1); + when(volumeInfoMock.getId()).thenReturn(VOLUME_ID); + when(volumeInfoMock.getPath()).thenReturn("/dev/vg1/volume-1"); + when(storagePoolDao.findById(POOL_ID_1)).thenReturn(storagePoolVOMock); + when(clvmPoolManager.getClvmLockHostId( + eq(VOLUME_ID), eq("test-volume-uuid"), eq("/dev/vg1/volume-1"), eq(storagePoolVOMock), eq(true))) + .thenReturn(HOST_ID_1); + when(storagePoolVOMock.getName()).thenReturn("test-pool"); + when(clvmPoolManager.transferClvmVolumeLock( + "test-volume-uuid", VOLUME_ID, "/dev/vg1/volume-1", storagePoolVOMock, HOST_ID_1, HOST_ID_2)) + .thenReturn(false); + + volumeService.performLockMigration(volumeInfoMock, HOST_ID_2); + } +} diff --git a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java index 7c5692564c99..c9c48dbb179f 100644 --- a/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java +++ b/engine/userdata/src/main/java/org/apache/cloudstack/userdata/UserDataManagerImpl.java @@ -119,10 +119,10 @@ public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) { byte[] decodedUserData = null; // If GET, use 4K. If POST, support up to 1M. - if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) { - decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); - } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) { + if (BaseCmd.HTTPMethod.POST.equals(httpmethod)) { decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST); + } else { + decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET); } // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR. diff --git a/framework/cluster/pom.xml b/framework/cluster/pom.xml index 2dd28e8e628f..75bcaf9a7ddf 100644 --- a/framework/cluster/pom.xml +++ b/framework/cluster/pom.xml @@ -48,6 +48,12 @@ cloud-api ${project.version} + + org.apache.cloudstack + cloud-engine-schema + ${project.version} + compile + diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java new file mode 100644 index 000000000000..fcaa2a22e341 --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/ManagementServerHostDetailVO.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.cluster; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "management_server_details") +public class ManagementServerHostDetailVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + long id; + + @Column(name = "management_server_id") + long resourceId; + + @Column(name = "name") + String name; + + @Column(name = "value") + String value; + + @Column(name = "display") + private boolean display = true; + + public ManagementServerHostDetailVO(long poolId, String name, String value, boolean display) { + this.resourceId = poolId; + this.name = name; + this.value = value; + this.display = display; + } + + public ManagementServerHostDetailVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java new file mode 100644 index 000000000000..24fd60d21b3c --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.cluster.dao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +import com.cloud.cluster.ManagementServerHostDetailVO; +import com.cloud.utils.db.GenericDao; + +public interface ManagementServerHostDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java new file mode 100644 index 000000000000..5865bee0926b --- /dev/null +++ b/framework/cluster/src/main/java/com/cloud/cluster/dao/ManagementServerHostDetailsDaoImpl.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.cluster.dao; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ScopedConfigStorage; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +import com.cloud.cluster.ManagementServerHostDetailVO; + +public class ManagementServerHostDetailsDaoImpl extends ResourceDetailsDaoBase implements ManagementServerHostDetailsDao, ScopedConfigStorage { + + public ManagementServerHostDetailsDaoImpl() { + } + + @Override + public ConfigKey.Scope getScope() { + return ConfigKey.Scope.ManagementServer; + } + + @Override + public String getConfigValue(long id, String key) { + ManagementServerHostDetailVO vo = findDetail(id, key); + return vo == null ? null : vo.getValue(); + } + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ManagementServerHostDetailVO(resourceId, key, value, display)); + } +} diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java index 926280bfeade..c334e91feb84 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java @@ -68,4 +68,6 @@ public interface AsyncJobDao extends GenericDao { // Returns the number of pending jobs for the given Management server msids. // NOTE: This is the msid and NOT the id long countPendingNonPseudoJobs(Long... msIds); + + List listPendingJobIdsForAccount(long accountId); } diff --git a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java index 81cc5d4f2a8c..d8385e9aecd1 100644 --- a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java +++ b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java @@ -299,4 +299,14 @@ public long countPendingJobs(String havingInfo, String... cmds) { List results = customSearch(sc, null); return results.get(0); } + + @Override + public List listPendingJobIdsForAccount(long accountId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.and("accountId", sb.entity().getAccountId(), SearchCriteria.Op.EQ); + sb.selectFields(sb.entity().getId()); + SearchCriteria sc = sb.create(); + sc.setParameters("accountId", accountId); + return customSearch(sc, null); + } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 58f51eae9fcd..5afef8bc95b6 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -32,6 +33,9 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.quota.activationrule.presetvariables.Configuration; import org.apache.cloudstack.quota.activationrule.presetvariables.GenericPresetVariable; @@ -43,9 +47,11 @@ import org.apache.cloudstack.quota.dao.QuotaAccountDao; import org.apache.cloudstack.quota.dao.QuotaBalanceDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -86,7 +92,8 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { private QuotaBalanceDao _quotaBalanceDao; @Inject private ConfigurationDao _configDao; - + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; @Inject protected PresetVariableHelper presetVariableHelper; @@ -311,14 +318,14 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO String accountToString = account.reflectionToString(); logger.info("Calculating quota usage of [{}] usage records for account [{}].", usageRecords.size(), accountToString); - List> pairsUsageAndQuotaUsage = new ArrayList<>(); + Map>> mapUsageAndQuotaUsage = new LinkedHashMap<>(); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { for (UsageVO usageRecord : usageRecords) { int usageType = usageRecord.getUsageType(); if (!shouldCalculateUsageRecord(account, usageRecord)) { - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, null)); + mapUsageAndQuotaUsage.put(usageRecord, null); continue; } @@ -326,18 +333,31 @@ protected List createQuotaUsagesAccordingToQuotaTariffs(AccountVO List quotaTariffs = pairQuotaTariffsPerUsageTypeAndHasActivationRule.first(); boolean hasAnyQuotaTariffWithActivationRule = pairQuotaTariffsPerUsageTypeAndHasActivationRule.second(); - BigDecimal aggregatedQuotaTariffsValue = aggregateQuotaTariffsValues(usageRecord, quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + Map aggregatedQuotaTariffsAndValues = aggregateQuotaTariffsValues(usageRecord, + quotaTariffs, hasAnyQuotaTariffWithActivationRule, jsInterpreter, accountToString); + BigDecimal aggregatedQuotaTariffsValue = aggregatedQuotaTariffsAndValues.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add); + logger.debug("The aggregation of the quota tariffs of account [{}] resulted in [{}] for the usage record [{}].", + account, aggregatedQuotaTariffsValue, usageRecord); QuotaUsageVO quotaUsage = createQuotaUsageAccordingToUsageUnit(usageRecord, aggregatedQuotaTariffsValue, accountToString); + if (quotaUsage == null) { + mapUsageAndQuotaUsage.put(usageRecord, null); + continue; + } - pairsUsageAndQuotaUsage.add(new Pair<>(usageRecord, quotaUsage)); + List quotaTariffUsages = new ArrayList<>(); + for (Map.Entry entry : aggregatedQuotaTariffsAndValues.entrySet()) { + QuotaTariffUsageVO quotaTariffUsage = createQuotaTariffUsage(usageRecord, entry.getKey(), entry.getValue()); + quotaTariffUsages.add(quotaTariffUsage); + } + mapUsageAndQuotaUsage.put(usageRecord, new Pair<>(quotaUsage, quotaTariffUsages)); } } catch (Exception e) { logger.error(String.format("Failed to calculate the quota usage for account [%s] due to [%s].", accountToString, e.getMessage()), e); return new ArrayList<>(); } - return persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(pairsUsageAndQuotaUsage); + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageAndQuotaUsage)); } protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageRecord) { @@ -349,31 +369,41 @@ protected boolean shouldCalculateUsageRecord(AccountVO accountVO, UsageVO usageR return true; } - protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(List> pairsUsageAndQuotaUsage) { + protected List persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(Map>> mapUsageAndQuotaTariffUsage) { List quotaUsages = new ArrayList<>(); - for (Pair pairUsageAndQuotaUsage : pairsUsageAndQuotaUsage) { - UsageVO usageVo = pairUsageAndQuotaUsage.first(); + for (Map.Entry>> usageAndTariffUsage : mapUsageAndQuotaTariffUsage.entrySet()) { + UsageVO usageVo = usageAndTariffUsage.getKey(); usageVo.setQuotaCalculated(1); _usageDao.persistUsage(usageVo); - QuotaUsageVO quotaUsageVo = pairUsageAndQuotaUsage.second(); - if (quotaUsageVo != null) { - _quotaUsageDao.persistQuotaUsage(quotaUsageVo); - quotaUsages.add(quotaUsageVo); + Pair> pairUsageAndTariffUsages = usageAndTariffUsage.getValue(); + if (pairUsageAndTariffUsages != null) { + QuotaUsageVO quotaUsage = pairUsageAndTariffUsages.first(); + _quotaUsageDao.persistQuotaUsage(quotaUsage); + quotaUsages.add(quotaUsage); + + persistQuotaTariffUsages(pairUsageAndTariffUsages.second(), quotaUsage.getId()); } } return quotaUsages; } - protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, - JsInterpreter jsInterpreter, String accountToString) { + protected void persistQuotaTariffUsages(List quotaTariffUsages, Long quotaUsageId) { + for (QuotaTariffUsageVO quotaTariffUsage : quotaTariffUsages) { + quotaTariffUsage.setQuotaUsageId(quotaUsageId); + quotaTariffUsageDao.persistQuotaTariffUsage(quotaTariffUsage); + } + } + + protected Map aggregateQuotaTariffsValues(UsageVO usageRecord, List quotaTariffs, boolean hasAnyQuotaTariffWithActivationRule, + JsInterpreter jsInterpreter, String accountToString) { String usageRecordToString = usageRecord.toString(usageAggregationTimeZone); logger.debug("Validating usage record [{}] for account [{}] against [{}] quota tariffs.", usageRecordToString, accountToString, quotaTariffs.size()); PresetVariables presetVariables = getPresetVariables(hasAnyQuotaTariffWithActivationRule, usageRecord); - BigDecimal aggregatedQuotaTariffsValue = BigDecimal.ZERO; + Map aggregatedQuotaTariffsAndValues = new HashMap<>(); quotaTariffs.sort(Comparator.comparing(QuotaTariffVO::getPosition)); @@ -382,10 +412,9 @@ protected BigDecimal aggregateQuotaTariffsValues(UsageVO usageRecord, List *

    diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java index 0da0d6e53f77..7b725d57c6ef 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java @@ -20,11 +20,23 @@ import java.util.HashMap; import java.util.Map; -import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.VpnUser; +import com.cloud.network.rules.LoadBalancer; +import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.storage.object.Bucket; import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.usage.UsageUnitTypes; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.commons.lang3.StringUtils; public class QuotaTypes extends UsageTypes { private final Integer quotaType; @@ -32,44 +44,46 @@ public class QuotaTypes extends UsageTypes { private final String quotaUnit; private final String description; private final String discriminator; + private final Class clazz; private final static Map quotaTypeMap; static { final HashMap quotaTypeList = new HashMap(); - quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage")); - quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage")); - quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage")); - quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)")); - quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)")); - quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage")); - quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage")); - quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage")); - quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage")); - quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage")); - quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage")); - quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage")); - quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage")); - quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage")); - quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)")); - quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)")); - quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)")); - quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)")); - quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage")); - quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage")); - quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage")); - quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage")); - quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage")); - quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage")); - quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage")); + quotaTypeList.put(RUNNING_VM, new QuotaTypes(RUNNING_VM, "RUNNING_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Running Vm Usage", VirtualMachine.class)); + quotaTypeList.put(ALLOCATED_VM, new QuotaTypes(ALLOCATED_VM, "ALLOCATED_VM", UsageUnitTypes.COMPUTE_MONTH.toString(), "Allocated Vm Usage", VirtualMachine.class)); + quotaTypeList.put(IP_ADDRESS, new QuotaTypes(IP_ADDRESS, "IP_ADDRESS", UsageUnitTypes.IP_MONTH.toString(), "IP Address Usage", IpAddress.class)); + quotaTypeList.put(NETWORK_BYTES_SENT, new QuotaTypes(NETWORK_BYTES_SENT, "NETWORK_BYTES_SENT", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Sent)", Network.class)); + quotaTypeList.put(NETWORK_BYTES_RECEIVED, new QuotaTypes(NETWORK_BYTES_RECEIVED, "NETWORK_BYTES_RECEIVED", UsageUnitTypes.GB.toString(), "Network Usage (Bytes Received)", Network.class)); + quotaTypeList.put(VOLUME, new QuotaTypes(VOLUME, "VOLUME", UsageUnitTypes.GB_MONTH.toString(), "Volume Usage", Volume.class)); + quotaTypeList.put(TEMPLATE, new QuotaTypes(TEMPLATE, "TEMPLATE", UsageUnitTypes.GB_MONTH.toString(), "Template Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(ISO, new QuotaTypes(ISO, "ISO", UsageUnitTypes.GB_MONTH.toString(), "ISO Usage", VirtualMachineTemplate.class)); + quotaTypeList.put(SNAPSHOT, new QuotaTypes(SNAPSHOT, "SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "Snapshot Usage", Snapshot.class)); + quotaTypeList.put(SECURITY_GROUP, new QuotaTypes(SECURITY_GROUP, "SECURITY_GROUP", UsageUnitTypes.POLICY_MONTH.toString(), "Security Group Usage", SecurityGroup.class)); + quotaTypeList.put(LOAD_BALANCER_POLICY, new QuotaTypes(LOAD_BALANCER_POLICY, "LOAD_BALANCER_POLICY", UsageUnitTypes.POLICY_MONTH.toString(), "Load Balancer Usage", LoadBalancer.class)); + quotaTypeList.put(PORT_FORWARDING_RULE, new QuotaTypes(PORT_FORWARDING_RULE, "PORT_FORWARDING_RULE", UsageUnitTypes.POLICY_MONTH.toString(), "Port Forwarding Usage", PortForwardingRule.class)); + quotaTypeList.put(NETWORK_OFFERING, new QuotaTypes(NETWORK_OFFERING, "NETWORK_OFFERING", UsageUnitTypes.POLICY_MONTH.toString(), "Network Offering Usage", NetworkOffering.class)); + quotaTypeList.put(VPN_USERS, new QuotaTypes(VPN_USERS, "VPN_USERS", UsageUnitTypes.POLICY_MONTH.toString(), "VPN users usage", VpnUser.class)); + quotaTypeList.put(VM_DISK_IO_READ, new QuotaTypes(VM_DISK_IO_READ, "VM_DISK_IO_READ", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Read)", Volume.class)); + quotaTypeList.put(VM_DISK_IO_WRITE, new QuotaTypes(VM_DISK_IO_WRITE, "VM_DISK_IO_WRITE", UsageUnitTypes.IOPS.toString(), "VM Disk usage(I/O Write)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_READ, new QuotaTypes(VM_DISK_BYTES_READ, "VM_DISK_BYTES_READ", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Read)", Volume.class)); + quotaTypeList.put(VM_DISK_BYTES_WRITE, new QuotaTypes(VM_DISK_BYTES_WRITE, "VM_DISK_BYTES_WRITE", UsageUnitTypes.BYTES.toString(), "VM Disk usage(Bytes Write)", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot storage usage", Snapshot.class)); + quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", UsageUnitTypes.GB_MONTH.toString(), "Volume secondary storage usage", Volume.class)); + quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", UsageUnitTypes.GB_MONTH.toString(), "VM Snapshot primary storage usage", Snapshot.class)); + quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", UsageUnitTypes.GB_MONTH.toString(), "Backup storage usage", BackupOffering.class)); + quotaTypeList.put(BUCKET, new QuotaTypes(BUCKET, "BUCKET", UsageUnitTypes.GB_MONTH.toString(), "Object Store bucket usage", Bucket.class)); + quotaTypeList.put(NETWORK, new QuotaTypes(NETWORK, "NETWORK", UsageUnitTypes.COMPUTE_MONTH.toString(), "Network usage", Network.class)); + quotaTypeList.put(VPC, new QuotaTypes(VPC, "VPC", UsageUnitTypes.COMPUTE_MONTH.toString(), "VPC usage", Vpc.class)); quotaTypeMap = Collections.unmodifiableMap(quotaTypeList); } - private QuotaTypes(Integer quotaType, String name, String unit, String description) { + private QuotaTypes(Integer quotaType, String name, String unit, String description, Class clazz) { this.quotaType = quotaType; this.description = description; this.quotaName = name; this.quotaUnit = unit; this.discriminator = "None"; + this.clazz = clazz; } public static Map listQuotaTypes() { @@ -104,22 +118,20 @@ static public String getDescription(int quotaType) { return null; } + public Class getClazz() { + return clazz; + } + static public QuotaTypes getQuotaType(int quotaType) { return quotaTypeMap.get(quotaType); } - static public QuotaTypes getQuotaTypeByName(String name) { - if (StringUtils.isBlank(name)) { - throw new CloudRuntimeException("Could not retrieve Quota type by name because the value passed as parameter is null, empty, or blank."); - } - - for (QuotaTypes type : quotaTypeMap.values()) { - if (type.getQuotaName().equals(name)) { - return type; - } + static public Class getClazz(int quotaType) { + QuotaTypes t = quotaTypeMap.get(quotaType); + if (t != null) { + return t.getClazz(); } - - throw new CloudRuntimeException(String.format("Could not find Quota type with name [%s].", name)); + return null; } @Override diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java index 126fa11413f7..ead70ae35012 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDao.java @@ -26,6 +26,6 @@ public interface QuotaUsageJoinDao extends GenericDao { - List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); + List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java index b98ea2b3a5d2..e69b1a05ba4d 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaUsageJoinDaoImpl.java @@ -61,7 +61,7 @@ public void init() { private void prepareQuotaUsageSearchBuilder(SearchBuilder searchBuilder) { searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); - searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainIds", searchBuilder.entity().getDomainId(), SearchCriteria.Op.IN); searchBuilder.and("usageType", searchBuilder.entity().getUsageType(), SearchCriteria.Op.EQ); searchBuilder.and("resourceId", searchBuilder.entity().getResourceId(), SearchCriteria.Op.EQ); searchBuilder.and("networkId", searchBuilder.entity().getNetworkId(), SearchCriteria.Op.EQ); @@ -71,11 +71,13 @@ private void prepareQuotaUsageSearchBuilder(SearchBuilder sear } @Override - public List findQuotaUsage(Long accountId, Long domainId, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { + public List findQuotaUsage(Long accountId, List domainIds, Integer usageType, Long resourceId, Long networkId, Long offeringId, Date startDate, Date endDate, Long tariffId) { SearchCriteria sc = tariffId == null ? searchQuotaUsages.create() : searchQuotaUsagesJoinTariffUsages.create(); sc.setParametersIfNotNull("accountId", accountId); - sc.setParametersIfNotNull("domainId", domainId); + if (domainIds != null) { + sc.setParameters("domainIds", domainIds.toArray()); + } sc.setParametersIfNotNull("usageType", usageType); sc.setParametersIfNotNull("resourceId", resourceId); sc.setParametersIfNotNull("networkId", networkId); diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index a33faa054de4..1e08e7d7fc00 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,9 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaTypes; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.usage.UsageUnitTypes; @@ -89,6 +92,9 @@ public class QuotaManagerImplTest { @Mock PresetVariables presetVariablesMock; + @Mock + QuotaTariffUsageDao quotaTariffUsageDaoMock; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Test @@ -484,47 +490,49 @@ public void getPresetVariablesTestHasTariffsWithActivationRuleReturnPresetVariab } @Test - public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnZero() { + public void aggregateQuotaTariffsValuesTestTariffsWereNotInPeriodToBeAppliedReturnEmptyMap() { List tariffs = createTariffList(); Mockito.doReturn(false).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnZero() { - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); + public void aggregateQuotaTariffsValuesTestTariffsIsEmptyReturnEmptyMap() { + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, new ArrayList<>(), false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.ZERO, result); + Assert.assertTrue(result.isEmpty()); } @Test - public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnAggregation() { + public void aggregateQuotaTariffsValuesTestTariffsAreInPeriodToBeAppliedReturnTariffsWithTheirValues() { List tariffs = createTariffList(); Mockito.doReturn(true, false, true).when(quotaManagerImplSpy).isQuotaTariffInPeriodToBeApplied(Mockito.any(), Mockito.any(), Mockito.anyString()); Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getQuotaTariffValueToBeApplied(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - BigDecimal result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); + Map result = quotaManagerImplSpy.aggregateQuotaTariffsValues(usageVoMock, tariffs, false, jsInterpreterMock, ""); - Assert.assertEquals(BigDecimal.TEN.multiply(new BigDecimal(2)), result); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.values().stream().allMatch(value -> value.equals(BigDecimal.TEN))); } @Test public void persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsagesTestReturnOnlyPersistedQuotaUsageVo() { - List> listPair = new ArrayList<>(); + Map>> mapUsageQuotaUsage = new LinkedHashMap<>(); QuotaUsageVO quotaUsageVoMock1 = Mockito.mock(QuotaUsageVO.class); QuotaUsageVO quotaUsageVoMock2 = Mockito.mock(QuotaUsageVO.class); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock1)); - listPair.add(new Pair<>(new UsageVO(), null)); - listPair.add(new Pair<>(new UsageVO(), quotaUsageVoMock2)); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock1, new ArrayList<>())); + mapUsageQuotaUsage.put(new UsageVO(), null); + mapUsageQuotaUsage.put(new UsageVO(), new Pair<>(quotaUsageVoMock2, new ArrayList<>())); Mockito.doReturn(null).when(usageDaoMock).persistUsage(Mockito.any()); Mockito.doReturn(null).when(quotaUsageDaoMock).persistQuotaUsage(Mockito.any()); + Mockito.doNothing().when(quotaManagerImplSpy).persistQuotaTariffUsages(Mockito.any(), Mockito.any()); - List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(listPair); + List result = quotaManagerImplSpy.persistUsagesAndQuotaUsagesAndRetrievePersistedQuotaUsages(mapUsageQuotaUsage); Assert.assertEquals(2, result.size()); Assert.assertEquals(quotaUsageVoMock1, result.get(0)); @@ -551,4 +559,41 @@ private static List createLastAppliedTariffsPresetVariableList(int numbe return lastTariffs; } + @Test + public void createQuotaTariffUsageTestTariffValueIsNotZeroReturnDetailedVo() { + Mockito.doReturn(1).when(usageVoMock).getUsageType(); + Mockito.doReturn(BigDecimal.TEN).when(quotaManagerImplSpy).getUsageValueAccordingToUsageUnitType(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(1L).when(quotaTariffVoMock).getId(); + + QuotaTariffUsageVO result = quotaManagerImplSpy.createQuotaTariffUsage(usageVoMock, quotaTariffVoMock, BigDecimal.TEN); + + Assert.assertEquals(1L, result.getTariffId().longValue()); + Assert.assertEquals(BigDecimal.TEN, result.getQuotaUsed()); + } + + @Test + public void persistQuotaTariffUsagesTestZeroQuotaTariffUsagesZeroPersisted() { + List quotaTariffUsages = new ArrayList<>(); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaTariffUsages, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.never()).persistQuotaTariffUsage(Mockito.any()); + } + + @Test + public void persistQuotaTariffUsagesTestTwoQuotaUsageDetailsTwoPersisted() { + List quotaUsageDetails = new ArrayList<>(); + QuotaTariffUsageVO quotaUsageDetailVoMock1 = Mockito.mock(QuotaTariffUsageVO.class); + QuotaTariffUsageVO quotaUsageDetailVoMock2 = Mockito.mock(QuotaTariffUsageVO.class); + + quotaUsageDetails.add(quotaUsageDetailVoMock1); + quotaUsageDetails.add(quotaUsageDetailVoMock2); + + Mockito.doNothing().when(quotaTariffUsageDaoMock).persistQuotaTariffUsage(Mockito.any()); + + quotaManagerImplSpy.persistQuotaTariffUsages(quotaUsageDetails, 1L); + + Mockito.verify(quotaTariffUsageDaoMock, Mockito.times(2)).persistQuotaTariffUsage(Mockito.any()); + } + } diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 13c0a36cb106..3ba2e4d5789e 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -127,6 +127,8 @@ Requires: rng-tools Requires: (libgcrypt > 1.8.3 or libgcrypt20) Requires: (selinux-tools if selinux-tools) Requires: sysstat +Requires: python3-libnbd +Requires: socat Provides: cloud-agent Group: System Environment/Libraries %description agent diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java new file mode 100644 index 000000000000..00dcb8fa5752 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaResourceStatementCmd.java @@ -0,0 +1,118 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.command; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.cloudstack.api.response.QuotaResourceStatementResponse; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; + +import javax.inject.Inject; + +import java.util.Date; + +@APICommand(name = "quotaResourceStatement", responseObject = QuotaResourceStatementResponse.class, since = "4.23.0.0", + description = "Generates a detailed Quota statement for a specific resource.", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false) +public class QuotaResourceStatementCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "ID of the resource.") + private String id; + + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, + description = "Generate the statement for this Account. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Account.") + private Long accountId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, + description = "Generate the statement for this Project. A resource may have belonged to different owners at distinct points in time, " + + "so this parameter can be used to only consider the period for which it belonged to a specific Project.") + private Long projectId; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, + description = "ID of the Domain for which the Quota statement will be generated.") + private Long domainId; + + @Parameter(name = ApiConstants.USAGE_TYPE, type = CommandType.INTEGER, required = true, description = "Usage type of the Quota usage.") + private Integer usageType; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = true, description = "Start date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = true, description = "End date for the query. " + + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + + @Inject + QuotaResponseBuilder responseBuilder; + + @Override + public void execute() { + QuotaResourceStatementResponse response = responseBuilder.createQuotaResourceStatement(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + if (ObjectUtils.allNull(accountId, projectId)) { + return -1; + } + return _accountService.finalizeAccountId(accountId, null, null, projectId); + } + + public String getId() { + return id; + } + + public Integer getUsageType() { + return usageType; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public Long getAccountId() { + return accountId; + } + + public Long getDomainId() { + return domainId; + } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java index bfe26a9f4250..f3a27ea58718 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaStatementCmd.java @@ -20,7 +20,6 @@ import javax.inject.Inject; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; @@ -32,18 +31,17 @@ import org.apache.cloudstack.api.response.QuotaStatementItemResponse; import org.apache.cloudstack.api.response.QuotaStatementResponse; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a Quota statement for the provided Account, Project, or Domain.", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, httpMethod = "GET") public class QuotaStatementCmd extends BaseCmd { - @ACL @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which the Quota statement will be generated. Deprecated, please use accountid instead.") private String accountName; - @ACL @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the Domain for which the Quota statement will be generated. May be used individually or with account.") private Long domainId; @@ -60,16 +58,18 @@ public class QuotaStatementCmd extends BaseCmd { description = "Consider only Quota usage records for the specified usage type in the statement.") private Integer usageType; - @ACL @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which the Quota statement will be generated. Can not be specified with projectid.") private Long accountId; - @ACL @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which the Quota statement will be generated. Can not be specified with accountid.", since = "4.23.0") private Long projectId; + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to include usage records from children of the filtered domain. " + + " Defaults to false.") + private Boolean recursive; + @Parameter(name = ApiConstants.SHOW_RESOURCES, type = CommandType.BOOLEAN, description = "List the resources of each Quota type in the period.", since = "4.23.0") private boolean showResources; @@ -152,4 +152,9 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } + + public boolean isRecursive() { + return BooleanUtils.isTrue(recursive); + } + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java new file mode 100644 index 000000000000..9f0a28389a9d --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementItemResponse.java @@ -0,0 +1,82 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.response; + +import java.math.BigDecimal; +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; + +public class QuotaResourceStatementItemResponse extends BaseResponse { + + @SerializedName(ApiConstants.TARIFF_ID) + @Param(description = "ID of the tariff.") + private String tariffId; + + @SerializedName(ApiConstants.TARIFF_NAME) + @Param(description = "Name of the tariff.") + private String tariffName; + + @SerializedName(ApiConstants.QUOTA_CONSUMED) + @Param(description = "Amount of quota used.") + private BigDecimal quotaUsed; + + @SerializedName(ApiConstants.START_DATE) + @Param(description = "Item's start date.") + private Date startDate; + + @SerializedName(ApiConstants.END_DATE) + @Param(description = "Item's end date.") + private Date endDate; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "UUID of the resource's owner.") + private String accountId; + + public QuotaResourceStatementItemResponse() { + super("quotaresourcestatementitem"); + } + + public void setTariffId(String tariffId) { + this.tariffId = tariffId; + } + + public void setTariffName(String tariffName) { + this.tariffName = tariffName; + } + + public void setQuotaUsed(BigDecimal quotaUsed) { + this.quotaUsed = quotaUsed; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java new file mode 100644 index 000000000000..ce3ab1f177ba --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResourceStatementResponse.java @@ -0,0 +1,66 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.math.BigDecimal; +import java.util.List; + +public class QuotaResourceStatementResponse extends BaseResponse { + + @SerializedName(ApiConstants.USAGE_NAME) + @Param(description = "Name of the usage type.") + private String usageName; + + @SerializedName(ApiConstants.UNIT) + @Param(description = "Unit of the usage type.") + private String unit; + + @SerializedName(ApiConstants.ITEMS) + @Param(description = "List of Quota tariff usages.", responseObject = QuotaResourceStatementItemResponse.class) + private List items; + + @SerializedName(ApiConstants.TOTAL_QUOTA) + @Param(description = "Total amount of quota used.") + private BigDecimal totalQuotaUsed; + + public QuotaResourceStatementResponse() { + super("quotaresourcestatement"); + } + + public void setUsageName(String usageName) { + this.usageName = usageName; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public void setQuotaUsageDetails(List items) { + this.items = items; + } + + public void setTotalQuotaUsed(BigDecimal totalQuotaUsed) { + this.totalQuotaUsed = totalQuotaUsed; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index f0390bf626d0..63bf043f4fa9 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -83,4 +84,6 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd); QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd); + + QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 231ba777e600..b71057e64234 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -66,11 +66,14 @@ import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; @@ -78,6 +81,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -107,16 +111,20 @@ import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; import org.apache.cloudstack.quota.dao.QuotaSummaryDao; import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaTariffUsageDao; import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao; import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; import org.apache.cloudstack.quota.vo.QuotaSummaryVO; +import org.apache.cloudstack.quota.vo.QuotaTariffUsageVO; import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO; import org.apache.cloudstack.quota.vo.QuotaUsageResourceVO; +import org.apache.cloudstack.usage.UsageTypes; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections4.CollectionUtils; @@ -181,7 +189,12 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private VMTemplateDao vmTemplateDao; @Inject private VolumeDao volumeDao; - + @Inject + private QuotaUsageJoinDao quotaUsageJoinDao; + @Inject + private QuotaTariffUsageDao quotaTariffUsageDao; + @Inject + private EntityManager entityMgr; private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class, ResourceCounting.class}; @@ -342,9 +355,9 @@ public QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd cmd) { @Override public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd) { - Long accountId = getAccountIdForQuotaStatement(cmd); - Long domainId = getDomainIdForQuotaStatement(cmd, accountId); - List quotaUsages = _quotaService.getQuotaUsage(accountId, null, domainId, cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), null); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + List quotaUsages = _quotaService.getQuotaUsage(accountId, null, baseDomainAndFilteredDomains.second(), cmd.getUsageType(), cmd.getStartDate(), cmd.getEndDate()); logger.debug("Creating quota statement from [{}] usage records for parameters [{}].", quotaUsages.size(), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "accountId", "projectId", "domainId", "startDate", "endDate", "usageType", "showResources")); @@ -367,52 +380,16 @@ public QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd Account account = _accountDao.findByIdIncludingRemoved(accountId); statement.setAccountId(account.getUuid()); statement.setAccountName(account.getAccountName()); - domainId = account.getDomainId(); } - if (domainId != null) { - DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + Long baseDomainId = baseDomainAndFilteredDomains.first(); + if (baseDomainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(baseDomainId); statement.setDomainId(domain.getUuid()); } return statement; } - protected Long getAccountIdForQuotaStatement(QuotaStatementCmd cmd) { - if (Account.Type.NORMAL.equals(CallContext.current().getCallingAccount().getType())) { - logger.debug("Limiting the Quota statement for the calling Account, as they are a User Account."); - return CallContext.current().getCallingAccountId(); - } - - long accountId = cmd.getEntityOwnerId(); - if (accountId != -1) { - return accountId; - } - - if (cmd.getDomainId() == null) { - logger.debug("Limiting the Quota statement for the calling Account, as 'domainid' was not informed."); - return CallContext.current().getCallingAccountId(); - } - - logger.debug("Allowing admin/domain admin to generate the Quota statement for the provided Domain."); - return null; - } - - protected Long getDomainIdForQuotaStatement(QuotaStatementCmd cmd, Long accountId) { - if (accountId != null) { - logger.debug("Quota statement is already limited to Account [{}].", accountId); - Account account = _accountDao.findByIdIncludingRemoved(accountId); - return account.getDomainId(); - } - - Long domainId = cmd.getDomainId(); - if (domainId != null) { - return domainId; - } - - logger.debug("Limiting the Quota statement for the calling Account's Domain."); - return CallContext.current().getCallingAccount().getDomainId(); - } - protected void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformed(List quotaUsages, Integer usageType) { if (usageType != null) { logger.debug("As the usage type [{}] was informed as parameter of the API quotaStatement, we will not create dummy records.", usageType); @@ -1138,7 +1115,7 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA addAllPresetVariables(PresetVariables.class, quotaType, usageTypeVariablesAndDescriptions, null); List usageTypeVariables = usageTypeVariablesAndDescriptions.stream().map(Pair::first).collect(Collectors.toList()); - try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value())) { + try (JsInterpreter jsInterpreter = new JsInterpreter(QuotaConfig.QuotaActivationRuleTimeout.value(), QuotaConfig.QuotaActivationRuleTimeout.key())) { Map newVariables = injectUsageTypeVariables(jsInterpreter, usageTypeVariables); String scriptToExecute = jsInterpreterHelper.replaceScriptVariables(activationRule, newVariables); jsInterpreter.executeScript(String.format("new Function(\"%s\")", scriptToExecute.replaceAll("\n", ""))); @@ -1158,6 +1135,184 @@ public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateA return createValidateActivationRuleResponse(activationRule, quotaName, false, message); } + @Override + public QuotaResourceStatementResponse createQuotaResourceStatement(QuotaResourceStatementCmd cmd) { + String resourceUuid = cmd.getId(); + Integer usageType = cmd.getUsageType(); + Date startDate = cmd.getStartDate(); + Date endDate = cmd.getEndDate(); + + if (startDate.after(endDate)) { + throw new InvalidParameterValueException(String.format("The start date [%s] must be before the end date [%s].", startDate, endDate)); + } + + InternalIdentity resource = retrieveResource(resourceUuid, usageType); + if (resource == null) { + logger.error("Could not find resource [{}] of type [{}]. Returning an empty list.", resourceUuid, usageType); + return createQuotaResourceStatementResponse(resourceUuid, usageType, new ArrayList<>(), new BigDecimal(0)); + } + + long id = resource.getId(); + + Long currentOwnerId = null; + if (resource instanceof ControlledEntity) { + currentOwnerId = ((ControlledEntity) resource).getAccountId(); + } + Long accountId = getAccountIdForQuotaStatement(cmd.getEntityOwnerId(), currentOwnerId); + Pair> baseDomainAndFilteredDomains = getDomainIdsForQuotaStatement(accountId, cmd.getDomainId(), cmd.isRecursive()); + + Long resourceId = null; + Long networkId = null; + Long offeringId = null; + + switch (usageType) { + case UsageTypes.NETWORK_OFFERING: + case UsageTypes.BACKUP: + offeringId = id; + break; + case UsageTypes.NETWORK_BYTES_RECEIVED: + case UsageTypes.NETWORK_BYTES_SENT: + networkId = id; + break; + default: + resourceId = id; + } + + logger.debug("Attempting to find quota usages with parameters usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaUsageJoinList = quotaUsageJoinDao.findQuotaUsage(accountId, baseDomainAndFilteredDomains.second(), usageType, resourceId, networkId, offeringId, startDate, endDate, null); + + logger.debug("Found [{}] quota usages using as parameter usage type [{}], usage id [{}], network id [{}], offering id [{}], and between [{}] and [{}].", + quotaUsageJoinList.size(), usageType, resourceId, networkId, offeringId, startDate, endDate); + + List quotaResourceStatementItemResponseList = new ArrayList<>(); + BigDecimal totalQuotaUsed = new BigDecimal(0); + List quotaTariffs = _quotaTariffDao.listQuotaTariffs(null, null, usageType, null, null, true, null, null).first(); + + for (QuotaUsageJoinVO quotaUsageJoin : quotaUsageJoinList) { + Account account = _accountMgr.getAccount(quotaUsageJoin.getAccountId()); + String accountUuid = account.getUuid(); + + List quotaTariffUsageList = quotaTariffUsageDao.listQuotaTariffUsages(quotaUsageJoin.getId()); + logger.debug("Found [{}] quota tariff usages associated to the quota usage [{}] of resource [{}] and type [{}] between [{}] and [{}].", + quotaTariffUsageList.size(), quotaUsageJoin, resourceUuid, usageType, startDate, endDate); + for (QuotaTariffUsageVO quotaTariffUsage: quotaTariffUsageList) { + quotaResourceStatementItemResponseList.add(createQuotaResourceStatementItemResponse(quotaTariffUsage, quotaTariffs, quotaUsageJoin.getStartDate(), + quotaUsageJoin.getEndDate(), accountUuid)); + totalQuotaUsed = totalQuotaUsed.add(quotaTariffUsage.getQuotaUsed()); + } + } + logger.debug("The total quota used of type [{}] between [{}] and [{}] for the resource [{}] was [{}].", usageType, startDate, endDate, resourceUuid, totalQuotaUsed); + + return createQuotaResourceStatementResponse(resourceUuid, usageType, quotaResourceStatementItemResponseList, totalQuotaUsed); + } + + protected Long getAccountIdForQuotaStatement(long providedAccountId, Long fallbackAccountId) { + Account caller = CallContext.current().getCallingAccount(); + + if (providedAccountId != -1L) { + Account account = _accountDao.findByIdIncludingRemoved(providedAccountId); + _accountMgr.checkAccess(caller, null, false, account); + logger.debug("Limiting the Quota resource statement for the provided Account [{}].", providedAccountId); + return providedAccountId; + } + + Account.Type callerType = caller.getType(); + if (Account.Type.ADMIN.equals(callerType) || Account.Type.DOMAIN_ADMIN.equals(callerType)) { + logger.debug("Not limiting the Quota resource statement for a specific Account, as no specific Account was provided and the caller is either an admin or domain admin."); + return null; + } + + if (fallbackAccountId != null) { + Account fallbackAccount = _accountDao.findByIdIncludingRemoved(fallbackAccountId); + _accountMgr.checkAccess(caller, null, false, fallbackAccount); + logger.debug("Limiting the Quota statement for the fallback Account [{}], as no specific Account was provided.", fallbackAccountId); + return fallbackAccountId; + } + + logger.debug("Limiting the Quota resource statement for the calling account, as no specific Account was provided and no fallback account was provided."); + return caller.getAccountId(); + } + + protected Pair> getDomainIdsForQuotaStatement(Long finalAccountId, Long providedDomainId, boolean isRecursive) { + if (finalAccountId != null) { + // Access to the provided account has already been validated + logger.debug("Not limiting the Quota statement for a specific Domain, as we are already limiting by Account."); + return new Pair<>(null, null); + } + + // User accounts will have already been limited to themselves + Account caller = CallContext.current().getCallingAccount(); + Long domainId = providedDomainId; + + if (domainId != null) { + Domain domain = domainDao.findByIdIncludingRemoved(domainId); + _accountMgr.checkAccess(caller, domain); + logger.debug("Limiting the Quota statement for the provided Domain [{}].", domainId); + } else { + domainId = caller.getDomainId(); + logger.debug("Limiting the Quota statement for the caller's Domain [{}], as no 'domainid' was provided.", domainId); + } + + if (isRecursive) { + logger.debug("Allowing the Quota statement for the Domain's children, as the 'isrecursive' parameter was provided."); + return new Pair<>(domainId, domainDao.getDomainAndChildrenIds(domainId)); + } + + return new Pair<>(domainId, List.of(domainId)); + } + + protected InternalIdentity retrieveResource(String resourceUuid, Integer usageType) { + Class clazz = QuotaTypes.getClazz(usageType); + if (clazz == null) { + throw new InvalidParameterValueException(String.format("Invalid usage type [%s] provided.", usageType)); + } + + logger.debug("Attempting to find a resource with ID [{}] and of type [{}].", resourceUuid, usageType); + Object object = entityMgr.findByUuidIncludingRemoved(clazz, resourceUuid); + if (object == null) { + return null; + } + return (InternalIdentity) object; + } + + protected QuotaResourceStatementItemResponse createQuotaResourceStatementItemResponse(QuotaTariffUsageVO quotaTariffUsage, List quotaTariffs, + Date startDate, Date endDate, String accountUuid) { + logger.trace("Creating quota resource statement item associated to quota tariff usage [{}].", quotaTariffUsage); + QuotaResourceStatementItemResponse quotaResourceStatementItemResponse = new QuotaResourceStatementItemResponse(); + + QuotaTariffVO quotaTariff = quotaTariffs.stream().filter(quotaTariffVO -> quotaTariffUsage.getTariffId().equals(quotaTariffVO.getId())).findAny().orElse(null); + + quotaResourceStatementItemResponse.setQuotaUsed(quotaTariffUsage.getQuotaUsed()); + quotaResourceStatementItemResponse.setStartDate(startDate); + quotaResourceStatementItemResponse.setEndDate(endDate); + quotaResourceStatementItemResponse.setAccountId(accountUuid); + if (quotaTariff != null) { + logger.trace("Quota usage details item will be associated to the quota tariff [{}].", quotaTariff); + quotaResourceStatementItemResponse.setTariffId(quotaTariff.getUuid()); + quotaResourceStatementItemResponse.setTariffName(quotaTariff.getName()); + } + + return quotaResourceStatementItemResponse; + } + + protected QuotaResourceStatementResponse createQuotaResourceStatementResponse(String resourceUuid, Integer usageType, + List quotaUsageDetailsItems, BigDecimal totalQuotaUsed) { + logger.trace("Creating quota usage details list response associated to the resource of UUID [{}], with an usage type of [{}], [{}] quota" + + " usage details items, and a total quota used of [{}].", resourceUuid, usageType, quotaUsageDetailsItems.size(), totalQuotaUsed); + QuotaResourceStatementResponse quotaResourceStatementResponse = new QuotaResourceStatementResponse(); + + QuotaTypes quotaType = QuotaTypes.getQuotaType(usageType); + quotaResourceStatementResponse.setUsageName(quotaType.getQuotaName()); + quotaResourceStatementResponse.setUnit(quotaType.getQuotaUnit()); + + quotaResourceStatementResponse.setQuotaUsageDetails(quotaUsageDetailsItems); + quotaResourceStatementResponse.setTotalQuotaUsed(totalQuotaUsed); + + return quotaResourceStatementResponse; + } + /** * Checks whether script variables are compatible with the usage type. First, we remove all script variables that correspond to the script's usage type variables. * Then, returns true if none of the remaining script variables match any usage types variables, and false otherwise. diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java index 81cb1011182d..c30780582d36 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaStatementResponse.java @@ -31,11 +31,11 @@ public class QuotaStatementResponse extends BaseResponse { @Param(description = "ID of the Account.") private String accountId; - @SerializedName(ApiConstants.ACCOUNT) + @SerializedName(ApiConstants.ACCOUNT_NAME) @Param(description = "Name of the Account.") private String accountName; - @SerializedName(ApiConstants.DOMAIN) + @SerializedName(ApiConstants.DOMAIN_ID) @Param(description = "ID of the Domain.") private String domainId; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index 478e43d2e203..cc257c233def 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -28,7 +28,7 @@ public interface QuotaService extends PluggableService { - List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate); + List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate); List listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index e1ec2ebb2a60..a788f0c8b01e 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.cloudstack.api.command.QuotaEnabledCmd; import org.apache.cloudstack.api.command.QuotaListEmailConfigurationCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaResourceStatementCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; @@ -131,6 +132,7 @@ public List> getCommands() { cmdList.add(QuotaListEmailConfigurationCmd.class); cmdList.add(QuotaPresetVariablesListCmd.class); cmdList.add(QuotaValidateActivationRuleCmd.class); + cmdList.add(QuotaResourceStatementCmd.class); return cmdList; } @@ -203,15 +205,15 @@ protected void validateStartDateAndEndDateForListQuotaBalancesForAccount(Date st } @Override - public List getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate) { + public List getQuotaUsage(Long accountId, String accountName, List domainIds, Integer usageType, Date startDate, Date endDate) { if (startDate.after(endDate)) { throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate); } - logger.debug("Getting quota records of type [{}] for account [{}] in domain [{}], between [{}] and [{}].", - usageType, accountId, domainId, startDate, endDate); + logger.debug("Getting quota records of type [{}] for account [{}] in domains [{}], between [{}] and [{}].", + usageType, accountId, domainIds, startDate, endDate); - return quotaUsageJoinDao.findQuotaUsage(accountId, domainId, usageType, null, null, null, startDate, endDate, null); + return quotaUsageJoinDao.findQuotaUsage(accountId, domainIds, usageType, null, null, null, startDate, endDate, null); } @Override diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 0c7610461c15..37307db45629 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -38,14 +38,15 @@ import com.cloud.user.AccountManager; import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; -import org.apache.cloudstack.api.command.QuotaStatementCmd; import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; @@ -156,7 +157,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { Date date = new Date(); @Mock - Account accountMock; + AccountVO accountMock; @Mock DomainVO domainVoMock; @@ -188,6 +189,9 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock User callerUserMock; + @Mock + EntityManager entityManagerMock; + @Before public void setup() { CallContext.register(callerUserMock, callerAccountMock); @@ -1024,107 +1028,161 @@ public void setStatementItemResourcesTestDoNotShowResourcesDoNothing() { } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountForNormalUser() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsProvidedAccount() { + long providedAccountId = 200L; - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.NORMAL).when(accountMock).getType(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(providedAccountId)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(providedAccountId, null); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); - - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + Assert.assertEquals(200L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getAccountIdForQuotaStatementTestReturnsEntityOwnerIdWhenProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.ADMIN); - Mockito.doReturn(42L).when(cmd).getEntityOwnerId(); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); + + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); + } + + @Test + public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsDomainAdminWithoutProvidedAccount() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.DOMAIN_ADMIN); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, null); - Assert.assertEquals(Long.valueOf(42L), result); + assertNull(result); + Mockito.verify(accountManagerMock, Mockito.never()).getAccount(Mockito.anyLong()); } @Test - public void getAccountIdForQuotaStatementTestLimitsToCallingAccountWhenCallerIsAdminAndDomainIsNotProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getAccountIdForQuotaStatementTestReturnsFallbackAccountWhenNoAccountProvidedAndCallerIsNotAdmin() { + Mockito.when(callerAccountMock.getType()).thenReturn(Account.Type.NORMAL); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(null).when(cmd).getDomainId(); + Mockito.when(accountDaoMock.findByIdIncludingRemoved(300L)).thenReturn(accountMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(-1L, 300L); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + assertEquals(300L, result); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + } - Assert.assertEquals(Long.valueOf(callerAccountMock.getAccountId()), result); - } + @Test + public void getAccountIdForQuotaStatementTestAccessDeniedForProvidedAccount() { + Mockito.when(accountDaoMock.findByIdIncludingRemoved(200L)).thenReturn(accountMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); + + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getAccountIdForQuotaStatement(200L, null)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, null, false, accountMock); } @Test - public void getAccountIdForQuotaStatementTestReturnsNullWhenCallerIsAdminAndDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getDomainIdsForQuotaStatementTestReturnsNullPairWhenAccountIsProvided() { + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(100L, null, false); - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - Mockito.doReturn(Account.Type.ADMIN).when(accountMock).getType(); - Mockito.doReturn(-1L).when(cmd).getEntityOwnerId(); - Mockito.doReturn(10L).when(cmd).getDomainId(); + assertNull(result.first()); + assertNull(result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainIdNonRecursively() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Long result = quotaResponseBuilderSpy.getAccountIdForQuotaStatement(cmd); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false); - Assert.assertNull(result); - } + assertEquals(5L, (long) result.first()); + assertEquals(List.of(5L), result.second()); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); } @Test - public void getDomainIdForQuotaStatementTestReturnsAccountDomainIdWhenAccountIdIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - AccountVO account = Mockito.mock(AccountVO.class); + public void getDomainIdsForQuotaStatementTestReturnsCallerDomainNonRecursively() { + Mockito.when(callerAccountMock.getDomainId()).thenReturn(7L); - Mockito.doReturn(account).when(accountDaoMock).findByIdIncludingRemoved(55L); - Mockito.doReturn(77L).when(account).getDomainId(); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, false); - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, 55L); + assertEquals(7L, (long) result.first()); + assertEquals(List.of(7L), result.second()); + Mockito.verify(domainDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); + } + + @Test + public void getDomainIdsForQuotaStatementTestReturnsProvidedDomainRecursively() { + List domainAndChildren = List.of(5L, 10L, 15L); + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(5L)).thenReturn(domainAndChildren); + Mockito.doNothing().when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Assert.assertEquals(Long.valueOf(77L), result); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, true); + + assertEquals(5L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(5L); } @Test - public void getDomainIdForQuotaStatementTestReturnsProvidedDomainIdWhenAccountIdIsNull() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); + public void getDomainIdsForQuotaStatementReturnsCallerDomainRecursively() { + List domainAndChildren = List.of(1L, 2L, 3L); + Mockito.when(callerAccountMock.getDomainId()).thenReturn(1L); - Mockito.doReturn(99L).when(cmd).getDomainId(); + Mockito.when(domainDaoMock.getDomainAndChildrenIds(1L)).thenReturn(domainAndChildren); - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + Pair> result = quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, null, true); - Assert.assertEquals(Long.valueOf(99L), result); + assertEquals(1L, (long) result.first()); + assertEquals(domainAndChildren, result.second()); + Mockito.verify(domainDaoMock).getDomainAndChildrenIds(1L); } @Test - public void getDomainIdForQuotaStatementTestFallsBackToCallingAccountDomainIdWhenNeitherAccountNorDomainIsProvided() { - QuotaStatementCmd cmd = Mockito.mock(QuotaStatementCmd.class); - Account account = Mockito.mock(Account.class); + public void getDomainIdsForQuotaStatementTestThrowsAccessDeniedForProvidedDomain() { + Mockito.when(domainDaoMock.findByIdIncludingRemoved(5L)).thenReturn(domainVoMock); + Mockito.doThrow(new PermissionDeniedException("Access denied")) + .when(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); - Mockito.doReturn(null).when(cmd).getDomainId(); - Mockito.doReturn(123L).when(account).getDomainId(); - Mockito.doReturn(account).when(callContextMock).getCallingAccount(); + Assert.assertThrows(PermissionDeniedException.class, + () -> quotaResponseBuilderSpy.getDomainIdsForQuotaStatement(null, 5L, false)); + Mockito.verify(accountManagerMock).checkAccess(callerAccountMock, domainVoMock); + } - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); + @Test(expected = InvalidParameterValueException.class) + public void retrieveResourceTestThrowsExceptionForInvalidUsageType() { + Integer invalidUsageType = 999; + quotaResponseBuilderSpy.retrieveResource("validUuid", invalidUsageType); + } + + @Test + public void retrieveResourceTestReturnsNullForNonexistentResource() { + String invalidUuid = "nonexistentUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; - Long result = quotaResponseBuilderSpy.getDomainIdForQuotaStatement(cmd, null); + Mockito.doReturn(null).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(invalidUuid)); + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(invalidUuid, validUsageType); - Assert.assertEquals(123L, result.longValue()); - } + Assert.assertNull(result); + } + + @Test + public void retrieveResourceTestReturnsCorrectResource() { + String validUuid = "validUuid"; + Integer validUsageType = QuotaTypes.ALLOCATED_VM; + InternalIdentity mockResource = Mockito.mock(InternalIdentity.class); + + Mockito.doReturn(mockResource).when(entityManagerMock).findByUuidIncludingRemoved(Mockito.any(), Mockito.eq(validUuid)); + + InternalIdentity result = quotaResponseBuilderSpy.retrieveResource(validUuid, validUsageType); + + Assert.assertNotNull(result); + Assert.assertEquals(mockResource, result); } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 89c48b1cc660..c0ee6c5fc3fe 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -117,12 +117,12 @@ public void setup() throws IllegalAccessException, NoSuchFieldException, Configu public void testGetQuotaUsage() { final long accountId = 2L; final String accountName = "admin123"; - final long domainId = 1L; + final List domainIds = List.of(1L); final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); - Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), + quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainIds, QuotaTypes.IP_ADDRESS, startDate, endDate); + Mockito.verify(quotaUsageJoinDaoMock, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainIds), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(Date.class), Mockito.any(Date.class), Mockito.any()); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java new file mode 100644 index 000000000000..2e9852f7bc1e --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/ImageServerControlSocket.java @@ -0,0 +1,123 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * Communicates with the cloudstack-image-server control socket via socat. + * + * Protocol: newline-delimited JSON over a Unix domain socket. + * Actions: register, unregister, status. + */ +public class ImageServerControlSocket { + private static final Logger LOGGER = LogManager.getLogger(ImageServerControlSocket.class); + static final String CONTROL_SOCKET_PATH = "/var/run/cloudstack/image-server.sock"; + private static final Gson GSON = new GsonBuilder().create(); + + private ImageServerControlSocket() { + } + + /** + * Send a JSON message to the image server control socket and return the + * parsed response, or null on communication failure. + */ + static JsonObject sendMessage(Map message) { + String json = GSON.toJson(message); + Script script = new Script("/bin/bash", LOGGER); + script.add("-c"); + script.add(String.format("echo '%s' | socat -t5 - UNIX-CONNECT:%s", + json.replace("'", "'\\''"), CONTROL_SOCKET_PATH)); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = script.execute(parser); + if (result != null) { + LOGGER.error("Control socket communication failed: {}", result); + return null; + } + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + LOGGER.error("Empty response from control socket"); + return null; + } + try { + return JsonParser.parseString(output.trim()).getAsJsonObject(); + } catch (Exception e) { + LOGGER.error("Failed to parse control socket response: {}", output, e); + return null; + } + } + + /** + * Register a transfer config with the image server. + * @return true if the server accepted the registration. + */ + public static boolean registerTransfer(String transferId, Map config) { + Map msg = new HashMap<>(); + msg.put("action", "register"); + msg.put("transfer_id", transferId); + msg.put("config", config); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return false; + } + return "ok".equals(resp.has("status") ? resp.get("status").getAsString() : null); + } + + /** + * Unregister a transfer from the image server. + * @return the number of remaining active transfers, or -1 on error. + */ + public static int unregisterTransfer(String transferId) { + Map msg = new HashMap<>(); + msg.put("action", "unregister"); + msg.put("transfer_id", transferId); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return -1; + } + if (!"ok".equals(resp.has("status") ? resp.get("status").getAsString() : null)) { + return -1; + } + return resp.has("active_transfers") ? resp.get("active_transfers").getAsInt() : -1; + } + + /** + * Check whether the image server control socket is responsive. + * @return true if the server responded with status "ok". + */ + public static boolean isReady() { + Map msg = new HashMap<>(); + msg.put("action", "status"); + JsonObject resp = sendMessage(msg); + if (resp == null) { + return false; + } + return "ok".equals(resp.has("status") ? resp.get("status").getAsString() : null); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 06b668b801d3..4a93b1bce4a1 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -79,6 +79,7 @@ import org.apache.cloudstack.command.ReconcileCommandUtils; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsAnswer; import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand; import org.apache.cloudstack.storage.configdrive.ConfigDrive; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; @@ -228,6 +229,7 @@ import com.cloud.storage.Storage; import com.cloud.storage.Storage.StoragePoolType; import com.cloud.storage.StorageLayer; +import com.cloud.storage.clvm.ClvmPoolManager; import com.cloud.storage.Volume; import com.cloud.storage.resource.StorageSubsystemCommandHandler; import com.cloud.storage.resource.StorageSubsystemCommandHandlerBase; @@ -387,6 +389,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String CHECKPOINT_DELETE_COMMAND = "virsh checkpoint-delete --domain %s --checkpointname %s --metadata"; + public static final int IMAGE_SERVER_DEFAULT_PORT = 54322; + public static final String IMAGE_SERVER_SYSTEMD_UNIT_NAME = "cloudstack-image-server"; + protected int qcow2DeltaMergeTimeout; private String modifyVlanPath; @@ -400,6 +405,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private String heartBeatPath; private String vmActivityCheckPath; private String nasBackupPath; + private String imageServerPath; + private boolean imageServerTlsEnabled = false; + private String imageServerListenAddress; private String securityGroupPath; private String ovsPvlanDhcpHostPath; private String ovsPvlanVmPath; @@ -814,6 +822,18 @@ public String getNasBackupPath() { return nasBackupPath; } + public String getImageServerPath() { + return imageServerPath; + } + + public boolean isImageServerTlsEnabled() { + return imageServerTlsEnabled; + } + + public String getImageServerListenAddress() { + return imageServerListenAddress; + } + public String getOvsPvlanDhcpHostPath() { return ovsPvlanDhcpHostPath; } @@ -1077,6 +1097,9 @@ public boolean configure(final String name, final Map params) th cachePath = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HOST_CACHE_LOCATION); + imageServerTlsEnabled = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_TLS_ENABLED); + imageServerListenAddress = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.IMAGE_SERVER_LISTEN_ADDRESS); + params.put("domr.scripts.dir", domrScriptsDir); virtRouterResource = new VirtualRoutingResource(this); @@ -1140,6 +1163,12 @@ public boolean configure(final String name, final Map params) th throw new ConfigurationException("Unable to find nasbackup.sh"); } + String imageServerMain = Script.findScript(kvmScriptsDir, "imageserver/__main__.py"); + if (imageServerMain == null) { + throw new ConfigurationException("Unable to find imageserver package"); + } + imageServerPath = new File(imageServerMain).getParent(); + createTmplPath = Script.findScript(storageScriptsDir, "createtmplt.sh"); if (createTmplPath == null) { throw new ConfigurationException("Unable to find the createtmplt.sh"); @@ -2557,6 +2586,8 @@ public String getResizeScriptType(final KVMStoragePool pool, final KVMPhysicalDi if (pool.getType() == StoragePoolType.CLVM && volFormat == PhysicalDiskFormat.RAW) { return "CLVM"; + } else if (poolType == StoragePoolType.CLVM_NG) { + return "CLVM_NG"; } else if ((poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SharedMountPoint || poolType == StoragePoolType.Filesystem @@ -3795,13 +3826,18 @@ public int compare(final DiskTO arg0, final DiskTO arg1) { final String glusterVolume = pool.getSourceDir().replace("/", ""); disk.defNetworkBasedDisk(glusterVolume + path.replace(mountpoint, ""), pool.getSourceHost(), pool.getSourcePort(), null, null, devId, diskBusType, DiskProtocol.GLUSTER, DiskDef.DiskFmtType.QCOW2); - } else if (pool.getType() == StoragePoolType.CLVM || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { + } else if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData); - } - else { + } else { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusType); } + + // CLVM_NG uses QCOW2 format on block devices, override the default RAW format + if (pool.getType() == StoragePoolType.CLVM_NG) { + disk.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); + } + if (pool.getType() == StoragePoolType.Linstor && isQemuDiscardBugFree(diskBusType)) { disk.setDiscard(DiscardType.UNMAP); } @@ -5349,6 +5385,24 @@ public void removeCheckpointsOnVm(String vmName, String volumeUuid, List logger.debug("Removed all checkpoints of volume [{}] on VM [{}].", volumeUuid, vmName); } + public Map getDiskPathLabelMap(String vmName) { + try { + Connect conn = LibvirtConnection.getConnectionByVmName(vmName); + List disks = getDisks(conn, vmName); + Map diskPathLabelMap = new HashMap<>(); + for (DiskDef disk : disks) { + if (disk.getDeviceType() != DeviceType.DISK) { + continue; + } + diskPathLabelMap.put(disk.getDiskPath(), disk.getDiskLabel()); + } + return diskPathLabelMap; + } catch (LibvirtException e) { + logger.error("Failed to get disk path label map for VM [{}] due to: [{}].", vmName, e.getMessage(), e); + throw new CloudRuntimeException(e); + } + } + public boolean recreateCheckpointsOnVm(List volumes, String vmName, Connect conn) { logger.debug("Trying to recreate checkpoints on VM [{}] with volumes [{}].", vmName, volumes); try { @@ -5683,10 +5737,73 @@ public boolean configureDefaultNetworkRulesForSystemVm(final Connect conn, final public Answer listFilesAtPath(ListDataStoreObjectsCommand command) { DataStoreTO store = command.getStore(); - KVMStoragePool storagePool = storagePoolManager.getStoragePool(StoragePoolType.NetworkFilesystem, store.getUuid()); + StoragePoolType poolType = StoragePoolType.NetworkFilesystem; + if (store instanceof PrimaryDataStoreTO) { + poolType = ((PrimaryDataStoreTO) store).getPoolType(); + } + KVMStoragePool storagePool = storagePoolManager.getStoragePool(poolType, store.getUuid()); + if (ClvmPoolManager.isClvmPoolType(poolType)) { + return listLvmVolumes(storagePool.getLocalPath(), command.getStartIndex(), command.getPageSize()); + } return listFilesAtPath(storagePool.getLocalPath(), command.getPath(), command.getStartIndex(), command.getPageSize()); } + private Answer listLvmVolumes(String localPath, int startIndex, int pageSize) { + String vgName = localPath; + if (vgName.startsWith("/")) { + String[] parts = vgName.split("/"); + for (int i = parts.length - 1; i >= 0; i--) { + if (!parts[i].isEmpty()) { + vgName = parts[i]; + break; + } + } + } + + Script lvs = new Script("lvs", 30000, logger); + lvs.add("--noheadings"); + lvs.add("--nosuffix"); + lvs.add("-o", "lv_name,lv_size"); + lvs.add("--units", "b"); + lvs.add(vgName); + AllLinesParser parser = new AllLinesParser(); + String result = lvs.execute(parser); + + List names = new ArrayList<>(); + List paths = new ArrayList<>(); + List absPaths = new ArrayList<>(); + List isDirs = new ArrayList<>(); + List sizes = new ArrayList<>(); + List lastModified = new ArrayList<>(); + + if (result != null) { + logger.warn("lvs listing failed for VG {}: {}", vgName, result); + return new ListDataStoreObjectsAnswer(false, 0, names, paths, absPaths, isDirs, sizes, lastModified); + } + + List entries = new ArrayList<>(); + for (String line : parser.getLines().split("\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + String[] cols = trimmed.split("\\s+"); + if (cols.length >= 2) entries.add(cols); + } + + int count = entries.size(); + for (int i = startIndex; i < startIndex + pageSize && i < count; i++) { + String lvName = entries.get(i)[0]; + long size = 0; + try { size = Long.parseLong(entries.get(i)[1]); } catch (NumberFormatException ignored) {} + names.add(lvName); + paths.add("/" + lvName); + absPaths.add("/dev/" + vgName + "/" + lvName); + isDirs.add(false); + sizes.add(size); + lastModified.add(0L); + } + return new ListDataStoreObjectsAnswer(true, count, names, paths, absPaths, isDirs, sizes, lastModified); + } + public boolean addNetworkRules(final String vmName, final String vmId, final String guestIP, final String guestIP6, final String sig, final String seq, final String mac, final String rules, final String vif, final String brname, final String secIps) { if (!canBridgeFirewall) { @@ -6775,4 +6892,237 @@ public String getHypervisorPath() { public String getGuestCpuArch() { return guestCpuArch; } + + /** + * CLVM volume state for migration operations on source host + */ + public enum ClvmVolumeState { + /** Shared mode (-asy) - used before migration to allow both hosts to access volume */ + SHARED("-asy", "shared", "Before migration: activating in shared mode"), + + /** Deactivate (-an) - used after successful migration to release volume on source */ + DEACTIVATE("-an", "deactivated", "After successful migration: deactivating volume"), + + /** Exclusive mode (-aey) - used after failed migration to revert to original exclusive state */ + EXCLUSIVE("-aey", "exclusive", "After failed migration: reverting to exclusive mode"); + + private final String lvchangeFlag; + private final String description; + private final String logMessage; + + ClvmVolumeState(String lvchangeFlag, String description, String logMessage) { + this.lvchangeFlag = lvchangeFlag; + this.description = description; + this.logMessage = logMessage; + } + + public String getLvchangeFlag() { + return lvchangeFlag; + } + + public String getDescription() { + return description; + } + + public String getLogMessage() { + return logMessage; + } + } + + public static void modifyClvmVolumesStateForMigration(List disks, VirtualMachineTO vmSpec, ClvmVolumeState state) { + for (DiskDef disk : disks) { + if (isClvmVolume(disk, vmSpec)) { + String volumePath = disk.getDiskPath(); + try { + modifyClvmVolumeState(volumePath, state.getLvchangeFlag(), state.getDescription(), state.getLogMessage()); + } catch (Exception e) { + LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}", + volumePath, state.getDescription(), e.getMessage(), e); + } + } + } + } + + private static void modifyClvmVolumeState(String volumePath, String lvchangeFlag, + String stateDescription, String logMessage) { + try { + LOGGER.info("{} for volume [{}]", logMessage, volumePath); + + Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); + cmd.add(lvchangeFlag); + cmd.add(volumePath); + + String result = cmd.execute(); + if (result != null) { + String errorMsg = String.format( + "Failed to set volume [%s] to %s state. Command result: %s", + volumePath, stateDescription, result); + LOGGER.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } else { + LOGGER.info("Successfully set volume [{}] to {} state.", + volumePath, stateDescription); + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = String.format( + "Exception while setting volume [%s] to %s state: %s", + volumePath, stateDescription, e.getMessage()); + LOGGER.error(errorMsg, e); + throw new CloudRuntimeException(errorMsg, e); + } + } + + public static void activateClvmVolumeExclusive(String volumePath) { + modifyClvmVolumeState(volumePath, ClvmVolumeState.EXCLUSIVE.getLvchangeFlag(), + ClvmVolumeState.EXCLUSIVE.getDescription(), + "Activating CLVM volume in exclusive mode"); + } + + public static void deactivateClvmVolume(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.DEACTIVATE.getLvchangeFlag(), + ClvmVolumeState.DEACTIVATE.getDescription(), + "Deactivating CLVM volume"); + } catch (Exception e) { + LOGGER.warn("Failed to deactivate CLVM volume {}: {}", volumePath, e.getMessage()); + } + } + + public static void setClvmVolumeToSharedMode(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.SHARED.getLvchangeFlag(), + ClvmVolumeState.SHARED.getDescription(), + "Setting CLVM volume to shared mode"); + } catch (Exception e) { + LOGGER.warn("Failed to set CLVM volume {} to shared mode: {}", volumePath, e.getMessage()); + } + } + + /** + * Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO. + * This is the most reliable method as it uses CloudStack's own storage pool information. + * + * @param disk The disk definition to check + * @param resource The LibvirtComputingResource instance (unused but kept for compatibility) + * @param vmSpec The VirtualMachineTO specification containing disk and pool information + * @return true if the disk is on a CLVM storage pool, false otherwise + */ + private static boolean isClvmVolume(DiskDef disk, VirtualMachineTO vmSpec) { + String diskPath = disk.getDiskPath(); + if (diskPath == null || vmSpec == null) { + return false; + } + + try { + if (vmSpec.getDisks() != null) { + for (DiskTO diskTO : vmSpec.getDisks()) { + if (!(diskTO.getData() instanceof VolumeObjectTO)) { + continue; + } + VolumeObjectTO volumeTO = (VolumeObjectTO) diskTO.getData(); + if (!diskPath.equals(volumeTO.getPath()) && !diskPath.equals(diskTO.getPath())) { + continue; + } + DataStoreTO dataStore = volumeTO.getDataStore(); + if (!(dataStore instanceof PrimaryDataStoreTO)) { + continue; + } + PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore; + boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType() || + StoragePoolType.CLVM_NG == primaryStore.getPoolType(); + LOGGER.debug("Disk {} identified as CLVM/CLVM_NG={} via VirtualMachineTO pool type: {}", + diskPath, isClvm, primaryStore.getPoolType()); + return isClvm; + } + } + + if (diskPath.startsWith("/dev/") && !diskPath.contains("/dev/mapper/")) { + String vgName = extractVolumeGroupFromPath(diskPath); + if (vgName != null) { + boolean isClustered = checkIfVolumeGroupIsClustered(vgName); + LOGGER.debug("Disk {} VG {} identified as clustered={} via vgs attribute check", + diskPath, vgName, isClustered); + return isClustered; + } + } + + } catch (Exception e) { + LOGGER.error("Error determining if volume {} is CLVM: {}", diskPath, e.getMessage(), e); + } + + return false; + } + + /** + * Extracts the volume group name from a device path. + * + * @param devicePath The device path (e.g., /dev/vgname/lvname) + * @return The volume group name, or null if cannot be determined + */ + static String extractVolumeGroupFromPath(String devicePath) { + if (devicePath == null || !devicePath.startsWith("/dev/")) { + return null; + } + + // Format: /dev// + String[] parts = devicePath.split("/"); + if (parts.length >= 3) { + return parts[2]; // ["", "dev", "vgname", ...] + } + + return null; + } + + /** + * Checks if a volume group is clustered (CLVM) by examining its attributes. + * Uses 'vgs' command to check for the clustered/shared flag in VG attributes. + * + * VG Attr format (6 characters): wz--nc or wz--ns + * Position 6: Clustered flag - 'c' = CLVM (clustered), 's' = shared (lvmlockd), '-' = not clustered + * + * @param vgName The volume group name + * @return true if the VG is clustered or shared, false otherwise + */ + static boolean checkIfVolumeGroupIsClustered(String vgName) { + if (vgName == null) { + return false; + } + + try { + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + Script vgsCmd = new Script("vgs", 30000, LOGGER); + vgsCmd.add("--noheadings"); + vgsCmd.add("--unbuffered"); + vgsCmd.add("-o"); + vgsCmd.add("vg_attr"); + vgsCmd.add(vgName); + + String result = vgsCmd.execute(parser); + + if (result == null && parser.getLines() != null) { + String output = parser.getLines(); + if (output != null && !output.isEmpty()) { + String vgAttr = output.trim(); + if (vgAttr.length() >= 6) { + char clusterFlag = vgAttr.charAt(5); // Position 6 (0-indexed 5) + boolean isClustered = (clusterFlag == 'c' || clusterFlag == 's'); + LOGGER.debug("VG {} has attributes '{}', cluster/shared flag '{}' = {}", + vgName, vgAttr, clusterFlag, isClustered); + return isClustered; + } else { + LOGGER.warn("VG {} attributes '{}' have unexpected format (expected 6+ chars)", vgName, vgAttr); + } + } + } else { + LOGGER.warn("Failed to get VG attributes for {}: {}", vgName, result); + } + + } catch (Exception e) { + LOGGER.debug("Error checking if VG {} is clustered: {}", vgName, e.getMessage()); + } + + return false; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java new file mode 100644 index 000000000000..18c4324addf5 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapper.java @@ -0,0 +1,173 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferAnswer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; +import com.cloud.utils.script.OutputInterpreter; + +@ResourceWrapper(handles = ClvmLockTransferCommand.class) +public class LibvirtClvmLockTransferCommandWrapper + extends CommandWrapper { + + @Override + public Answer execute(ClvmLockTransferCommand cmd, LibvirtComputingResource serverResource) { + String lvPath = cmd.getLvPath(); + ClvmLockTransferCommand.Operation operation = cmd.getOperation(); + String volumeUuid = cmd.getVolumeUuid(); + + logger.info("Executing CLVM lock transfer: operation={}, lv={}, volume={}", + operation, lvPath, volumeUuid); + + try { + + if (operation == ClvmLockTransferCommand.Operation.QUERY_LOCK_STATE) { + return handleQueryLockState(cmd, lvPath, volumeUuid); + } + + String lvchangeOpt; + String operationDesc; + switch (operation) { + case DEACTIVATE: + lvchangeOpt = "-an"; + operationDesc = "deactivated"; + break; + case ACTIVATE_EXCLUSIVE: + lvchangeOpt = "-aey"; + operationDesc = "activated exclusively"; + break; + case ACTIVATE_SHARED: + lvchangeOpt = "-asy"; + operationDesc = "activated in shared mode"; + break; + default: + return new ClvmLockTransferAnswer(cmd, false, "Unknown operation: " + operation); + } + + Script script = new Script("/usr/sbin/lvchange", 60000, logger); + script.add(lvchangeOpt); + script.add(lvPath); + + String result = script.execute(); + + if (result != null) { + logger.error("CLVM lock transfer failed for volume {}: {}", + volumeUuid, result); + return new ClvmLockTransferAnswer(cmd, false, + String.format("lvchange %s %s failed: %s", lvchangeOpt, lvPath, result)); + } + + logger.info("Successfully executed CLVM lock transfer: {} {} for volume {}", + lvchangeOpt, lvPath, volumeUuid); + + return new ClvmLockTransferAnswer(cmd, true, + String.format("Successfully %s CLVM volume %s", operationDesc, volumeUuid)); + + } catch (Exception e) { + logger.error("Exception during CLVM lock transfer for volume {}: {}", + volumeUuid, e.getMessage(), e); + return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage()); + } + } + + /** + * Query whether this host currently has the CLVM LV activated locally. + * Executes: lvs -o lv_attr,lv_host,lv_active --noheadings + * + * lv_attr[4]=='a' (isActive) is LOCAL and is the authoritative signal — true only on + * the host where the LV is currently activated. The management server fans out this + * query to all cluster hosts; the one returning isActive=true is the lock holder. + * lv_attr[5]=='o' (isOpen) means a VM has the device open on this host (doing I/O). + * lv_host is retained for diagnostic logging only — do NOT use it to identify the + * lock holder. + */ + private Answer handleQueryLockState(ClvmLockTransferCommand cmd, String lvPath, String volumeUuid) { + try { + Script script = new Script("/usr/sbin/lvs", 30000, logger); + script.add("-o"); + script.add("lv_attr,lv_host"); + script.add("--noheadings"); + script.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = script.execute(parser); + + if (result != null) { + logger.error("Failed to query lock state for volume {}: {}", volumeUuid, result); + return new ClvmLockTransferAnswer(cmd, false, + String.format("lvs command failed: %s", result)); + } + + String[] lines = parser.getLines().split("\n"); + String dataLine = null; + + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && + trimmed.length() >= 10 && + "-wrsvmpco".indexOf(trimmed.charAt(0)) >= 0) { + dataLine = trimmed; + break; + } + } + + if (dataLine == null) { + String allOutput = parser.getLines(); + logger.warn("Could not find lv_attr data line in lvs output for volume {}: {}", + volumeUuid, allOutput); + return new ClvmLockTransferAnswer(cmd, false, + String.format("Could not parse lvs output. Full output: %s", allOutput)); + } + + logger.debug("Parsed lv_attr data line for volume {}: {}", volumeUuid, dataLine); + + String[] parts = dataLine.split("\\s+"); + if (parts.length < 1) { + return new ClvmLockTransferAnswer(cmd, false, "Invalid lvs output format"); + } + + String lvAttr = parts[0]; + // lv_host: for diagnostics only, unreliable for lock-holder identification + String hostname = parts.length > 1 ? parts[1] : null; + + // lv_attr[4]=='a' → LV is active on THIS host (local activation state) + boolean isActive = lvAttr.length() > 4 && lvAttr.charAt(4) == 'a'; + // lv_attr[5]=='o' → a process has the device file open on this host (VM doing I/O) + boolean isOpen = lvAttr.length() > 5 && lvAttr.charAt(5) == 'o'; + + logger.info("Queried lock state for volume {}: attr={}, hostname={}, active={}, open={}", + volumeUuid, lvAttr, hostname, isActive, isOpen); + + return new ClvmLockTransferAnswer(cmd, true, + String.format("Lock state: active=%s, open=%s, host=%s", + isActive, isOpen, hostname != null ? hostname : "none"), + hostname, isActive, isOpen, lvAttr); + + } catch (Exception e) { + logger.error("Exception during lock state query for volume {}: {}", + volumeUuid, e.getMessage(), e); + return new ClvmLockTransferAnswer(cmd, false, "Exception: " + e.getMessage()); + } + } + +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java new file mode 100644 index 000000000000..7cf05da9b211 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCreateImageTransferCommandWrapper.java @@ -0,0 +1,178 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.backup.CreateImageTransferAnswer; +import org.apache.cloudstack.backup.CreateImageTransferCommand; +import org.apache.cloudstack.backup.ImageTransfer; +import org.apache.cloudstack.storage.resource.IpTablesHelper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.ImageServerControlSocket; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = CreateImageTransferCommand.class) +public class LibvirtCreateImageTransferCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private static final String IMAGE_SERVER_TLS_CERT_FILE = "/etc/cloudstack/agent/cloud.crt"; + private static final String IMAGE_SERVER_TLS_KEY_FILE = "/etc/cloudstack/agent/cloud.key"; + + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + private static String shellQuote(String value) { + return "'" + value.replace("'", "'\\''") + "'"; + } + + private boolean startImageServerIfNotRunning(int imageServerPort, String listenAddress, LibvirtComputingResource resource) { + final String imageServerPackageDir = resource.getImageServerPath(); + final String imageServerParentDir = new File(imageServerPackageDir).getParent(); + final String imageServerModuleName = new File(imageServerPackageDir).getName(); + final boolean tlsEnabled = resource.isImageServerTlsEnabled(); + String unitName = resource.IMAGE_SERVER_SYSTEMD_UNIT_NAME; + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult == null && ImageServerControlSocket.isReady()) { + return true; + } + + resetService(unitName); + if (checkResult != null) { + StringBuilder systemdRunCmd = new StringBuilder(String.format( + "systemd-run --unit=%s --property=Restart=no --property=WorkingDirectory=%s /usr/bin/python3 -m %s --listen %s --port %d", + unitName, shellQuote(imageServerParentDir), imageServerModuleName, shellQuote(listenAddress), imageServerPort)); + + if (tlsEnabled) { + systemdRunCmd.append(" --tls-enabled"); + systemdRunCmd.append(" --tls-cert-file ").append(IMAGE_SERVER_TLS_CERT_FILE); + systemdRunCmd.append(" --tls-key-file ").append(IMAGE_SERVER_TLS_KEY_FILE); + } + + Script startScript = new Script("/bin/bash", logger); + startScript.add("-c"); + startScript.add(systemdRunCmd.toString()); + String startResult = startScript.execute(); + + if (startResult != null) { + logger.error(String.format("Failed to start the Image server: %s", startResult)); + return false; + } + } + + int maxWaitSeconds = 10; + int pollIntervalMs = 1000; + int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs; + boolean serverReady = false; + + for (int attempt = 0; attempt < maxAttempts; attempt++) { + if (ImageServerControlSocket.isReady()) { + serverReady = true; + logger.info(String.format("Image server control socket is ready (attempt %d)", attempt + 1)); + break; + } + try { + Thread.sleep(pollIntervalMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + if (!serverReady) { + logger.error(String.format("Image server control socket not ready within %d seconds", maxWaitSeconds)); + return false; + } + + String rule = String.format("-p tcp -m state --state NEW -m tcp --dport %d -j ACCEPT", imageServerPort); + IpTablesHelper.addConditionally(IpTablesHelper.INPUT_CHAIN, true, rule, + String.format("Error in opening up image server port %d", imageServerPort)); + + return true; + } + + public Answer execute(CreateImageTransferCommand cmd, LibvirtComputingResource resource) { + final String transferId = cmd.getTransferId(); + ImageTransfer.Backend backend = cmd.getBackend(); + + if (StringUtils.isBlank(transferId)) { + return new CreateImageTransferAnswer(cmd, false, "transferId is empty."); + } + + final Map payload = new HashMap<>(); + payload.put("backend", backend.toString()); + payload.put("idle_timeout_seconds", cmd.getIdleTimeoutSeconds()); + + if (backend == ImageTransfer.Backend.file) { + final String filePath = cmd.getFile(); + if (StringUtils.isBlank(filePath)) { + return new CreateImageTransferAnswer(cmd, false, "file path is empty for file backend."); + } + payload.put("file", filePath); + } else { + String socket = cmd.getSocket(); + final String exportName = cmd.getExportName(); + if (StringUtils.isBlank(socket)) { + return new CreateImageTransferAnswer(cmd, false, "Empty socket."); + } + if (StringUtils.isBlank(exportName)) { + return new CreateImageTransferAnswer(cmd, false, "exportName is empty."); + } + payload.put("socket", "/tmp/imagetransfer/" + socket + ".sock"); + payload.put("export", exportName); + String checkpointId = cmd.getCheckpointId(); + if (checkpointId != null) { + payload.put("export_bitmap", cmd.getCheckpointId()); + } + } + + final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT; + String listenAddress = resource.getImageServerListenAddress(); + if (StringUtils.isBlank(listenAddress)) { + listenAddress = resource.getPrivateIp(); + } + if (!startImageServerIfNotRunning(imageServerPort, listenAddress, resource)) { + return new CreateImageTransferAnswer(cmd, false, "Failed to start image server."); + } + + if (!ImageServerControlSocket.registerTransfer(transferId, payload)) { + return new CreateImageTransferAnswer(cmd, false, "Failed to register transfer with image server."); + } + + final String transferScheme = resource.isImageServerTlsEnabled() ? "https" : "http"; + final String transferUrl = String.format("%s://%s:%d/images/%s", transferScheme, listenAddress, imageServerPort, transferId); + return new CreateImageTransferAnswer(cmd, true, "Image transfer prepared on KVM host.", transferId, transferUrl); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java new file mode 100644 index 000000000000..ddb84ab29cb3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java @@ -0,0 +1,79 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.Map; + +import org.apache.cloudstack.backup.DeleteVmCheckpointCommand; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = DeleteVmCheckpointCommand.class) +public class LibvirtDeleteVmCheckpointCommandWrapper extends CommandWrapper { + + @Override + public Answer execute(DeleteVmCheckpointCommand cmd, LibvirtComputingResource resource) { + if (cmd.isStoppedVM()) { + return deleteBitmapsOnDisks(cmd); + } + return deleteDomainCheckpoint(cmd); + } + + private Answer deleteDomainCheckpoint(DeleteVmCheckpointCommand cmd) { + String vmName = cmd.getVmName(); + String checkpointId = cmd.getCheckpointId(); + String virshCmd = String.format("virsh checkpoint-delete %s %s", vmName, checkpointId); + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(virshCmd); + String result = script.execute(); + if (result != null) { + return new Answer(cmd, false, "Failed to delete checkpoint: " + result); + } + return new Answer(cmd, true, "Checkpoint deleted"); + } + + /** + * Stopped VM: persistent bitmaps on disk images ({@code qemu-img bitmap --remove}), matching {@link LibvirtStartBackupCommandWrapper} bitmap --add. + */ + private Answer deleteBitmapsOnDisks(DeleteVmCheckpointCommand cmd) { + String checkpointId = cmd.getCheckpointId(); + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + if (diskPathUuidMap == null || diskPathUuidMap.isEmpty()) { + return new Answer(cmd, false, "No disks provided for bitmap removal"); + } + for (Map.Entry entry : diskPathUuidMap.entrySet()) { + String diskPath = entry.getKey(); + Script script = new Script("qemu-img"); + script.add("bitmap"); + script.add("--remove"); + script.add(diskPath); + script.add(checkpointId); + String result = script.execute(); + if (result != null) { + return new Answer(cmd, false, + "Failed to remove bitmap " + checkpointId + " from disk " + diskPath + ": " + result); + } + } + return new Answer(cmd, true, "Checkpoint bitmap removed from disks"); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java new file mode 100644 index 000000000000..3d9f6563d5eb --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtFinalizeImageTransferCommandWrapper.java @@ -0,0 +1,101 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.FinalizeImageTransferCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.ImageServerControlSocket; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = FinalizeImageTransferCommand.class) +public class LibvirtFinalizeImageTransferCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + private boolean stopImageServer(int imageServerPort, LibvirtComputingResource resource) { + String unitName = resource.IMAGE_SERVER_SYSTEMD_UNIT_NAME; + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult != null) { + logger.info("Image server not running, resetting failed state"); + resetService(unitName); + removeFirewallRule(imageServerPort); + return true; + } + + Script stopScript = new Script("/bin/bash", logger); + stopScript.add("-c"); + stopScript.add(String.format("systemctl stop %s", unitName)); + stopScript.execute(); + resetService(unitName); + logger.info("Image server {} stopped", unitName); + + removeFirewallRule(imageServerPort); + + return true; + } + + private void removeFirewallRule(int port) { + String rule = String.format("-p tcp -m state --state NEW -m tcp --dport %d -j ACCEPT", port); + Script removeScript = new Script("/bin/bash", logger); + removeScript.add("-c"); + removeScript.add(String.format("iptables -D INPUT %s || true", rule)); + String result = removeScript.execute(); + if (result != null && !result.isEmpty() && !result.contains("iptables: Bad rule")) { + logger.debug("Firewall rule removal result for port {}: {}", port, result); + } else { + logger.info("Firewall rule removed for port {} (or did not exist)", port); + } + } + + public Answer execute(FinalizeImageTransferCommand cmd, LibvirtComputingResource resource) { + final String transferId = cmd.getTransferId(); + final int imageServerPort = LibvirtComputingResource.IMAGE_SERVER_DEFAULT_PORT; + if (StringUtils.isBlank(transferId)) { + return new Answer(cmd, false, "transferId is empty."); + } + + int activeTransfers = ImageServerControlSocket.unregisterTransfer(transferId); + if (activeTransfers < 0) { + logger.warn("Could not reach image server to unregister transfer {}; assuming server is down", transferId); + stopImageServer(imageServerPort, resource); + return new Answer(cmd, true, "Image transfer finalized (server unreachable, forced stop)."); + } + + if (activeTransfers == 0) { + stopImageServer(imageServerPort, resource); + } + + return new Answer(cmd, true, "Image transfer finalized."); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java index f54918bbc228..ed02ae6da38d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtMigrateCommandWrapper.java @@ -1,5 +1,3 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file @@ -42,9 +40,16 @@ import javax.xml.transform.TransformerException; import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.hypervisor.kvm.resource.LibvirtGpuDef; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.Ternary; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.collections4.CollectionUtils; @@ -69,7 +74,6 @@ import com.cloud.agent.api.MigrateAnswer; import com.cloud.agent.api.MigrateCommand; import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo; -import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DpdkTO; import com.cloud.agent.api.to.VirtualMachineTO; @@ -82,11 +86,6 @@ import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.InterfaceDef; import com.cloud.hypervisor.kvm.resource.MigrateKVMAsync; import com.cloud.hypervisor.kvm.resource.VifDriver; -import com.cloud.resource.CommandWrapper; -import com.cloud.resource.ResourceWrapper; -import com.cloud.utils.Ternary; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VirtualMachine; @ResourceWrapper(handles = MigrateCommand.class) public final class LibvirtMigrateCommandWrapper extends CommandWrapper { @@ -117,7 +116,8 @@ public Answer execute(final MigrateCommand command, final LibvirtComputingResour Command.State commandState = null; List ifaces = null; - List disks; + List disks = new ArrayList<>(); + VirtualMachineTO to = null; Domain dm = null; Connect dconn = null; @@ -136,7 +136,7 @@ public Answer execute(final MigrateCommand command, final LibvirtComputingResour if (logger.isDebugEnabled()) { logger.debug(String.format("Found domain with name [%s]. Starting VM migration to host [%s].", vmName, destinationUri)); } - VirtualMachineTO to = command.getVirtualMachine(); + to = command.getVirtualMachine(); dm = conn.domainLookupByName(vmName); /* @@ -338,6 +338,14 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. logger.debug(String.format("Cleaning the disks of VM [%s] in the source pool after VM migration finished.", vmName)); } resumeDomainIfPaused(destDomain, vmName); + + // For cross-pool CLVM migration, skip deactivation so the source LV stays + // active (in shared mode) and deletion can route directly to the source host + // without fanning out across the cluster to find an inactive LV. + if (to != null && !command.isClvmCrossPoolMigration()) { + LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.DEACTIVATE); + } + deleteOrDisconnectDisksOnSourcePool(libvirtComputingResource, migrateDiskInfoList, disks); libvirtComputingResource.cleanOldSecretsByDiskDef(conn, disks); } @@ -384,6 +392,10 @@ Use VIR_DOMAIN_XML_SECURE (value = 1) prior to v1.0.0. if (destDomain != null) { destDomain.free(); } + // Revert CLVM volumes to exclusive mode on failure + if (to != null && result != null) { + LibvirtComputingResource.modifyClvmVolumesStateForMigration(disks, to, LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE); + } } catch (final LibvirtException e) { logger.trace("Ignoring libvirt error.", e); } @@ -683,7 +695,7 @@ protected String replaceDpdkInterfaces(String xmlDesc, Map dpdkP protected void deleteOrDisconnectDisksOnSourcePool(final LibvirtComputingResource libvirtComputingResource, final List migrateDiskInfoList, List disks) { for (DiskDef disk : disks) { - MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); + MigrateCommand.MigrateDiskInfo migrateDiskInfo = searchDiskDefOnMigrateDiskInfoList(migrateDiskInfoList, disk); if (migrateDiskInfo != null && migrateDiskInfo.isSourceDiskOnStorageFileSystem()) { deleteLocalVolume(disk.getDiskPath()); } else { @@ -800,7 +812,10 @@ protected String replaceStorage(String xmlDesc, Map]*type=['\"]vnc['\"][^>]*passwd=['\"])([^'\"]*)(['\"])", "$1*****$3"); } + + /** + * Checks if any of the destination disks in the migration target a CLVM or CLVM_NG storage pool. + * This is used to determine if incremental migration should be disabled to avoid libvirt + * precreate errors with QCOW2-on-LVM setups. + * + * @param mapMigrateStorage the map containing migration disk information with destination pool types + * @return true if any destination disk targets CLVM or CLVM_NG, false otherwise + */ + protected boolean hasClvmDestinationDisks(Map mapMigrateStorage) { + if (MapUtils.isEmpty(mapMigrateStorage)) { + return false; + } + + try { + for (Map.Entry entry : mapMigrateStorage.entrySet()) { + MigrateCommand.MigrateDiskInfo diskInfo = entry.getValue(); + if (isClvmBlockDevice(diskInfo)) { + logger.debug("Found disk targeting CLVM/CLVM_NG destination pool"); + return true; + } + } + } catch (final Exception e) { + logger.debug("Failed to check for CLVM destination disks: {}. Assuming no CLVM disks.", e.getMessage()); + } + + return false; + } + + private boolean isClvmBlockDevice(MigrateCommand.MigrateDiskInfo diskInfo) { + if (diskInfo == null ||diskInfo.getDestPoolType() == null) { + return false; + } + return (Storage.StoragePoolType.CLVM.equals(diskInfo.getDestPoolType()) || Storage.StoragePoolType.CLVM_NG.equals(diskInfo.getDestPoolType())); + } + + /** + * Determines if the driver type should be updated during migration based on CLVM involvement. + * The driver type needs to be updated when: + * - Managed storage is being migrated, OR + * - Source pool is CLVM or CLVM_NG, OR + * - Destination pool is CLVM or CLVM_NG + * + * This ensures the libvirt XML driver type matches the destination format (raw/qcow2/etc). + * + * @param migrateStorageManaged true if migrating managed storage + * @param migrateDiskInfo the migration disk information containing source and destination pool types + * @return true if driver type should be updated, false otherwise + */ + private boolean shouldUpdateDriverTypeForMigration(boolean migrateStorageManaged, + MigrateCommand.MigrateDiskInfo migrateDiskInfo) { + boolean sourceIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getSourcePoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getSourcePoolType(); + + boolean destIsClvm = Storage.StoragePoolType.CLVM == migrateDiskInfo.getDestPoolType() || + Storage.StoragePoolType.CLVM_NG == migrateDiskInfo.getDestPoolType(); + + boolean isClvmRelatedMigration = sourceIsClvm || destIsClvm; + return migrateStorageManaged || isClvmRelatedMigration; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java index 990cefda8f33..bc22d7bfd70a 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtModifyStoragePoolCommandWrapper.java @@ -52,9 +52,19 @@ public Answer execute(final ModifyStoragePoolCommand command, final LibvirtCompu final KVMStoragePool storagepool; try { + Map poolDetails = command.getDetails(); + if (poolDetails == null) { + poolDetails = new HashMap<>(); + } + + // Ensure CLVM secure zero-fill setting has a default value if not provided by MS + if (!poolDetails.containsKey(KVMStoragePool.CLVM_SECURE_ZERO_FILL)) { + poolDetails.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + } + storagepool = storagePoolMgr.createStoragePool(command.getPool().getUuid(), command.getPool().getHost(), command.getPool().getPort(), command.getPool().getPath(), command.getPool() - .getUserInfo(), command.getPool().getType(), command.getDetails()); + .getUserInfo(), command.getPool().getType(), poolDetails); if (storagepool == null) { return new Answer(command, false, " Failed to create storage pool"); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java new file mode 100644 index 000000000000..608770974dc1 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPostMigrationCommandWrapper.java @@ -0,0 +1,82 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.PostMigrationAnswer; +import com.cloud.agent.api.PostMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +/** + * Wrapper for PostMigrationCommand on KVM hypervisor. + * Handles post-migration tasks on the destination host after a VM has been successfully migrated. + * Primary responsibility: Convert CLVM volumes from shared mode to exclusive mode on destination. + */ +@ResourceWrapper(handles = PostMigrationCommand.class) +public final class LibvirtPostMigrationCommandWrapper extends CommandWrapper { + + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(final PostMigrationCommand command, final LibvirtComputingResource libvirtComputingResource) { + final VirtualMachineTO vm = command.getVirtualMachine(); + final String vmName = command.getVmName(); + + if (vm == null || vmName == null) { + return new PostMigrationAnswer(command, "VM or VM name is null"); + } + + logger.debug("Executing post-migration tasks for VM {} on destination host", vmName); + + try { + final Connect conn = LibvirtConnection.getConnectionByVmName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.debug("[CLVM Post-Migration] Processing volumes for VM {} to claim exclusive locks on any CLVM volumes", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.EXCLUSIVE + ); + + logger.debug("Successfully completed post-migration tasks for VM {}", vmName); + return new PostMigrationAnswer(command); + + } catch (final LibvirtException e) { + logger.error("Libvirt error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } catch (final Exception e) { + logger.error("Error during post-migration for VM {}: {}", vmName, e.getMessage(), e); + return new PostMigrationAnswer(command, e); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java new file mode 100644 index 000000000000..c47760040c88 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPreMigrationCommandWrapper.java @@ -0,0 +1,84 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.PreMigrationCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import java.util.List; + +/** + * Handles PreMigrationCommand on the source host before live migration. + * Converts CLVM volume locks from exclusive to shared mode so the destination host can access them. + */ +@ResourceWrapper(handles = PreMigrationCommand.class) +public class LibvirtPreMigrationCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(PreMigrationCommand command, LibvirtComputingResource libvirtComputingResource) { + String vmName = command.getVmName(); + VirtualMachineTO vmSpec = command.getVirtualMachine(); + + logger.info("Preparing source host for migration of VM: {}", vmName); + + Connect conn = null; + Domain dm = null; + + try { + LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + dm = conn.domainLookupByName(vmName); + + List disks = libvirtComputingResource.getDisks(conn, vmName); + logger.info("Converting CLVM volumes to shared mode for VM: {}", vmName); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vmSpec, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + + logger.info("Successfully prepared source host for migration of VM: {}", vmName); + return new Answer(command, true, "Source host prepared for migration"); + + } catch (LibvirtException e) { + logger.error("Failed to prepare source host for migration of VM: {}", vmName, e); + return new Answer(command, false, "Failed to prepare source host: " + e.getMessage()); + } finally { + if (dm != null) { + try { + dm.free(); + } catch (LibvirtException e) { + logger.warn("Failed to free domain {}: {}", vmName, e.getMessage()); + } + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java index d9323df4477d..f7ca79127dad 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareForMigrationCommandWrapper.java @@ -21,6 +21,7 @@ import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.cloudstack.storage.configdrive.ConfigDrive; @@ -124,6 +125,19 @@ public Answer execute(final PrepareForMigrationCommand command, final LibvirtCom return new PrepareForMigrationAnswer(command, "failed to connect physical disks to host"); } + // Activate CLVM volumes in shared mode on destination host for live migration + try { + List disks = libvirtComputingResource.getDisks(conn, vm.getName()); + LibvirtComputingResource.modifyClvmVolumesStateForMigration( + disks, + vm, + LibvirtComputingResource.ClvmVolumeState.SHARED + ); + } catch (Exception e) { + logger.warn("Failed to activate CLVM volumes in shared mode on destination for VM {}: {}", + vm.getName(), e.getMessage(), e); + } + logger.info("Successfully prepared destination host for migration of VM {}", vm.getName()); return createPrepareForMigrationAnswer(command, dpdkInterfaceMapping, libvirtComputingResource, vm); } catch (final LibvirtException | CloudRuntimeException | InternalErrorException | URISyntaxException e) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java index f2af46d4cc8a..a43b584dd6d6 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java @@ -113,7 +113,8 @@ public Answer execute(final ResizeVolumeCommand command, final LibvirtComputingR logger.debug("Resizing volume: " + path + ", from: " + toHumanReadableSize(currentSize) + ", to: " + toHumanReadableSize(newSize) + ", type: " + type + ", name: " + vmInstanceName + ", shrinkOk: " + shrinkOk); /* libvirt doesn't support resizing (C)LVM devices, and corrupts QCOW2 in some scenarios, so we have to do these via qemu-img */ - if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex + if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG + && pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex && vol.getFormat() != PhysicalDiskFormat.QCOW2) { logger.debug("Volume " + path + " can be resized by libvirt. Asking libvirt to resize the volume."); try { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRevertSnapshotCommandWrapper.java index 5d76d140f229..16c1a5a2fac1 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 @@ -117,7 +117,7 @@ public Answer execute(final RevertSnapshotCommand command, final LibvirtComputin secondaryStoragePool = storagePoolMgr.getStoragePoolByURI(snapshotImageStore.getUrl()); } - if (primaryPool.getType() == StoragePoolType.CLVM) { + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { Script cmd = new Script(libvirtComputingResource.manageSnapshotPath(), libvirtComputingResource.getCmdsTimeout(), logger); cmd.add("-v", getFullPathAccordingToStorage(secondaryStoragePool, snapshotRelPath)); cmd.add("-n", snapshotDisk.getName()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java new file mode 100644 index 000000000000..629fd179afb2 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartBackupCommandWrapper.java @@ -0,0 +1,300 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.backup.StartBackupAnswer; +import org.apache.cloudstack.backup.StartBackupCommand; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.cloudstack.utils.qemu.QemuCommand; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.json.JSONArray; +import org.json.JSONObject; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StartBackupCommand.class) +public class LibvirtStartBackupCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(StartBackupCommand cmd, LibvirtComputingResource resource) { + if (cmd.isStoppedVM()) { + return handleStoppedVmBackup(cmd, cmd.getToCheckpointId()); + } + return handleRunningVmBackup(cmd, resource); + } + + public Answer handleRunningVmBackup(StartBackupCommand cmd, LibvirtComputingResource resource) { + String vmName = cmd.getVmName(); + String toCheckpointId = cmd.getToCheckpointId(); + String fromCheckpointId = cmd.getFromCheckpointId(); + Long fromCheckpointCreateTime = cmd.getFromCheckpointCreateTime(); + String socket = cmd.getSocket(); + + try { + if (StringUtils.isNotBlank(fromCheckpointId)) { + Answer redefineAnswer = ensureFromCheckpointExists(cmd, fromCheckpointId, fromCheckpointCreateTime); + if (redefineAnswer != null) { + return redefineAnswer; + } + } + + File dir = new File("/tmp/imagetransfer"); + if (!dir.exists()) { + dir.mkdirs(); + } + + // Create backup XML + String backupXml = createBackupXml(cmd, fromCheckpointId, socket, resource); + String checkpointXml = createCheckpointXml(toCheckpointId); + + // Write XMLs to temp files + File backupXmlFile = File.createTempFile("backup-", ".xml"); + File checkpointXmlFile = File.createTempFile("checkpoint-", ".xml"); + + try (FileWriter writer = new FileWriter(backupXmlFile)) { + writer.write(backupXml); + } + try (FileWriter writer = new FileWriter(checkpointXmlFile)) { + writer.write(checkpointXml); + } + + // Execute virsh backup-begin + String backupCmd = String.format("virsh backup-begin %s %s --checkpointxml %s", + vmName, backupXmlFile.getAbsolutePath(), checkpointXmlFile.getAbsolutePath()); + + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(backupCmd); + String result = script.execute(); + + backupXmlFile.delete(); + checkpointXmlFile.delete(); + + if (result != null) { + return new StartBackupAnswer(cmd, false, "Backup begin failed: " + result); + } + + long checkpointCreateTime = getCheckpointCreateTime(); + return new StartBackupAnswer(cmd, true, "Backup started successfully", checkpointCreateTime); + + } catch (Exception e) { + return new StartBackupAnswer(cmd, false, "Error starting backup: " + e.getMessage()); + } + } + + private Answer ensureFromCheckpointExists(StartBackupCommand cmd, String fromCheckpointId, Long fromCheckpointCreateTime) { + String vmName = cmd.getVmName(); + Script dumpScript = new Script("/bin/bash"); + dumpScript.add("-c"); + dumpScript.add(String.format("virsh checkpoint-dumpxml --domain %s --checkpointname %s --no-domain", + vmName, fromCheckpointId)); + if (dumpScript.execute() == null) { + return null; + } + if (fromCheckpointCreateTime == null) { + return new StartBackupAnswer(cmd, false, "From checkpoint create time is null for checkpoint " + fromCheckpointId); + } + + String redefineXml = createCheckpointXmlForRedefine(fromCheckpointId, fromCheckpointCreateTime); + File redefineFile; + try { + redefineFile = File.createTempFile("checkpoint-redefine-", ".xml"); + } catch (Exception e) { + return new StartBackupAnswer(cmd, false, "Failed to create temp file for checkpoint redefine: " + e.getMessage()); + } + try (FileWriter writer = new FileWriter(redefineFile)) { + writer.write(redefineXml); + } catch (Exception e) { + redefineFile.delete(); + return new StartBackupAnswer(cmd, false, "Failed to write checkpoint redefine XML: " + e.getMessage()); + } + String createCmd = String.format(LibvirtComputingResource.CHECKPOINT_CREATE_COMMAND, vmName, redefineFile.getAbsolutePath()); + Script createScript = new Script("/bin/bash"); + createScript.add("-c"); + createScript.add(createCmd); + String result = createScript.execute(); + redefineFile.delete(); + if (result != null) { + return new StartBackupAnswer(cmd, false, "Failed to redefine from-checkpoint " + fromCheckpointId + ": " + result); + } + return null; + } + + private String createCheckpointXmlForRedefine(String checkpointName, Long createTime) { + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + xml.append(" ").append(checkpointName).append("\n"); + xml.append(" ").append(createTime).append("\n"); + xml.append(""); + return xml.toString(); + } + + private String createBackupXml(StartBackupCommand cmd, String fromCheckpointId, String socket, LibvirtComputingResource resource) throws LibvirtException { + StringBuilder xml = new StringBuilder(); + xml.append("\n"); + + xml.append(String.format(" \n", socket)); + + xml.append(" \n"); + + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + Map diskPathLabelMap = resource.getDiskPathLabelMap(cmd.getVmName()); + Map diskPathHasFromCheckpointMap = new HashMap<>(); + if (StringUtils.isNotBlank(fromCheckpointId)) { + Domain vm = null; + try { + vm = resource.getDomain(resource.getLibvirtUtilitiesHelper().getConnection(), cmd.getVmName()); + if (vm != null) { + diskPathHasFromCheckpointMap = getVmDiskPathHasFromCheckpointMap(vm, fromCheckpointId); + } else { + logger.warn("Failed to get domain for VM [{}] while evaluating export bitmap [{}]. Falling back to full Backup", + cmd.getVmName(), fromCheckpointId); + } + } finally { + if (vm != null) { + vm.free(); + } + } + } + + for (Map.Entry entry : diskPathLabelMap.entrySet()) { + String diskPath = entry.getKey(); + if (!diskPathUuidMap.containsKey(diskPath)) { + continue; + } + String diskName = entry.getValue(); + String export = diskPathUuidMap.get(diskPath); + String scratchFile = "/var/tmp/scratch-" + export + ".qcow2"; + xml.append(" \n"); + xml.append(" \n"); + xml.append(" \n"); + } + + xml.append(" \n"); + xml.append(""); + + return xml.toString(); + } + + private String createCheckpointXml(String checkpointId) { + return "\n" + + " " + checkpointId + "\n" + + ""; + } + + private Answer handleStoppedVmBackup(StartBackupCommand cmd, String toCheckpointId) { + Map diskPathUuidMap = cmd.getDiskPathUuidMap(); + Map diskPathPassphraseMap = cmd.getDiskPathPassphraseMap(); + for (Map.Entry entry : diskPathUuidMap.entrySet()) { + String diskPath = entry.getKey(); + Script script = new Script("qemu-img"); + byte[] passphrase = diskPathPassphraseMap.get(diskPath); + + script.add("bitmap"); + script.add("--add"); + + if (passphrase != null && passphrase.length > 0) { + KeyFile srcKey; + try { + srcKey = new KeyFile(passphrase); + } catch (IOException ex) { + return new StartBackupAnswer(cmd, false, "Failed to create KeyFile while adding bitmap " + toCheckpointId + " to disk " + diskPath); + } + script.add("--object"); + script.add(String.format("secret,id=sec0,file=%s", srcKey)); + script.add("--image-opts"); + script.add(String.format("driver=qcow2,file.driver=file,file.filename=%s,encrypt.key-secret=sec0", diskPath)); + } else { + script.add(diskPath); + } + + script.add(toCheckpointId); + String result = script.execute(); + if (result != null) { + return new StartBackupAnswer(cmd, false, + "Failed to add bitmap " + toCheckpointId + " to disk " + diskPath + ": " + result); + } + } + long checkpointCreateTime = getCheckpointCreateTime(); + return new StartBackupAnswer(cmd, true, "Stopped VM backup: checkpoint bitmap added successfully", + checkpointCreateTime); + } + + private long getCheckpointCreateTime() { + return System.currentTimeMillis() / 1000; + } + + private Map getVmDiskPathHasFromCheckpointMap(Domain vm, String fromCheckpointId) throws LibvirtException { + Map diskPathHasFromCheckpointMap = new HashMap<>(); + String queryBlock = vm.qemuMonitorCommand(QemuCommand.buildQemuCommand("query-block", null), 0); + JSONObject response = new JSONObject(queryBlock); + JSONArray blocks = response.optJSONArray("return"); + if (blocks == null) { + logger.warn("Couldn't get bitmap information for the VM [{}]. Falling back to full Backup", vm.getName()); + return diskPathHasFromCheckpointMap; + } + for (int i = 0; i < blocks.length(); i++) { + JSONObject block = blocks.getJSONObject(i); + JSONObject inserted = block.optJSONObject("inserted"); + if (inserted == null) { + continue; + } + String file = inserted.optString("file"); + if (StringUtils.isBlank(file)) { + continue; + } + JSONArray dirtyBitmaps = inserted.optJSONArray("dirty-bitmaps"); + boolean hasFromCheckpointBitmap = false; + if (dirtyBitmaps != null) { + for (int j = 0; j < dirtyBitmaps.length(); j++) { + JSONObject dirtyBitmap = dirtyBitmaps.optJSONObject(j); + if (dirtyBitmap == null) { + continue; + } + String bitmapName = dirtyBitmap.optString("name"); + if (fromCheckpointId.equals(bitmapName)) { + hasFromCheckpointBitmap = true; + break; + } + } + } + diskPathHasFromCheckpointMap.put(file, hasFromCheckpointBitmap); + } + return diskPathHasFromCheckpointMap; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java new file mode 100644 index 000000000000..661344677640 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartNBDServerCommandWrapper.java @@ -0,0 +1,197 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import java.io.File; +import java.io.IOException; + +import org.apache.cloudstack.backup.StartNBDServerAnswer; +import org.apache.cloudstack.backup.StartNBDServerCommand; +import org.apache.cloudstack.utils.cryptsetup.KeyFile; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.json.JSONArray; +import org.json.JSONObject; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.StringUtils; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StartNBDServerCommand.class) +public class LibvirtStartNBDServerCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private String createSystemdRunCmd(StartNBDServerCommand cmd, String unitName, String volumePath, String exportName, String socket) throws IOException { + String socketName = "/tmp/imagetransfer/" + socket + ".sock"; + + String bitmapArg = ""; + if (StringUtils.isNotBlank(cmd.getFromCheckpointId()) + && isBitmapPresentOnDisk(volumePath, cmd.getFromCheckpointId())) { + bitmapArg = "-B " + cmd.getFromCheckpointId(); + } + + byte[] passphrase = cmd.getPassphrase(); // may be null + String readOnlyArg = cmd.getDirection().equals("download") ? "--read-only" : ""; + String imageArg = volumePath; + String secretArg = ""; + if (passphrase != null && passphrase.length > 0) { + KeyFile srcKey = new KeyFile(passphrase); + secretArg = String.format("--object secret,id=sec0,file=%s", srcKey); + imageArg = String.format("--image-opts driver=qcow2,file.driver=file,file.filename=%s,encrypt.key-secret=sec0", volumePath); + } + + // --persistent: Don't stop the service when the last client disconnects. + // --shared=NUM: Allow up to NUM clients to share the device (default 1), 0 for unlimited. Number of parallel connections is managed by the image server. + String systemdRunCmd = String.format( + "systemd-run --unit=%s --property=Restart=no qemu-nbd %s " + + "--export-name %s --socket %s --persistent --shared=0 %s %s %s", + unitName, + secretArg, + exportName, + socketName, + bitmapArg, + readOnlyArg, + imageArg + ); + return systemdRunCmd; + } + + @Override + public Answer execute(StartNBDServerCommand cmd, LibvirtComputingResource resource) { + String volumePath = cmd.getVolumePath(); + String socket = cmd.getSocket(); + String exportName = cmd.getExportName(); + String transferId = cmd.getTransferId(); + + if (StringUtils.isBlank(volumePath)) { + return new StartNBDServerAnswer(cmd, false, "Volume path is required for the nbd server"); + } + if (StringUtils.isBlank(exportName)) { + return new StartNBDServerAnswer(cmd, false, "Export name is required for the nbd server"); + } + if (StringUtils.isBlank(socket)) { + return new StartNBDServerAnswer(cmd, false, "Socket is required for the nbd server"); + } + + String unitName = "qemu-nbd-" + transferId.hashCode(); + + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult == null) { + return new StartNBDServerAnswer(cmd, false, "A qemu-nbd service is already running on the port."); + } + + File dir = new File("/tmp/imagetransfer"); + if (!dir.exists()) { + dir.mkdirs(); + } + + String systemdRunCmd = ""; + try { + systemdRunCmd = createSystemdRunCmd(cmd, unitName, volumePath, exportName, socket); + } catch (IOException e) { + logger.error("Failed to create the KeyFile for qemu-nbd service", e); + return new StartNBDServerAnswer(cmd, false, "Failed to create systemd run command for qemu-nbd service: " + e.getMessage()); + } + + Script startScript = new Script("/bin/bash", logger); + startScript.add("-c"); + startScript.add(systemdRunCmd); + String startResult = startScript.execute(); + + if (startResult != null) { + logger.error(String.format("Failed to start qemu-nbd service: %s", startResult)); + return new StartNBDServerAnswer(cmd, false, "Failed to start qemu-nbd service: " + startResult); + } + + // Wait with timeout until the service is up + int maxWaitSeconds = 20; + int pollIntervalMs = 5000; + int maxAttempts = (maxWaitSeconds * 1000) / pollIntervalMs; + boolean serviceActive = false; + + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + Thread.sleep(pollIntervalMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new StartNBDServerAnswer(cmd, false, "Interrupted while waiting for qemu-nbd service to start"); + } + Script verifyScript = new Script("/bin/bash", logger); + verifyScript.add("-c"); + verifyScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String verifyResult = verifyScript.execute(); + if (verifyResult == null) { + serviceActive = true; + logger.info(String.format("qemu-nbd service %s is now active (attempt %d)", unitName, attempt + 1)); + break; + } + } + + if (!serviceActive) { + logger.error(String.format("qemu-nbd service %s failed to become active within %d seconds", unitName, maxWaitSeconds)); + return new StartNBDServerAnswer(cmd, false, + String.format("qemu-nbd service failed to start within %d seconds", maxWaitSeconds)); + } + + String transferUrl = String.format("nbd+unix:///%s", cmd.getSocket()); + return new StartNBDServerAnswer(cmd, true, "qemu-nbd service started for upload", + transferId, transferUrl); + } + + private boolean isBitmapPresentOnDisk(String volumePath, String fromCheckpointId) { + String qemuImgInfo = Script.runBashScriptIgnoreExitValue( + String.format("qemu-img info --output=json %s", volumePath), 0); + if (StringUtils.isBlank(qemuImgInfo)) { + logger.warn("Unable to read qemu-img info output for disk path [{}].", volumePath); + return false; + } + try { + JSONObject info = new JSONObject(qemuImgInfo); + JSONObject formatSpecific = info.optJSONObject("format-specific"); + if (formatSpecific == null) { + return false; + } + JSONObject formatData = formatSpecific.optJSONObject("data"); + if (formatData == null) { + return false; + } + JSONArray bitmaps = formatData.optJSONArray("bitmaps"); + if (bitmaps == null) { + return false; + } + for (int i = 0; i < bitmaps.length(); i++) { + JSONObject bitmap = bitmaps.optJSONObject(i); + if (bitmap == null) { + continue; + } + if (fromCheckpointId.equals(bitmap.optString("name"))) { + return true; + } + } + } catch (Exception e) { + logger.warn("Failed to parse qemu-img info output for disk path [{}].", volumePath, e); + } + return false; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java new file mode 100644 index 000000000000..1185d89bc0b3 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopBackupCommandWrapper.java @@ -0,0 +1,69 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.StopBackupAnswer; +import org.apache.cloudstack.backup.StopBackupCommand; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.libvirt.Connect; +import org.libvirt.Domain; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StopBackupCommand.class) +public class LibvirtStopBackupCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + @Override + public Answer execute(StopBackupCommand cmd, LibvirtComputingResource resource) { + String vmName = cmd.getVmName(); + + try { + Connect conn = LibvirtConnection.getConnection(); + Domain dm = conn.domainLookupByName(vmName); + + if (dm == null) { + return new StopBackupAnswer(cmd, false, "Domain not found: " + vmName); + } + + // Execute virsh domjobabort + String abortCmd = String.format("virsh domjobabort %s", vmName); + + Script script = new Script("/bin/bash"); + script.add("-c"); + script.add(abortCmd); + String result = script.execute(); + + if (result != null && !result.isEmpty()) { + // Job abort may fail if no job is running, which is acceptable + logger.debug("domjobabort result: " + result); + } + + return new StopBackupAnswer(cmd, true, "Backup stopped successfully"); + + } catch (Exception e) { + return new StopBackupAnswer(cmd, false, "Error stopping backup: " + e.getMessage()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java new file mode 100644 index 000000000000..57c7ebb706bc --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java @@ -0,0 +1,72 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.backup.StopNBDServerCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.script.Script; + +@ResourceWrapper(handles = StopNBDServerCommand.class) +public class LibvirtStopNBDServerCommandWrapper extends CommandWrapper { + protected Logger logger = LogManager.getLogger(getClass()); + + private void resetService(String unitName) { + Script resetScript = new Script("/bin/bash", logger); + resetScript.add("-c"); + resetScript.add(String.format("systemctl reset-failed %s || true", unitName)); + resetScript.execute(); + } + + @Override + public Answer execute(StopNBDServerCommand cmd, LibvirtComputingResource resource) { + try { + String unitName = "qemu-nbd-" + cmd.getTransferId().hashCode(); + + // Check if the service is running + Script checkScript = new Script("/bin/bash", logger); + checkScript.add("-c"); + checkScript.add(String.format("systemctl is-active --quiet %s", unitName)); + String checkResult = checkScript.execute(); + if (checkResult != null) { + // Service is not running, but still reset-failed to clear any stale state + logger.info(String.format("qemu-nbd service %s is not running, resetting failed state", unitName)); + resetService(unitName); + return new Answer(cmd, true, "Image transfer finalized"); + } + + // Stop the systemd service + Script stopScript = new Script("/bin/bash", logger); + stopScript.add("-c"); + stopScript.add(String.format("systemctl stop %s", unitName)); + stopScript.execute(); + resetService(unitName); + + return new Answer(cmd, true, "Image transfer finalized"); + + } catch (Exception e) { + logger.error("Error finalizing image transfer for upload", e); + return new Answer(cmd, false, "Error finalizing image transfer: " + e.getMessage()); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java new file mode 100644 index 000000000000..e9ec65505e40 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/ClvmStorageAdaptor.java @@ -0,0 +1,1071 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.hypervisor.kvm.storage; + +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef; +import com.cloud.storage.Storage; +import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.StorageLayer; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; +import org.joda.time.Duration; +import org.libvirt.Connect; +import org.libvirt.LibvirtException; +import org.libvirt.StoragePool; +import org.libvirt.StorageVol; + +/** + * Storage adaptor for CLVM and CLVM_NG pool types. + * Extends LibvirtStorageAdaptor and overrides methods with CLVM-specific logic, + * using direct LVM commands instead of libvirt for volume operations. + */ +public class ClvmStorageAdaptor extends LibvirtStorageAdaptor { + + public ClvmStorageAdaptor(StorageLayer storage) { + super(storage); + } + + @Override + public StoragePoolType getStoragePoolType() { + // Registered manually for both CLVM and CLVM_NG in KVMStoragePoolManager + return null; + } + + @Override + public KVMStoragePool createStoragePool(String name, String host, int port, String path, + String userInfo, StoragePoolType type, Map details, boolean isPrimaryStorage) { + logger.info("Attempting to create CLVM/CLVM_NG storage pool {} in libvirt", name); + + Connect conn; + try { + conn = LibvirtConnection.getConnection(); + } catch (LibvirtException e) { + throw new CloudRuntimeException(e.toString()); + } + + StoragePool sp = createCLVMStoragePool(conn, name, host, path); + if (sp == null) { + logger.info("Falling back to virtual CLVM/CLVM_NG pool without libvirt for: {}", name); + return createVirtualClvmPool(name, host, path, type, details); + } + + try { + if (!isPrimaryStorage) { + incStoragePoolRefCount(name); + } + // CLVM/CLVM_NG pools are kept inactive in libvirt; we use direct LVM commands + return getStoragePool(name); + } catch (Exception e) { + decStoragePoolRefCount(name); + throw new CloudRuntimeException("Failed to create CLVM storage pool: " + name, e); + } + } + + @Override + public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { + logger.info("Fetching CLVM/CLVM_NG storage pool {} ", uuid); + try { + Connect conn = LibvirtConnection.getConnection(); + StoragePool storage = conn.storagePoolLookupByUUIDString(uuid); + + LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); + if (spd == null) { + throw new CloudRuntimeException("Unable to parse storage pool definition for pool " + uuid); + } + + // CLVM pools in libvirt are always LOGICAL type + StoragePoolType type = StoragePoolType.CLVM; + + // Do NOT activate the pool — CLVM/CLVM_NG pools stay inactive in libvirt + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); + pool.setLocalPath(spd.getTargetPath()); + + // Always read capacity from LVM directly + String vgName = storage.getName(); + try { + long[] vgStats = getVgStats(vgName); + setPoolCapacityFromVgStats(pool, vgStats, vgName); + } catch (CloudRuntimeException e) { + logger.warn("Failed to get VG stats for CLVM/CLVM_NG pool {}: {}. Using libvirt values (may be 0)", vgName, e.getMessage()); + pool.setCapacity(storage.getInfo().capacity); + pool.setUsed(storage.getInfo().allocation); + pool.setAvailable(storage.getInfo().available); + } + + return pool; + } catch (LibvirtException e) { + logger.debug("CLVM/CLVM_NG pool {} not found in libvirt, creating virtual pool", uuid); + throw new CloudRuntimeException(e.toString(), e); + } + } + + @Override + public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + + // Pool has no libvirt backing - go directly to block device + if (libvirtPool.getPool() == null) { + logger.debug("CLVM/CLVM_NG pool has no libvirt backing, using direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } + + try { + StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + logger.debug("Volume {} not found in libvirt, falling back to CLVM direct access", volumeUuid); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW); + return disk; + } catch (LibvirtException e) { + logger.warn("LibvirtException looking up volume {}: {}", volumeUuid, e.getMessage()); + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); + } + } + + @Override + public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, + PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + logger.info("Creating CLVM/CLVM_NG volume {} in pool {} with size {}", name, pool.getUuid(), toHumanReadableSize(size)); + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + return createClvmNgDiskWithBacking(name, 0, size, null, pool, provisioningType); + } else { + return createClvmVolume(name, size, pool); + } + } + + @Override + public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { + if (isVMMigrate) { + logger.info("Activating CLVM/CLVM_NG volume {} in shared mode for VM migration", name); + Script activateVol = new Script("lvchange", 30000, logger); + activateVol.add("-asy"); + activateVol.add(pool.getLocalPath() + File.separator + name); + String result = activateVol.execute(); + if (result != null) { + logger.error("Failed to activate CLVM/CLVM_NG volume {} in shared mode. Output: {}", name, result); + return false; + } + } + + if (StoragePoolType.CLVM_NG.equals(pool.getType())) { + ensureClvmNgBackingFileAccessible(name, pool); + } + + return true; + } + + @Override + public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, + String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, + long size, KVMStoragePool destPool, int timeout, byte[] passphrase) { + + if (StoragePoolType.CLVM_NG.equals(destPool.getType()) && format == PhysicalDiskFormat.QCOW2) { + logger.info("Creating CLVM_NG volume {} with backing file from template {}", name, template.getName()); + String backingFile = getClvmBackingFile(template, destPool); + return createClvmNgDiskWithBacking(name, timeout, size, backingFile, destPool, provisioningType); + } + + return super.createDiskFromTemplate(template, name, format, provisioningType, size, destPool, timeout, passphrase); + } + + @Override + public void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String lvName = "template-" + templateUuid; + String lvPath = "/dev/" + vgName + "/" + lvName; + + if (lvExists(lvPath)) { + logger.info("Template LV {} already exists in VG {}. Skipping creation.", lvName, vgName); + return; + } + + logger.info("Creating new template LV {} in VG {} for template {}", lvName, vgName, templateUuid); + + long virtualSize = getQcow2VirtualSize(templatePath); + long physicalSize = getQcow2PhysicalSize(templatePath); + long lvSize = virtualSize; + + logger.info("Template source - Physical: {} bytes, Virtual: {} bytes, LV will be: {} bytes", physicalSize, virtualSize, lvSize); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", lvName); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG template: " + result); + } + + Script qemuImgConvert = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImgConvert.add("convert"); + qemuImgConvert.add(templatePath); + qemuImgConvert.add("-O", "qcow2"); + qemuImgConvert.add("-o", "cluster_size=64k,extended_l2=off,preallocation=off"); + qemuImgConvert.add(lvPath); + result = qemuImgConvert.execute(); + + if (result != null) { + removeLvOnFailure(lvPath, timeout); + throw new CloudRuntimeException("Failed to convert template to CLVM_NG volume: " + result); + } + + long actualVirtualSize = getQcow2VirtualSize(lvPath); + + try { + ensureTemplateLvInSharedMode(lvPath, true); + } catch (CloudRuntimeException e) { + logger.error("Failed to activate template LV {} in shared mode. Cleaning up.", lvPath); + removeLvOnFailure(lvPath, timeout); + throw e; + } + + KVMPhysicalDisk templateDisk = new KVMPhysicalDisk(lvPath, lvName, pool); + templateDisk.setFormat(PhysicalDiskFormat.QCOW2); + templateDisk.setVirtualSize(actualVirtualSize); + templateDisk.setSize(lvSize); + } + + private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { + String volgroupPath = "/dev/" + path; + String volgroupName = path; + volgroupName = volgroupName.replaceFirst("^/", ""); + + Script checkVgExists = new Script("vgs", 30000, logger); + checkVgExists.add("--noheadings"); + checkVgExists.add("-o", "vg_name"); + checkVgExists.add(volgroupName); + String vgCheckResult = checkVgExists.execute(); + + if (vgCheckResult != null) { + logger.error("Volume group {} does not exist or is not accessible", volgroupName); + return null; + } + + logger.info("Volume group {} verified, creating libvirt pool definition for CLVM/CLVM_NG", volgroupName); + LibvirtStoragePoolDef poolDef = new LibvirtStoragePoolDef( + LibvirtStoragePoolDef.PoolType.LOGICAL, + volgroupName, + uuid, + null, + volgroupName, + volgroupPath + ); + + try { + StoragePool pool = conn.storagePoolDefineXML(poolDef.toString(), 0); + logger.info("Created libvirt pool definition for CLVM/CLVM_NG VG: {} (pool will remain inactive)", volgroupName); + pool.setAutostart(1); + return pool; + } catch (LibvirtException e) { + logger.warn("Failed to define CLVM/CLVM_NG pool in libvirt: {}", e.getMessage()); + return null; + } + } + + private void setPoolCapacityFromVgStats(LibvirtStoragePool pool, long[] vgStats, String vgName) { + long capacity = vgStats[0]; + long available = vgStats[1]; + long used = capacity - available; + + pool.setCapacity(capacity); + pool.setAvailable(available); + pool.setUsed(used); + + logger.debug("CLVM/CLVM_NG pool {} - Capacity: {}, Used: {}, Available: {}", + vgName, toHumanReadableSize(capacity), toHumanReadableSize(used), toHumanReadableSize(available)); + } + + private long[] getVgStats(String vgName) { + Script getVgStats = new Script("vgs", 30000, logger); + getVgStats.add("--noheadings"); + getVgStats.add("--units", "b"); + getVgStats.add("--nosuffix"); + getVgStats.add("-o", "vg_size,vg_free"); + getVgStats.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = getVgStats.execute(parser); + + if (result != null) { + String errorMsg = "Failed to get statistics for volume group " + vgName + ": " + result; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String output = parser.getLines().trim(); + String[] lines = output.split("\\n"); + String dataLine = null; + + for (String line : lines) { + line = line.trim(); + if (!line.isEmpty() && Character.isDigit(line.charAt(0))) { + dataLine = line; + break; + } + } + + if (dataLine == null) { + String errorMsg = "No valid data line found in vgs output for " + vgName + ": " + output; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + String[] stats = dataLine.split("\\s+"); + + if (stats.length < 2) { + String errorMsg = "Unexpected output from vgs command for " + vgName + ": " + dataLine; + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } + + try { + long capacity = Long.parseLong(stats[0].trim()); + long available = Long.parseLong(stats[1].trim()); + return new long[]{capacity, available}; + } catch (NumberFormatException e) { + String errorMsg = "Failed to parse VG statistics for " + vgName + ": " + e.getMessage(); + logger.error(errorMsg); + throw new CloudRuntimeException(errorMsg, e); + } + } + + private KVMStoragePool createVirtualClvmPool(String uuid, String host, String path, StoragePoolType type, Map details) { + String volgroupName = path.replaceFirst("^/", ""); + String volgroupPath = "/dev/" + volgroupName; + + logger.info("Creating virtual CLVM/CLVM_NG pool {} without libvirt using direct LVM access", volgroupName); + + long[] vgStats = getVgStats(volgroupName); + + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, volgroupName, type, this, null); + pool.setLocalPath(volgroupPath); + setPoolCapacityFromVgStats(pool, vgStats, volgroupName); + + if (details != null) { + pool.setDetails(details); + } + + return pool; + } + + /** + * CLVM fallback: First tries to refresh libvirt pool to make volume visible, + * if that fails, accesses volume directly via block device path. + */ + private KVMPhysicalDisk getPhysicalDiskWithClvmFallback(String volumeUuid, KVMStoragePool pool, LibvirtStoragePool libvirtPool) { + logger.info("CLVM volume not visible to libvirt, attempting pool refresh for volume: {}", volumeUuid); + + try { + logger.debug("Refreshing libvirt storage pool: {}", pool.getUuid()); + libvirtPool.getPool().refresh(0); + + StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol != null) { + logger.info("Volume found after pool refresh: {}", volumeUuid); + boolean isQcow2 = StoragePoolType.CLVM_NG.equals(pool.getType()); + KVMPhysicalDisk disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); + disk.setSize(vol.getInfo().allocation); + disk.setVirtualSize(isQcow2 ? getQcow2VirtualSize(vol.getPath()) : vol.getInfo().capacity); + disk.setFormat(isQcow2 ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW); + return disk; + } + } catch (LibvirtException refreshEx) { + logger.debug("Pool refresh failed or volume still not found: {}", refreshEx.getMessage()); + } + + logger.info("Falling back to direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } + + private String getVgName(String sourceDir) { + String vgName = sourceDir; + if (vgName.startsWith("/")) { + String[] parts = vgName.split("/"); + List tokens = Arrays.stream(parts) + .filter(s -> !s.isEmpty()).collect(Collectors.toList()); + + vgName = tokens.size() > 1 ? tokens.get(1) + : tokens.size() == 1 ? tokens.get(0) + : ""; + } + return vgName; + } + + private String extractVgNameFromPool(KVMStoragePool pool) { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + throw new CloudRuntimeException("CLVM pool sourceDir is not set, cannot determine VG name"); + } + String vgName = getVgName(sourceDir); + logger.debug("Using VG name: {} (from sourceDir: {})", vgName, sourceDir); + return vgName; + } + + /** + * For CLVM volumes that exist in LVM but are not visible to libvirt, + * access them directly via block device path. + */ + private KVMPhysicalDisk getPhysicalDiskViaDirectBlockDevice(String volumeUuid, KVMStoragePool pool) { + try { + String vgName = extractVgNameFromPool(pool); + + verifyLvExistsInVg(volumeUuid, vgName); + + logger.info("Volume {} exists in LVM but not visible to libvirt, accessing directly", volumeUuid); + + String lvPath = findAccessibleDeviceNode(volumeUuid, vgName, pool); + long size = getClvmVolumeSize(lvPath); + + KVMPhysicalDisk disk = createPhysicalDiskFromClvmLv(lvPath, volumeUuid, pool, size); + ensureTemplateAccessibility(volumeUuid, lvPath, pool); + + return disk; + } catch (CloudRuntimeException ex) { + throw ex; + } catch (Exception ex) { + logger.error("Failed to access CLVM volume via direct block device: {}", volumeUuid, ex); + throw new CloudRuntimeException(String.format("Could not find volume %s: %s", volumeUuid, ex.getMessage())); + } + } + + private void verifyLvExistsInVg(String volumeUuid, String vgName) { + logger.debug("Checking if volume {} exists in VG {}", volumeUuid, vgName); + Script checkLvCmd = new Script("/usr/sbin/lvs", 30000, logger); + checkLvCmd.add("--noheadings"); + checkLvCmd.add("--unbuffered"); + checkLvCmd.add(vgName + "/" + volumeUuid); + String checkResult = checkLvCmd.execute(); + if (checkResult != null) { + throw new CloudRuntimeException(String.format("Storage volume not found: no storage vol with matching name '%s'", volumeUuid)); + } + } + + private String findAccessibleDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + String lvPath = "/dev/" + vgName + "/" + volumeUuid; + File lvDevice = new File(lvPath); + + if (!lvDevice.exists()) { + lvPath = tryDeviceMapperPath(volumeUuid, vgName); + if (!new File(lvPath).exists()) { + lvPath = handleMissingDeviceNode(volumeUuid, vgName, pool); + } + } + + return lvPath; + } + + private String tryDeviceMapperPath(String volumeUuid, String vgName) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + return "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + } + + private String handleMissingDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + return activateTemplateAndGetPath(volumeUuid, vgName); + } + throw new CloudRuntimeException(String.format("Could not find volume %s in VG %s - volume exists in LVM but device node not accessible", volumeUuid, vgName)); + } + + private String activateTemplateAndGetPath(String volumeUuid, String vgName) { + logger.info("Template volume {} device node not found. Attempting to activate in shared mode.", volumeUuid); + String templateLvPath = "/dev/" + vgName + "/" + volumeUuid; + + try { + ensureTemplateLvInSharedMode(templateLvPath, false); + + String lvPath = findDeviceNodeAfterActivation(templateLvPath, volumeUuid, vgName); + + logger.info("Successfully activated template volume {} at {}", volumeUuid, lvPath); + return lvPath; + } catch (CloudRuntimeException e) { + throw new CloudRuntimeException(String.format("Failed to activate template volume %s in VG %s: %s", volumeUuid, vgName, e.getMessage()), e); + } + } + + private String findDeviceNodeAfterActivation(String templateLvPath, String volumeUuid, String vgName) { + File lvDevice = new File(templateLvPath); + String lvPath = templateLvPath; + + if (!lvDevice.exists()) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + lvPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + lvDevice = new File(lvPath); + } + + if (!lvDevice.exists()) { + logger.error("Template volume {} still not accessible after activation attempt", volumeUuid); + throw new CloudRuntimeException(String.format("Could not activate template volume %s in VG %s - device node not accessible after activation", volumeUuid, vgName)); + } + + return lvPath; + } + + private void ensureTemplateAccessibility(String volumeUuid, String lvPath, KVMStoragePool pool) { + if (StoragePoolType.CLVM_NG.equals(pool.getType()) && volumeUuid.startsWith("template-")) { + logger.info("Detected template volume {}. Ensuring it's activated in shared mode.", volumeUuid); + ensureTemplateLvInSharedMode(lvPath, false); + } + } + + private long getClvmVolumeSize(String lvPath) { + try { + Script lvsCmd = new Script("/usr/sbin/lvs", 30000, logger); + lvsCmd.add("--noheadings"); + lvsCmd.add("--units"); + lvsCmd.add("b"); + lvsCmd.add("-o"); + lvsCmd.add("lv_size"); + lvsCmd.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = lvsCmd.execute(parser); + + String output = (result == null) ? parser.getLines() : result; + + if (output != null && !output.isEmpty()) { + String sizeStr = output.trim().replaceAll("[^0-9]", ""); + if (!sizeStr.isEmpty()) { + return Long.parseLong(sizeStr); + } + } + } catch (Exception sizeEx) { + logger.warn("Failed to get size for CLVM volume via lvs: {}", sizeEx.getMessage()); + File lvDevice = new File(lvPath); + if (lvDevice.isFile()) { + return lvDevice.length(); + } + } + return 0; + } + + private KVMPhysicalDisk createPhysicalDiskFromClvmLv(String lvPath, String volumeUuid, KVMStoragePool pool, long size) { + PhysicalDiskFormat diskFormat = StoragePoolType.CLVM_NG.equals(pool.getType()) + ? PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW; + + logger.debug("{} pool detected, setting disk format to {} for volume {}", pool.getType(), diskFormat, volumeUuid); + + KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); + disk.setFormat(diskFormat); + disk.setSize(size); + disk.setVirtualSize(diskFormat == PhysicalDiskFormat.QCOW2 ? getQcow2VirtualSize(lvPath) : size); + + logger.info("Successfully accessed CLVM/CLVM_NG volume via direct block device: {} with format: {} and size: {} bytes", + lvPath, diskFormat, size); + return disk; + } + + /** + * Checks if a CLVM_NG QCOW2 volume has a backing file (template) and ensures it's activated in shared mode. + */ + private void ensureClvmNgBackingFileAccessible(String volumeName, KVMStoragePool pool) { + try { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + + logger.debug("Checking if CLVM_NG volume {} has a backing file that needs activation", volumePath); + + Script qemuImgInfo = new Script("qemu-img", Duration.millis(10000), logger); + qemuImgInfo.add("info"); + qemuImgInfo.add("--output=json"); + qemuImgInfo.add(volumePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImgInfo.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String jsonOutput = parser.getLines(); + + if (jsonOutput.contains("\"backing-filename\"")) { + int backingStart = jsonOutput.indexOf("\"backing-filename\""); + if (backingStart > 0) { + int valueStart = jsonOutput.indexOf(":", backingStart); + if (valueStart > 0) { + valueStart = jsonOutput.indexOf("\"", valueStart) + 1; + int valueEnd = jsonOutput.indexOf("\"", valueStart); + + if (valueEnd > valueStart) { + String backingFile = jsonOutput.substring(valueStart, valueEnd).trim(); + if (!backingFile.isEmpty() && backingFile.startsWith("/dev/")) { + logger.info("Volume {} has backing file: {}. Ensuring backing file is in shared mode.", volumePath, backingFile); + ensureTemplateLvInSharedMode(backingFile, false); + } + } + } + } + } else { + logger.debug("Volume {} does not have a backing file (full clone)", volumePath); + } + } + } catch (Exception e) { + logger.warn("Failed to check/activate backing file for volume {}: {}. VM deployment may fail if template is not accessible.", + volumeName, e.getMessage()); + } + } + + private String getClvmBackingFile(KVMPhysicalDisk template, KVMStoragePool destPool) { + String templateLvName = template.getName(); + KVMPhysicalDisk templateOnPrimary = null; + + try { + templateOnPrimary = destPool.getPhysicalDisk(templateLvName); + } catch (CloudRuntimeException e) { + logger.warn("Template {} not found on CLVM_NG pool {}.", templateLvName, destPool.getUuid()); + } + + if (templateOnPrimary != null) { + String backingFile = templateOnPrimary.getPath(); + logger.info("Using template on primary storage as backing file: {}", backingFile); + ensureTemplateLvInSharedMode(backingFile); + return backingFile; + } + + logger.error("Template {} should be on primary storage before creating volumes from it", templateLvName); + throw new CloudRuntimeException(String.format("Template not found on CLVM_NG primary storage: %s. Template must be copied to primary storage first.", templateLvName)); + } + + /** + * Ensures a template LV is activated in shared mode so multiple VMs can use it as a backing file. + */ + private void ensureTemplateLvInSharedMode(String templatePath, boolean throwOnFailure) { + try { + Script checkLvs = new Script("lvs", Duration.millis(30000), logger); + checkLvs.add("--noheadings"); + checkLvs.add("-o", "lv_attr"); + checkLvs.add(templatePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = checkLvs.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String lvAttr = parser.getLines().trim(); + if (lvAttr.length() >= 6) { + boolean isActive = (lvAttr.indexOf('a') >= 0); + boolean isShared = (lvAttr.indexOf('s') >= 0); + + if (!isShared || !isActive) { + logger.info("Template LV {} is not in shared mode (attr: {}). Activating in shared mode.", templatePath, lvAttr); + LibvirtComputingResource.setClvmVolumeToSharedMode(templatePath); + } else { + logger.debug("Template LV {} is already in shared mode (attr: {})", templatePath, lvAttr); + } + } + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = "Failed to check/ensure template LV shared mode for " + templatePath + ": " + e.getMessage(); + if (throwOnFailure) { + throw new CloudRuntimeException(errorMsg, e); + } else { + logger.warn(errorMsg, e); + } + } + } + + private void ensureTemplateLvInSharedMode(String templatePath) { + ensureTemplateLvInSharedMode(templatePath, false); + } + + private long getVgPhysicalExtentSize(String vgName) { + final long DEFAULT_PE_SIZE = 4 * 1024 * 1024L; + String warningMessage = String.format("Failed to get PE size for VG %s, defaulting to 4MiB", vgName); + + try { + Script vgDisplay = new Script("vgdisplay", 300000, logger); + vgDisplay.add("--units", "b"); + vgDisplay.add("-C"); + vgDisplay.add("--noheadings"); + vgDisplay.add("-o", "vg_extent_size"); + vgDisplay.add(vgName); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = vgDisplay.execute(parser); + + if (result != null) { + logger.warn("{}: {}", warningMessage, result); + return DEFAULT_PE_SIZE; + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + logger.warn("{}: empty output", warningMessage); + return DEFAULT_PE_SIZE; + } + + output = output.trim(); + if (output.endsWith("B")) { + output = output.substring(0, output.length() - 1).trim(); + } + + long peSize = Long.parseLong(output); + logger.debug("Physical Extent size for VG {} is {} bytes", vgName, peSize); + return peSize; + } catch (NumberFormatException e) { + logger.warn("{}: failed to parse PE size", warningMessage, e); + } catch (Exception e) { + logger.warn("{}: {}", warningMessage, e.getMessage()); + } + + logger.info("Using default PE size for VG {}: {} bytes (4 MiB)", vgName, DEFAULT_PE_SIZE); + return DEFAULT_PE_SIZE; + } + + /** + * Calculate LVM LV size for CLVM_NG volume allocation. + * {@code peSize} must be the Physical Extent size of the VG (from {@link #getVgPhysicalExtentSize}). + */ + private long calculateClvmNgLvSize(long virtualSize, long peSize) { + long clusterSize = 64 * 1024L; + long l2Multiplier = 4096L; + + long numDataClusters = (virtualSize + clusterSize - 1) / clusterSize; + long numL2Clusters = (numDataClusters + l2Multiplier - 1) / l2Multiplier; + long l2TableSize = numL2Clusters * clusterSize; + long refcountTableSize = l2TableSize; + + long headerOverhead = 2 * 1024 * 1024L; + long metadataOverhead = l2TableSize + refcountTableSize + headerOverhead; + long targetSize = virtualSize + metadataOverhead; + long roundedSize = ((targetSize + peSize - 1) / peSize) * peSize; + long virtualSizeGiB = virtualSize / (1024 * 1024 * 1024L); + long overheadMiB = metadataOverhead / (1024 * 1024L); + + logger.info("Calculated volume LV size: {} bytes (virtual: {} GiB, QCOW2 metadata overhead: {} MiB, rounded to {} PEs, PE size = {} bytes)", + roundedSize, virtualSizeGiB, overheadMiB, roundedSize / peSize, peSize); + + return roundedSize; + } + + private long getQcow2VirtualSize(String imagePath) { + Script qemuImg = new Script("qemu-img", 300000, logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add("-U"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 virtual size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("virtual-size").getAsLong(); + } + + private long getQcow2PhysicalSize(String imagePath) { + Script qemuImg = new Script("qemu-img", Duration.millis(300000), logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 physical size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("actual-size").getAsLong(); + } + + private KVMPhysicalDisk createClvmNgDiskWithBacking(String volumeUuid, int timeout, long virtualSize, String backingFile, + KVMStoragePool pool, Storage.ProvisioningType provisioningType) { + String vgName = getVgName(pool.getLocalPath()); + // Query PE size once and reuse for both the QCOW2 virtual-size alignment and the + long peSize = getVgPhysicalExtentSize(vgName); + long peAlignedVirtualSize = ((virtualSize + peSize - 1) / peSize) * peSize; + long lvSize = calculateClvmNgLvSize(peAlignedVirtualSize, peSize); + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + logger.debug("Creating CLVM_NG volume {} with LV size {} bytes (requested virtual: {} bytes, PE-aligned virtual: {} bytes, provisioning: {})", + volumeUuid, lvSize, virtualSize, peAlignedVirtualSize, provisioningType); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeUuid); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG volume: " + result); + } + + Script qemuImg = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImg.add("create"); + qemuImg.add("-f", "qcow2"); + + StringBuilder qcow2Options = new StringBuilder(); + String preallocation = (provisioningType == Storage.ProvisioningType.THIN) ? "off" : "metadata"; + qcow2Options.append("preallocation=").append(preallocation); + qcow2Options.append(",extended_l2=on"); + qcow2Options.append(",cluster_size=64k"); + + if (backingFile != null && !backingFile.isEmpty()) { + qcow2Options.append(",backing_file=").append(backingFile); + qcow2Options.append(",backing_fmt=qcow2"); + logger.debug("Creating CLVM_NG volume with backing file: {}", backingFile); + } + + qemuImg.add("-o", qcow2Options.toString()); + qemuImg.add(volumePath); + qemuImg.add(peAlignedVirtualSize + ""); + + result = qemuImg.execute(); + if (result != null) { + removeLvOnFailure(volumePath, timeout); + throw new CloudRuntimeException("Failed to create QCOW2 on CLVM_NG volume: " + result); + } + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeUuid, pool); + disk.setFormat(PhysicalDiskFormat.QCOW2); + disk.setSize(actualSize); + disk.setVirtualSize(peAlignedVirtualSize); + + logger.info("Successfully created CLVM_NG volume {} (LV size: {}, PE-aligned virtual size: {}, provisioning: {}, preallocation: {})", + volumeUuid, lvSize, peAlignedVirtualSize, provisioningType, preallocation); + + return disk; + } + + private boolean lvExists(String lvPath) { + Script checkLv = new Script("lvs", Duration.millis(30000), logger); + checkLv.add("--noheadings"); + checkLv.add("--unbuffered"); + checkLv.add(lvPath); + return checkLv.execute() == null; + } + + private void removeLvOnFailure(String lvPath, int timeout) { + Script lvremove = new Script("lvremove", Duration.millis(timeout), logger); + lvremove.add("-f"); + lvremove.add(lvPath); + lvremove.execute(); + } + + private KVMPhysicalDisk createClvmVolume(String volumeName, long size, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + int timeout = 30000; + + logger.info("Creating CLVM volume {} in VG {} with size {} bytes", volumeName, vgName, size); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeName); + lvcreate.add("-L", size + "B"); + lvcreate.add("--yes"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create CLVM volume: " + result); + } + + logger.info("Successfully created CLVM volume {} at {} with size {}", volumeName, volumePath, toHumanReadableSize(size)); + + long actualSize = getClvmVolumeSize(volumePath); + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeName, pool); + disk.setFormat(PhysicalDiskFormat.RAW); + disk.setSize(actualSize); + disk.setVirtualSize(actualSize); + + return disk; + } + + @Override + public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.ImageFormat format) { + logger.info("CLVM/CLVM_NG pool detected - using direct LVM cleanup with secure zero-fill for volume {}", uuid); + return cleanupCLVMVolume(uuid, pool); + } + + /** + * Clean up CLVM volume and its snapshots directly using LVM commands. + */ + private boolean cleanupCLVMVolume(String uuid, KVMStoragePool pool) { + logger.info("Starting direct LVM cleanup for CLVM volume: {} in pool: {}", uuid, pool.getUuid()); + + try { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + logger.debug("Source directory is null or empty, cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + String vgName = getVgName(sourceDir); + logger.info("Determined VG name: {} for pool: {}", vgName, pool.getUuid()); + + if (vgName == null || vgName.isEmpty()) { + logger.warn("Cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); + return true; + } + + String lvPath = "/dev/" + vgName + "/" + uuid; + logger.debug("Volume path: {}", lvPath); + + Script checkLvs = new Script("lvs", 30000, logger); + checkLvs.add("--noheadings"); + checkLvs.add("--unbuffered"); + checkLvs.add(lvPath); + + logger.info("Checking if volume exists: lvs --noheadings --unbuffered {}", lvPath); + String checkResult = checkLvs.execute(); + + if (checkResult != null) { + logger.info("CLVM volume {} does not exist in LVM (check returned: {}), considering it as already deleted", uuid, checkResult); + return true; + } + + logger.info("Volume {} exists, proceeding with cleanup", uuid); + + boolean secureZeroFillEnabled = shouldSecureZeroFill(pool); + + if (secureZeroFillEnabled) { + logger.info("Step 1: Zero-filling volume {} for security", uuid); + secureZeroFillVolume(lvPath, uuid); + } else { + logger.info("Secure zero-fill is disabled, skipping zero-filling for volume {}", uuid); + } + + logger.info("Step 2: Removing volume {}", uuid); + Script removeLv = new Script("lvremove", 30000, logger); + removeLv.add("-f"); + removeLv.add(lvPath); + + logger.info("Executing command: lvremove -f {}", lvPath); + String removeResult = removeLv.execute(); + + if (removeResult == null) { + logger.info("Successfully removed CLVM volume {} using direct LVM cleanup", uuid); + return true; + } else { + logger.warn("Command 'lvremove -f {}' returned error: {}", lvPath, removeResult); + if (removeResult.contains("not found") || removeResult.contains("Failed to find")) { + logger.info("CLVM volume {} not found during cleanup, considering it as already deleted", uuid); + return true; + } + return false; + } + } catch (Exception ex) { + logger.error("Exception during CLVM volume cleanup for {}: {}", uuid, ex.getMessage(), ex); + return true; + } + } + + private boolean shouldSecureZeroFill(KVMStoragePool pool) { + Map details = pool.getDetails(); + String secureZeroFillStr = (details != null) ? details.get(KVMStoragePool.CLVM_SECURE_ZERO_FILL) : null; + return Boolean.parseBoolean(secureZeroFillStr); + } + + /** + * Securely zero-fill a volume before deletion to prevent data leakage. + * Uses blkdiscard (fast TRIM) as primary method, with dd zero-fill as fallback. + */ + private void secureZeroFillVolume(String lvPath, String volumeUuid) { + logger.info("Starting secure zero-fill for CLVM volume: {} at path: {}", volumeUuid, lvPath); + + boolean blkdiscardSuccess = false; + + try { + Script blkdiscard = new Script("blkdiscard", 300000, logger); + blkdiscard.add("-f"); + blkdiscard.add(lvPath); + + String result = blkdiscard.execute(); + if (result == null) { + logger.info("Successfully zero-filled CLVM volume {} using blkdiscard (TRIM)", volumeUuid); + blkdiscardSuccess = true; + } else { + if (result.contains("Operation not supported") || result.contains("BLKDISCARD ioctl failed")) { + logger.info("blkdiscard not supported for volume {} (device doesn't support TRIM/DISCARD), using dd fallback", volumeUuid); + } else { + logger.warn("blkdiscard failed for volume {}: {}, will try dd fallback", volumeUuid, result); + } + } + } catch (Exception e) { + logger.warn("Exception during blkdiscard for volume {}: {}, will try dd fallback", volumeUuid, e.getMessage()); + } + + if (!blkdiscardSuccess) { + logger.info("Attempting zero-fill using dd for CLVM volume: {}", volumeUuid); + try { + String command = String.format( + "nice -n 19 ionice -c 2 -n 7 dd if=/dev/zero of=%s bs=1M oflag=direct 2>&1 || true", + lvPath + ); + + Script ddZeroFill = new Script("/bin/bash", 3600000, logger); + ddZeroFill.add("-c"); + ddZeroFill.add(command); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String ddResult = ddZeroFill.execute(parser); + String output = parser.getLines(); + + if (output != null && (output.contains("copied") || output.contains("records in") || + output.contains("No space left on device"))) { + logger.info("Successfully zero-filled CLVM volume {} using dd", volumeUuid); + } else if (ddResult == null) { + logger.info("Zero-fill completed for CLVM volume {}", volumeUuid); + } else { + logger.warn("dd zero-fill for volume {} completed with output: {}", volumeUuid, + output != null ? output : ddResult); + } + } catch (Exception e) { + logger.warn("Failed to zero-fill CLVM volume {} before deletion: {}. Proceeding with deletion anyway.", + volumeUuid, e.getMessage()); + } + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java index 3e35ed9476b1..a8207cec3fa6 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java @@ -33,6 +33,7 @@ public interface KVMStoragePool { + public static final String CLVM_SECURE_ZERO_FILL = "clvmsecurezerofill"; long HeartBeatUpdateTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.HEARTBEAT_UPDATE_TIMEOUT); long HeartBeatUpdateFreqInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_UPDATE_FREQUENCY); long HeartBeatCheckerTimeoutInMs = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.KVM_HEARTBEAT_CHECKER_TIMEOUT); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java index 35cc864268c3..996398a286ff 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java @@ -72,7 +72,9 @@ private StorageAdaptor getStorageAdaptor(StoragePoolType type) { private void addStoragePool(String uuid, StoragePoolInformation pool) { synchronized (_storagePools) { - if (!_storagePools.containsKey(uuid)) { + // Insert on first registration; on subsequent calls (e.g. ModifyStoragePoolCommand) + // overwrite when new details are present so config changes are reflected + if (!_storagePools.containsKey(uuid) || MapUtils.isNotEmpty(pool.getDetails())) { _storagePools.put(uuid, pool); } } @@ -81,6 +83,10 @@ private void addStoragePool(String uuid, StoragePoolInformation pool) { public KVMStoragePoolManager(StorageLayer storagelayer, KVMHAMonitor monitor) { this._haMonitor = monitor; this._storageMapper.put("libvirt", new LibvirtStorageAdaptor(storagelayer)); + // Register CLVM/CLVM_NG adaptor explicitly for both types (one shared instance) + ClvmStorageAdaptor clvmAdaptor = new ClvmStorageAdaptor(storagelayer); + this._storageMapper.put(StoragePoolType.CLVM.toString(), clvmAdaptor); + this._storageMapper.put(StoragePoolType.CLVM_NG.toString(), clvmAdaptor); // add other storage adaptors manually here // add any adaptors that wish to register themselves via call to adaptor.getStoragePoolType() @@ -92,8 +98,8 @@ public KVMStoragePoolManager(StorageLayer storagelayer, KVMHAMonitor monitor) { logger.debug("Skipping registration of abstract class / interface " + storageAdaptorClass.getName()); continue; } - if (storageAdaptorClass.isAssignableFrom(LibvirtStorageAdaptor.class)) { - logger.debug("Skipping re-registration of LibvirtStorageAdaptor"); + if (storageAdaptorClass == LibvirtStorageAdaptor.class || storageAdaptorClass == ClvmStorageAdaptor.class) { + logger.debug("Skipping re-registration of explicitly registered adaptor: {}", storageAdaptorClass.getSimpleName()); continue; } try { @@ -288,19 +294,45 @@ public KVMStoragePool getStoragePool(StoragePoolType type, String uuid, boolean } if (pool instanceof LibvirtStoragePool) { - addPoolDetails(uuid, (LibvirtStoragePool) pool); + LibvirtStoragePool libvirtPool = (LibvirtStoragePool) pool; + addPoolDetails(uuid, libvirtPool); + ((LibvirtStoragePool) pool).setType(type); + updatePoolTypeIfApplicable(libvirtPool, pool, type, uuid); } return pool; } + private void updatePoolTypeIfApplicable(LibvirtStoragePool libvirtPool, KVMStoragePool pool, + StoragePoolType type, String uuid) { + StoragePoolType correctType = type; + if (correctType == null || correctType == StoragePoolType.CLVM) { + StoragePoolInformation info = _storagePools.get(uuid); + if (info != null && info.getPoolType() != null) { + correctType = info.getPoolType(); + } + } + + if (correctType != null && correctType != pool.getType() && + (correctType == StoragePoolType.CLVM || correctType == StoragePoolType.CLVM_NG) && + (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG)) { + logger.debug("Correcting pool type from {} to {} for pool {} based on caller/cached information", + pool.getType(), correctType, uuid); + libvirtPool.setType(correctType); + } + } + /** * As the class {@link LibvirtStoragePool} is constrained to the {@link org.libvirt.StoragePool} class, there is no way of saving a generic parameter such as the details, hence, * this method was created to always make available the details of libvirt primary storages for when they are needed. */ private void addPoolDetails(String uuid, LibvirtStoragePool pool) { StoragePoolInformation storagePoolInformation = _storagePools.get(uuid); + if (storagePoolInformation == null) { + logger.warn("No cached StoragePoolInformation found for pool UUID {}, pool details will not be set.", uuid); + return; + } Map details = storagePoolInformation.getDetails(); if (MapUtils.isNotEmpty(details)) { @@ -454,6 +486,10 @@ public KVMPhysicalDisk createDiskFromTemplate(KVMPhysicalDisk template, String n return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.RAW, provisioningType, size, destPool, timeout, passphrase); + } else if (destPool.getType() == StoragePoolType.CLVM_NG) { + return adaptor.createDiskFromTemplate(template, name, + PhysicalDiskFormat.QCOW2, provisioningType, + size, destPool, timeout, passphrase); } else if (template.getFormat() == PhysicalDiskFormat.DIR) { return adaptor.createDiskFromTemplate(template, name, PhysicalDiskFormat.DIR, provisioningType, @@ -495,6 +531,11 @@ public KVMPhysicalDisk createPhysicalDiskFromDirectDownloadTemplate(String templ return adaptor.createTemplateFromDirectDownloadFile(templateFilePath, destTemplatePath, destPool, format, timeout); } + public void createTemplateOnClvmNg(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + StorageAdaptor adaptor = getStorageAdaptor(pool.getType()); + adaptor.createTemplate(templatePath, templateUuid, timeout, pool); + } + public Ternary, String> prepareStorageClient(StoragePoolType type, String uuid, Map details) { StorageAdaptor adaptor = getStorageAdaptor(type); return adaptor.prepareStorageClient(uuid, details); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index f95ebff5326f..4a77f7e9e19c 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 @@ -55,6 +55,7 @@ import com.cloud.agent.api.Command; import com.cloud.hypervisor.kvm.resource.LibvirtXMLParser; +import com.cloud.storage.clvm.ClvmPoolManager; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -225,6 +226,26 @@ public class KVMStorageProcessor implements StorageProcessor { " \n" + ""; + private static final String DUMMY_VM_XML_BLOCK = "\n" + + " %s\n" + + " 256\n" + + " 256\n" + + " 1\n" + + " \n" + + " hvm\n" + + " \n" + + " \n" + + " \n" + + " %s\n" + + " \n" + + " \n"+ + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + public KVMStorageProcessor(final KVMStoragePoolManager storagePoolMgr, final LibvirtComputingResource resource) { this.storagePoolMgr = storagePoolMgr; @@ -347,15 +368,28 @@ public Answer copyTemplateToPrimaryStorage(final CopyCommand cmd) { path = destTempl.getUuid(); } - if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { - logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); - return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); - } + if (primaryPool.getType() == StoragePoolType.CLVM_NG) { + logger.info("Copying template {} to CLVM_NG pool {}", + destTempl.getUuid(), primaryPool.getUuid()); - primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + try { + storagePoolMgr.createTemplateOnClvmNg(tmplVol.getPath(), path, cmd.getWaitInMillSeconds(), primaryPool); + primaryVol = primaryPool.getPhysicalDisk("template-" + path); + } catch (Exception e) { + logger.error("Failed to create CLVM_NG template: {}", e.getMessage(), e); + return new PrimaryStorageDownloadAnswer("Failed to create CLVM_NG template: " + e.getMessage()); + } + } else { + if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { + logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); + } + + primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); - if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { - logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { + logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + } } } else { primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, UUID.randomUUID().toString(), primaryPool, cmd.getWaitInMillSeconds()); @@ -376,7 +410,8 @@ public Answer copyTemplateToPrimaryStorage(final CopyCommand cmd) { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(primaryPool.getType())) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(primaryPool.getType())) { newTemplate.setFormat(ImageFormat.RAW); } else { newTemplate.setFormat(ImageFormat.QCOW2); @@ -589,7 +624,9 @@ public Answer copyVolumeFromImageCacheToPrimary(final CopyCommand cmd) { String path = details != null ? details.get(DiskTO.IQN) : null; - storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + if (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details); + } final String volumeName = UUID.randomUUID().toString(); @@ -618,7 +655,9 @@ public Answer copyVolumeFromImageCacheToPrimary(final CopyCommand cmd) { final KVMPhysicalDisk newDisk = storagePoolMgr.copyPhysicalDisk(volume, path != null ? path : volumeName, primaryPool, cmd.getWaitInMillSeconds()); resource.createOrUpdateLogFileForCommand(cmd, Command.State.COMPLETED); - storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path); + if (!ClvmPoolManager.isClvmPoolType(primaryStore.getPoolType())) { + storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path); + } final VolumeObjectTO newVol = new VolumeObjectTO(); @@ -1118,7 +1157,14 @@ public Answer backupSnapshot(final CopyCommand cmd) { } } else { final Script command = new Script(_manageSnapshotPath, cmd.getWaitInMillSeconds(), logger); - command.add("-b", isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath()); + String backupPath; + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + backupPath = snapshotDisk.getPath(); + logger.debug("Using snapshotDisk path for CLVM/CLVM_NG backup: " + backupPath); + } else { + backupPath = isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath(); + } + command.add("-b", backupPath); command.add(NAME_OPTION, snapshotName); command.add("-p", snapshotDestPath); @@ -1163,6 +1209,90 @@ public Answer backupSnapshot(final CopyCommand cmd) { } } + /** + * Parse CLVM/CLVM_NG snapshot path and compute MD5 hash. + * Snapshot path format: /dev/vgname/volumeuuid/snapshotuuid + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (for logging) + * @return Array of [vgName, volumeUuid, snapshotUuid, md5Hash] or null if invalid + */ + private String[] parseClvmSnapshotPath(String snapshotPath, StoragePoolType poolType) { + String[] pathParts = snapshotPath.split("/"); + if (pathParts.length < 5) { + logger.warn("Invalid {} snapshot path format: {}, expected format: /dev/vgname/volume-uuid/snapshot-uuid", + poolType, snapshotPath); + return null; + } + + String vgName = pathParts[2]; + String volumeUuid = pathParts[3]; + String snapshotUuid = pathParts[4]; + + logger.info("Parsed {} snapshot path - VG: {}, Volume: {}, Snapshot: {}", + poolType, vgName, volumeUuid, snapshotUuid); + + String md5Hash = computeMd5Hash(snapshotUuid); + logger.debug("Computed MD5 hash for snapshot UUID {}: {}", snapshotUuid, md5Hash); + + return new String[]{vgName, volumeUuid, snapshotUuid, md5Hash}; + } + + /** + * Delete a CLVM or CLVM_NG snapshot using managesnapshot.sh script. + * For both CLVM and CLVM_NG, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid + * The script handles MD5 transformation and pool-specific deletion commands internally: + * - CLVM: Uses lvremove to delete LVM snapshot + * - CLVM_NG: Uses qemu-img snapshot -d to delete QCOW2 internal snapshot + * This approach is consistent with snapshot creation and backup which also use the script. + * + * @param snapshotPath The snapshot path from database + * @param poolType Storage pool type (CLVM or CLVM_NG) + * @param checkExistence If true, checks if snapshot exists before cleanup (for explicit deletion) + * If false, always performs cleanup (for post-backup cleanup) + * @return true if cleanup was performed, false if snapshot didn't exist (when checkExistence=true) + */ + private boolean deleteClvmSnapshot(String snapshotPath, StoragePoolType poolType, boolean checkExistence) { + logger.info("Starting {} snapshot deletion for path: {}, checkExistence: {}", poolType, snapshotPath, checkExistence); + + try { + String[] parsed = parseClvmSnapshotPath(snapshotPath, poolType); + if (parsed == null) { + return false; + } + + String vgName = parsed[0]; + String volumeUuid = parsed[1]; + String snapshotUuid = parsed[2]; + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + // Use managesnapshot.sh script for deletion (consistent with create/backup) + // Script handles MD5 transformation and pool-specific commands internally + Script deleteCommand = new Script(_manageSnapshotPath, 30000, logger); + deleteCommand.add("-d", volumePath); + deleteCommand.add("-n", snapshotUuid); + + logger.info("Executing: managesnapshot.sh -d {} -n {}", volumePath, snapshotUuid); + String result = deleteCommand.execute(); + + if (result == null) { + logger.info("Successfully deleted {} snapshot: {}", poolType, snapshotPath); + return true; + } else { + if (checkExistence && result.contains("does not exist")) { + logger.info("{} snapshot {} already deleted, no cleanup needed", poolType, snapshotPath); + return true; + } + logger.warn("Failed to delete {} snapshot {}: {}", poolType, snapshotPath, result); + return false; + } + + } catch (Exception ex) { + logger.error("Exception while deleting {} snapshot {}", poolType, snapshotPath, ex); + return false; + } + } + private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObjectTO snapshot, KVMStoragePool primaryPool) { String snapshotPath = snapshot.getPath(); @@ -1175,7 +1305,19 @@ private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObject if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) { try { - Files.deleteIfExists(Paths.get(snapshotPath)); + if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // Both CLVM and CLVM_NG use the same deletion method via managesnapshot.sh script + boolean cleanedUp = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), false); + if (!cleanedUp) { + String[] parsedPath = parseClvmSnapshotPath(snapshotPath, primaryPool.getType()); + String snapMd5 = (parsedPath != null) ? computeMd5Hash(parsedPath[2]) : computeMd5Hash(snapshotPath); + logger.warn("Deletion of Snapshot: {} on primary store may have failed as it doesn't exist: {} " + + "(MD5 of snapshot UUID: {} - admins can use this to manually locate and delete the LV via managesnapshot.sh or lvremove)", + primaryPool.getType(), snapshotPath, snapMd5); + } + } else { + Files.deleteIfExists(Paths.get(snapshotPath)); + } } catch (IOException ex) { logger.error("Failed to delete snapshot [{}] on primary storage [{}].", snapshot.getId(), snapshot.getName(), ex); } @@ -1184,6 +1326,26 @@ private void deleteSnapshotOnPrimary(final CopyCommand cmd, final SnapshotObject } } + + /** + * Compute MD5 hash of a string, matching what managesnapshot.sh does: + * echo "${snapshot}" | md5sum -t | awk '{ print $1 }' + */ + private String computeMd5Hash(String input) { + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] array = md.digest((input + "\n").getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : array) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + logger.error("Failed to compute MD5 hash for: {}", input, e); + return input; + } + } + protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map params, DataStoreTO store) throws LibvirtException, InternalErrorException { DiskDef iso = new DiskDef(); @@ -1523,6 +1685,10 @@ protected synchronized void attachOrDetachDisk(final Connect conn, final boolean if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } + } else if (attachingPool.getType() == StoragePoolType.CLVM_NG) { + // CLVM_NG uses QCOW2 format on block devices + diskdef.defBlockBasedDisk(attachingDisk.getPath(), devId, busT); + diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) { diskdef.defFileBasedDisk(attachingDisk.getPath(), devId, busT, DiskDef.DiskFmtType.QCOW2); } else if (attachingDisk.getFormat() == PhysicalDiskFormat.RAW) { @@ -1738,13 +1904,22 @@ public Answer createVolume(final CreateObjectCommand cmd) { primaryPool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); disksize = volume.getSize(); PhysicalDiskFormat format; - if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType())) { + + MigrationOptions migrationOptions = volume.getMigrationOptions(); + boolean useDstPoolFormat = useDestPoolFormat(migrationOptions, primaryStore); + + if (volume.getFormat() == null || StoragePoolType.RBD.equals(primaryStore.getPoolType()) || useDstPoolFormat) { format = primaryPool.getDefaultFormat(); + if (useDstPoolFormat) { + logger.debug("Using destination pool default format {} for volume {} due to CLVM migration (src: {}, dst: {})", + format, volume.getUuid(), + migrationOptions != null ? migrationOptions.getSrcPoolType() : "unknown", + primaryStore.getPoolType()); + } } else { format = PhysicalDiskFormat.valueOf(volume.getFormat().toString().toUpperCase()); } - MigrationOptions migrationOptions = volume.getMigrationOptions(); if (migrationOptions != null) { int timeout = migrationOptions.getTimeout(); @@ -1769,7 +1944,11 @@ public Answer createVolume(final CreateObjectCommand cmd) { format = vol.getFormat(); } } - newVol.setSize(volume.getSize()); + if (StoragePoolType.CLVM_NG.equals(primaryStore.getPoolType()) && vol != null && vol.getVirtualSize() > 0) { + newVol.setSize(vol.getVirtualSize()); + } else { + newVol.setSize(volume.getSize()); + } newVol.setFormat(ImageFormat.valueOf(format.toString().toUpperCase())); return new CreateObjectAnswer(newVol); @@ -1781,6 +1960,29 @@ public Answer createVolume(final CreateObjectCommand cmd) { } } + /** + * For migration involving CLVM (RAW format), use destination pool's default format + * CLVM uses RAW format which may not match destination pool's format (e.g., NFS uses QCOW2) + * This specifically handles: + * - CLVM (RAW) -> NFS/Local/CLVM_NG (QCOW2) + * - NFS/Local/CLVM_NG (QCOW2) -> CLVM (RAW) + * @param migrationOptions + * @param primaryStore + * @return + */ + private boolean useDestPoolFormat(MigrationOptions migrationOptions, PrimaryDataStoreTO primaryStore) { + boolean useDstPoolFormat = false; + if (migrationOptions != null && migrationOptions.getSrcPoolType() != null) { + StoragePoolType srcPoolType = migrationOptions.getSrcPoolType(); + StoragePoolType dstPoolType = primaryStore.getPoolType(); + + if (srcPoolType != dstPoolType) { + useDstPoolFormat = (srcPoolType == StoragePoolType.CLVM || dstPoolType == StoragePoolType.CLVM); + } + } + return useDstPoolFormat; + } + /** * XML to take disk-only snapshot of the VM.

    * 1st parameter: snapshot's name;
    @@ -1870,10 +2072,22 @@ public Answer createSnapshot(final CreateObjectCommand cmd) { if (snapshotSize != null) { newSnapshot.setPhysicalSize(snapshotSize); } - } else if (primaryPool.getType() == StoragePoolType.CLVM) { - CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); - if (result != null) return result; - newSnapshot.setPath(snapshotPath); + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + if (primaryPool.getType() == StoragePoolType.CLVM_NG && snapshotTO.isKvmIncrementalSnapshot()) { + if (secondaryPool == null) { + String errorMsg = String.format("Incremental snapshots for CLVM_NG require secondary storage. " + + "Please configure secondary storage or disable incremental snapshots for volume [%s].", volume.getName()); + logger.error(errorMsg); + return new CreateObjectAnswer(errorMsg); + } + logger.info("Taking incremental snapshot of CLVM_NG volume [{}] using QCOW2 backup to secondary storage.", volume.getName()); + newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, + imageStoreTo.getUrl(), snapshotName, volume, conn, cmd.getWait()); + } else { + CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName); + if (result != null) return result; + newSnapshot.setPath(snapshotPath); + } } else { if (snapshotTO.isKvmIncrementalSnapshot()) { newSnapshot = takeIncrementalVolumeSnapshotOfStoppedVm(snapshotTO, primaryPool, secondaryPool, imageStoreTo != null ? imageStoreTo.getUrl() : null, snapshotName, volume, conn, cmd.getWait()); @@ -1946,7 +2160,11 @@ private String getVmXml(KVMStoragePool primaryPool, VolumeObjectTO volumeObjectT String machine = resource.isGuestAarch64() ? LibvirtComputingResource.VIRT : LibvirtComputingResource.PC; String cpuArch = resource.getGuestCpuArch() != null ? resource.getGuestCpuArch() : "x86_64"; - return String.format(DUMMY_VM_XML, vmName, cpuArch, machine, resource.getHypervisorPath(), primaryPool.getLocalPathFor(volumeObjectTo.getPath())); + String volumePath = primaryPool.getLocalPathFor(volumeObjectTo.getPath()); + boolean isClvmNg = StoragePoolType.CLVM_NG == primaryPool.getType(); + + String xmlTemplate = isClvmNg ? DUMMY_VM_XML_BLOCK : DUMMY_VM_XML; + return String.format(xmlTemplate, vmName, cpuArch, machine, resource.getHypervisorPath(), volumePath); } private SnapshotObjectTO takeIncrementalVolumeSnapshotOfRunningVm(SnapshotObjectTO snapshotObjectTO, KVMStoragePool primaryPool, KVMStoragePool secondaryPool, @@ -2667,11 +2885,13 @@ public Answer deleteVolume(final DeleteCommand cmd) { final PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO)vol.getDataStore(); try { final KVMStoragePool pool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid()); - try { - pool.getPhysicalDisk(vol.getPath()); - } catch (final Exception e) { - logger.debug(String.format("can't find volume: %s, return true", vol)); - return new Answer(null); + if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG) { + try { + pool.getPhysicalDisk(vol.getPath()); + } catch (final Exception e) { + logger.debug(String.format("can't find volume: %s, return true", vol)); + return new Answer(null); + } } pool.deletePhysicalDisk(vol.getPath(), vol.getFormat()); return new Answer(null); @@ -2900,6 +3120,25 @@ public Answer deleteSnapshot(final DeleteCommand cmd) { if (snapshotTO.isKvmIncrementalSnapshot()) { deleteCheckpoint(snapshotTO); } + } else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) { + // For CLVM/CLVM_NG, snapshots are typically already deleted from primary storage during backup + // via deleteSnapshotOnPrimary in the backupSnapshot finally block. + // This is called when the user explicitly deletes the snapshot via UI/API. + // We check if the snapshot still exists and clean it up if needed. + logger.info("Processing CLVM/CLVM_NG snapshot deletion (id={}, name={}, path={}) on primary storage", + snapshotTO.getId(), snapshotTO.getName(), snapshotTO.getPath()); + + String snapshotPath = snapshotTO.getPath(); + if (snapshotPath != null && !snapshotPath.isEmpty()) { + boolean wasDeleted = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), true); + if (wasDeleted) { + logger.info("Successfully cleaned up {} snapshot {} from primary storage", primaryPool.getType(), snapshotName); + } else { + logger.info("{} snapshot {} was already deleted from primary storage during backup, no cleanup needed", primaryPool.getType(), snapshotName); + } + } else { + logger.debug("{} snapshot path is null or empty, assuming already cleaned up", primaryPool.getType()); + } } else { logger.warn("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); throw new InternalErrorException("Operation not implemented for storage pool type of " + primaryPool.getType().toString()); @@ -3175,7 +3414,8 @@ private Storage.ImageFormat getFormat(StoragePoolType poolType) { StoragePoolType.RBD, StoragePoolType.PowerFlex, StoragePoolType.Linstor, - StoragePoolType.FiberChannel).contains(poolType)) { + StoragePoolType.FiberChannel, + StoragePoolType.CLVM).contains(poolType)) { return ImageFormat.RAW; } else { return ImageFormat.QCOW2; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index a03daeb197bf..ed159c92790d 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -231,6 +231,11 @@ private void createTemplateOnRBDFromDirectDownloadFile(String srcTemplateFilePat } public StorageVol getVolume(StoragePool pool, String volName) { + if (pool == null) { + logger.debug("LibVirt StoragePool is null (likely CLVM/CLVM_NG virtual pool), cannot lookup volume {} via libvirt", volName); + return null; + } + StorageVol vol = null; try { @@ -254,9 +259,12 @@ public StorageVol getVolume(StoragePool pool, String volName) { try { vol = pool.storageVolLookupByName(volName); - logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + if (vol != null) { + logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool"); + } } catch (LibvirtException e) { - throw new CloudRuntimeException("Could not find volume " + volName + ": " + e.getMessage()); + logger.debug("Volume " + volName + " still not found after pool refresh: " + e.getMessage()); + return null; } } @@ -349,38 +357,6 @@ private StoragePool createSharedStoragePool(Connect conn, String uuid, String ho } } - private StoragePool createCLVMStoragePool(Connect conn, String uuid, String host, String path) { - - String volgroupPath = "/dev/" + path; - String volgroupName = path; - volgroupName = volgroupName.replaceFirst("/", ""); - - LibvirtStoragePoolDef spd = new LibvirtStoragePoolDef(PoolType.LOGICAL, volgroupName, uuid, host, volgroupPath, volgroupPath); - StoragePool sp = null; - try { - logger.debug(spd.toString()); - sp = conn.storagePoolCreateXML(spd.toString(), 0); - return sp; - } catch (LibvirtException e) { - logger.error(e.toString()); - if (sp != null) { - try { - if (sp.isPersistent() == 1) { - sp.destroy(); - sp.undefine(); - } else { - sp.destroy(); - } - sp.free(); - } catch (LibvirtException l) { - logger.debug("Failed to define clvm storage pool with: " + l.toString()); - } - } - return null; - } - - } - private List getNFSMountOptsFromDetails(StoragePoolType type, Map details) { List nfsMountOpts = null; if (!type.equals(StoragePoolType.NetworkFilesystem) || details == null) { @@ -580,14 +556,11 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { Connect conn = LibvirtConnection.getConnection(); storage = conn.storagePoolLookupByUUIDString(uuid); - if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { - logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); - storage.create(0); - } LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); if (spd == null) { throw new CloudRuntimeException("Unable to parse the storage pool definition for storage pool " + uuid); } + StoragePoolType type = null; if (spd.getPoolType() == LibvirtStoragePoolDef.PoolType.NETFS) { type = StoragePoolType.NetworkFilesystem; @@ -603,6 +576,12 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { type = StoragePoolType.PowerFlex; } + // Activate pool if not running + if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { + logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); + storage.create(0); + } + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); if (pool.getType() != StoragePoolType.RBD && pool.getType() != StoragePoolType.PowerFlex) @@ -640,15 +619,17 @@ public KVMStoragePool getStoragePool(String uuid, boolean refreshInfo) { logger.info("Asking libvirt to refresh storage pool " + uuid); pool.refresh(); } + pool.setCapacity(storage.getInfo().capacity); pool.setUsed(storage.getInfo().allocation); - updateLocalPoolIops(pool); pool.setAvailable(storage.getInfo().available); - logger.debug("Successfully refreshed pool " + uuid + - " Capacity: " + toHumanReadableSize(storage.getInfo().capacity) + - " Used: " + toHumanReadableSize(storage.getInfo().allocation) + - " Available: " + toHumanReadableSize(storage.getInfo().available)); + logger.debug("Successfully refreshed pool {} Capacity: {} Used: {} Available: {}", + uuid, toHumanReadableSize(storage.getInfo().capacity), + toHumanReadableSize(storage.getInfo().allocation), + toHumanReadableSize(storage.getInfo().available)); + + updateLocalPoolIops(pool); return pool; } catch (LibvirtException e) { @@ -663,6 +644,10 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { try { StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); + if (vol == null) { + throw new CloudRuntimeException("Volume " + volumeUuid + " not found in libvirt pool"); + } + KVMPhysicalDisk disk; LibvirtStorageVolumeDef voldef = getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol); disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool); @@ -693,7 +678,7 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { } return disk; } catch (LibvirtException e) { - logger.debug("Failed to get physical disk:", e); + logger.debug("Failed to get volume from libvirt: " + e.getMessage()); throw new CloudRuntimeException(e.toString()); } } @@ -722,7 +707,7 @@ private int adjustStoragePoolRefCount(String uuid, int adjustment) { * Thread-safe increment storage pool usage refcount * @param uuid UUID of the storage pool to increment the count */ - private void incStoragePoolRefCount(String uuid) { + protected void incStoragePoolRefCount(String uuid) { adjustStoragePoolRefCount(uuid, 1); } /** @@ -730,7 +715,7 @@ private void incStoragePoolRefCount(String uuid) { * @param uuid UUID of the storage pool to decrement the count * @return true if the storage pool is still used, else false. */ - private boolean decStoragePoolRefCount(String uuid) { + protected boolean decStoragePoolRefCount(String uuid) { return adjustStoragePoolRefCount(uuid, -1) > 0; } @@ -814,7 +799,7 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri try { sp = createNetfsStoragePool(PoolType.NETFS, conn, name, host, path, nfsMountOpts); } catch (LibvirtException e) { - logger.error("Failed to create netfs mount: " + host + ":" + path , e); + logger.error("Failed to create netfs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -822,7 +807,7 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri try { sp = createNetfsStoragePool(PoolType.GLUSTERFS, conn, name, host, path, null); } catch (LibvirtException e) { - logger.error("Failed to create glusterfs mount: " + host + ":" + path , e); + logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -830,8 +815,6 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri sp = createSharedStoragePool(conn, name, host, path); } else if (type == StoragePoolType.RBD) { sp = createRBDStoragePool(conn, name, host, port, userInfo, path); - } else if (type == StoragePoolType.CLVM) { - sp = createCLVMStoragePool(conn, name, host, path); } } @@ -845,7 +828,6 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri // to be always mounted, as long the primary storage isn't fully deleted. incStoragePoolRefCount(name); } - if (sp.isActive() == 0) { logger.debug("Attempting to activate pool " + name); sp.create(0); @@ -1116,6 +1098,7 @@ private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool @Override public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { // this is for managed storage that needs to prep disks prior to use + return true; } @@ -1227,7 +1210,11 @@ public boolean deletePhysicalDisk(String uuid, KVMStoragePool pool, Storage.Imag LibvirtStoragePool libvirtPool = (LibvirtStoragePool)pool; try { StorageVol vol = getVolume(libvirtPool.getPool(), uuid); - logger.debug("Instructing libvirt to remove volume " + uuid + " from pool " + pool.getUuid()); + if (vol == null) { + logger.warn("Volume {} not found in libvirt pool {}, it may have been already deleted", uuid, pool.getUuid()); + return true; + } + logger.debug("Instructing libvirt to remove volume {} from pool {}", uuid, pool.getUuid()); if(Storage.ImageFormat.DIR.equals(format)){ deleteDirVol(libvirtPool, vol); } else { @@ -1420,9 +1407,7 @@ private KVMPhysicalDisk createDiskFromTemplateOnRBD(KVMPhysicalDisk template, rbd.close(destImage); } else { logger.debug("The source image " + srcPool.getSourceDir() + "/" + template.getName() - + " is RBD format 2. We will perform a RBD clone using snapshot " - + rbdTemplateSnapName); - /* The source image is format 2, we can do a RBD snapshot+clone (layering) */ + + " is RBD format 2. We will perform a RBD snapshot+clone (layering)"); logger.debug("Checking if RBD snapshot " + srcPool.getSourceDir() + "/" + template.getName() @@ -1618,9 +1603,12 @@ to support snapshots(backuped) as qcow2 files. */ } else { destFile = new QemuImgFile(destPath, destFormat); try { - boolean isQCOW2 = PhysicalDiskFormat.QCOW2.equals(sourceFormat); + boolean keepBitmaps = PhysicalDiskFormat.QCOW2.equals(sourceFormat); + if (destPool.getType() == StoragePoolType.CLVM) { + keepBitmaps = false; + } qemu.convert(srcFile, destFile, null, null, new QemuImageOptions(srcFile.getFormat(), srcFile.getFileName(), null), - null, false, isQCOW2); + null, false, keepBitmaps); Map destInfo = qemu.info(destFile); Long virtualSize = Long.parseLong(destInfo.get(QemuImg.VIRTUAL_SIZE)); newDisk.setVirtualSize(virtualSize); @@ -1684,8 +1672,8 @@ to support snapshots(backuped) as qcow2 files. */ } } else { /** - We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning - it doesn't benefit us. It's better to keep the current code in place which works + We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning + it doesn't benefit us. It's better to keep the current code in place which works */ srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool, sourcePath)); srcFile.setFormat(sourceFormat); @@ -1737,6 +1725,7 @@ private void deleteVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtEx vol.delete(0); } + private void deleteDirVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtException { Script.runSimpleBashScript("rm -r --interactive=never " + vol.getPath()); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java index 910f0eb15e0b..a8c32baa6ef3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java @@ -213,7 +213,7 @@ public boolean refresh() { @Override public boolean isExternalSnapshot() { - if (this.type == StoragePoolType.CLVM || type == StoragePoolType.RBD) { + if (this.type == StoragePoolType.CLVM || this.type == StoragePoolType.CLVM_NG || type == StoragePoolType.RBD) { return true; } return false; @@ -278,6 +278,10 @@ public StoragePoolType getType() { return this.type; } + public void setType(StoragePoolType type) { + this.type = type; + } + public StoragePool getPool() { return this._pool; } @@ -420,8 +424,4 @@ public Boolean hasVmActivity(HAStoragePool pool, HostTO host, Duration activityS return true; } } - - public void setType(StoragePoolType type) { - this.type = type; - } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java index 76b5a413e70f..fb474a6bc7d3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/StorageAdaptor.java @@ -148,4 +148,8 @@ default Ternary, String> prepareStorageClient(Strin default Pair unprepareStorageClient(String uuid, Map details) { return new Pair<>(true, ""); } + + default void createTemplate(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + // no-op + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index b3bdafb73751..ce431d9bab26 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -2728,8 +2728,11 @@ public void testCreateStoragePoolCommand() { @Test public void testModifyStoragePoolCommand() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); final KVMStoragePool kvmStoragePool = Mockito.mock(KVMStoragePool.class); @@ -2753,8 +2756,11 @@ public void testModifyStoragePoolCommand() { @Test public void testModifyStoragePoolCommandFailure() { - final StoragePool pool = Mockito.mock(StoragePool.class);; + final StoragePool pool = Mockito.mock(StoragePool.class); final ModifyStoragePoolCommand command = new ModifyStoragePoolCommand(true, pool); + Map details = new HashMap<>(); + details.put(KVMStoragePool.CLVM_SECURE_ZERO_FILL, "false"); + command.setDetails(details); final KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); @@ -7245,6 +7251,307 @@ public void getInterfaceTestInvalidMacAddressThrowCloudRuntimeException() { libvirtComputingResourceSpy.getInterface(connMock, vmName, invalidMacAddress); } + @Test + public void testExtractVolumeGroupFromPath_ValidPath() { + String devicePath = "/dev/vg1/volume-123"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_ComplexVGName() { + String devicePath = "/dev/cloudstack-vg-primary/volume-456"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("cloudstack-vg-primary", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MultiLevelPath() { + String devicePath = "/dev/vg-cluster-01/lv-data-001"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-cluster-01", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NullPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(null); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_EmptyPath() { + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(""); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_NonDevPath() { + String devicePath = "/var/lib/libvirt/images/disk.qcow2"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_InvalidFormat() { + String devicePath = "/dev/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertNull(vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_OnlyVG() { + String devicePath = "/dev/vg1"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + // Implementation extracts parts[2] regardless of whether there's an LV name + assertEquals("vg1", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_MapperPath() { + String devicePath = "/dev/mapper/vg1-volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("mapper", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithDashes() { + String devicePath = "/dev/vg-name-with-dashes/lv-name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg-name-with-dashes", vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_WithUnderscores() { + String devicePath = "/dev/vg_name_with_underscores/lv_name"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg_name_with_underscores", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NullVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(null); + assertFalse(result); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_EmptyVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(""); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ValidPath() { + try { + String volumePath = "/dev/test-vg/test-lv"; + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_ValidPath() { + String volumePath = "/dev/test-vg/test-lv"; + + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_NullPath() { + LibvirtComputingResource.deactivateClvmVolume(null); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_NullPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(null); + assertTrue(true); // Passes if no exception + } + + @Test + public void testDeactivateClvmVolume_EmptyPath() { + LibvirtComputingResource.deactivateClvmVolume(""); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_EmptyPath() { + LibvirtComputingResource.setClvmVolumeToSharedMode(""); + assertTrue(true); + } + + @Test + public void testDeactivateClvmVolume_InvalidPath() { + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.deactivateClvmVolume(invalidPath); + assertTrue(true); + } + + @Test + public void testSetClvmVolumeToSharedMode_InvalidPath() { + // Should handle invalid path gracefully without throwing + String invalidPath = "/invalid/path/that/does/not/exist"; + LibvirtComputingResource.setClvmVolumeToSharedMode(invalidPath); + assertTrue(true); // Passes if no exception + } + + @Test + public void testExtractVolumeGroupFromPath_RealWorldPaths() { + assertEquals("acsvg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/acsvg/volume-123")); + assertEquals("cloudstack-primary", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/cloudstack-primary/vm-disk-1")); + assertEquals("ceph-vg", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/ceph-vg/snapshot-456")); + assertEquals("vg01", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg01/data")); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_NonExistentVG() { + String nonExistentVG = "non-existent-vg-" + System.currentTimeMillis(); + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(nonExistentVG); + assertFalse(result); + } + + @Test + public void testActivateClvmVolumeExclusive_ComplexPath() { + try { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.activateClvmVolumeExclusive(complexPath); + } catch (Exception e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Should be LVM-related error", + message.contains("lvm") || + message.contains("lvchange") || + message.contains("volume") || + message.contains("not found") || + message.contains("failed")); + } + } + + @Test + public void testDeactivateClvmVolume_ComplexPath() { + String complexPath = "/dev/cloudstack-vg-primary-cluster-01/volume-123-456-789-abc"; + LibvirtComputingResource.deactivateClvmVolume(complexPath); + assertTrue(true); + } + + @Test + public void testExtractVolumeGroupFromPath_SpecialCharacters() { + assertEquals("vg.name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg.name/lv")); + assertEquals("vg_name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg_name/lv")); + assertEquals("vg-name", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg-name/lv")); + assertEquals("vg123", LibvirtComputingResource.extractVolumeGroupFromPath("/dev/vg123/lv456")); + } + + @Test + public void testExtractVolumeGroupFromPath_TrailingSlash() { + String devicePath = "/dev/vg1/volume-123/"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_WhitespaceVGName() { + boolean result = LibvirtComputingResource.checkIfVolumeGroupIsClustered(" "); + assertFalse(result); + } + + @Test + public void testExtractVolumeGroupFromPath_DevMapperExcluded() { + String mapperPath1 = "/dev/mapper/vg1-lv1"; + String mapperPath2 = "/dev/mapper/cloudstack--vg-volume--1"; + + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath1)); + assertEquals("mapper", LibvirtComputingResource.extractVolumeGroupFromPath(mapperPath2)); + } + + @Test + public void testExtractVolumeGroupFromPath_EdgeCases() { + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("/dev/")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("dev/vg/lv")); + assertNull(LibvirtComputingResource.extractVolumeGroupFromPath("//dev//vg//lv")); + } + + @Test + public void testClvmVolumeActivationSequence() { + // Test a typical sequence: deactivate -> activate exclusive -> deactivate -> shared + String volumePath = "/dev/test-vg/test-volume"; + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + + try { + LibvirtComputingResource.activateClvmVolumeExclusive(volumePath); + } catch (Exception e) { + // Expected in test environment + } + + LibvirtComputingResource.deactivateClvmVolume(volumePath); + LibvirtComputingResource.setClvmVolumeToSharedMode(volumePath); + + assertTrue(true); // Test passes if sequence completes + } + + @Test + public void testExtractVolumeGroupFromPath_LongVGName() { + String longVGName = "a".repeat(100); + String devicePath = "/dev/" + longVGName + "/volume"; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals(longVGName, vgName); + } + + @Test + public void testExtractVolumeGroupFromPath_LongLVName() { + String longLVName = "volume-" + "b".repeat(100); + String devicePath = "/dev/vg1/" + longLVName; + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(devicePath); + assertEquals("vg1", vgName); + } + + @Test + public void testCheckIfVolumeGroupIsClustered_SpecialCharactersInName() { + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg.test.name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg_test_name")); + assertFalse(LibvirtComputingResource.checkIfVolumeGroupIsClustered("vg-test-name")); + } + + @Test + public void testClvmMethodsWithMultiplePaths() { + String[] paths = { + "/dev/vg1/vol1", + "/dev/vg2/vol2", + "/dev/cloudstack-primary/vol3", + "/dev/test-vg/test-vol" + }; + + for (String path : paths) { + LibvirtComputingResource.deactivateClvmVolume(path); + LibvirtComputingResource.setClvmVolumeToSharedMode(path); + + String vgName = LibvirtComputingResource.extractVolumeGroupFromPath(path); + assertNotNull("Should extract VG from: " + path, vgName); + + boolean clustered = LibvirtComputingResource.checkIfVolumeGroupIsClustered(vgName); + } + + assertTrue(true); // Passes if all paths processed + } + @Test public void updateCpuQuotaAndPeriodTestAssertPeriodAndQuotaAreNotUpdatedWhenLibvirtVersionIsLessThanTheMinimum() throws LibvirtException { libvirtComputingResourceSpy.hypervisorLibvirtVersion = 8999; diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java new file mode 100644 index 000000000000..5d11cf209a45 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtClvmLockTransferCommandWrapperTest.java @@ -0,0 +1,462 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.utils.script.Script; + +/** + * Tests for LibvirtClvmLockTransferCommandWrapper + */ +@RunWith(MockitoJUnitRunner.class) +public class LibvirtClvmLockTransferCommandWrapperTest { + + @Mock + private LibvirtComputingResource libvirtComputingResource; + + private LibvirtClvmLockTransferCommandWrapper wrapper; + + private static final String TEST_LV_PATH = "/dev/vg1/volume-123"; + private static final String TEST_VOLUME_UUID = "test-volume-uuid-456"; + + @Before + public void setUp() { + wrapper = new LibvirtClvmLockTransferCommandWrapper(); + } + + @Test + public void testExecute_DeactivateSuccess() { + ClvmLockTransferCommand cmd = new ClvmLockTransferCommand( + ClvmLockTransferCommand.Operation.DEACTIVATE, + TEST_LV_PATH, + TEST_VOLUME_UUID + ); + + try (MockedConstruction + + diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 4145eeb9be6d..ecff34b2949d 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -97,7 +97,7 @@ {{ $t(dataResource[item].toLowerCase()) }} {{ dataResource[item] }} -
    +
    {{ $toLocaleDate(dataResource[item]) }}
    {{ dataResource[item] }}
    @@ -168,7 +168,7 @@
    {{ dataResource[item] }}
    - +
    {{ $t('label.' + item.replace('date', '.date.and.time'))}}
    diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 996e30ead3b5..41c822de238d 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -120,12 +120,18 @@ -
    +
    {{ $t('label.status') }}
    +
    +
    {{ $t('label.apikeyaccess') }}
    +
    + +
    +
    {{ $t('label.allocationstate') }}
    @@ -159,6 +165,42 @@
    +
    +
    + + + {{ $t('label.apikey') }} + + +
    + {{ resource.apikey.substring(0, 20) }}... +
    +

    +
    + + + {{ $t('label.secretkey') }} + + +
    + {{ resource.secretkey.substring(0, 20) }}... +
    +
    +
    {{ $t('label.ostypename') }}
    @@ -609,7 +651,8 @@
    - {{ resource.templatedisplaytext || resource.templatename || resource.templateid }} + {{ resource.templatedisplaytext || resource.templatename || resource.templateid }} + {{ resource.templatedisplaytext || resource.templatename || resource.templateid }}
    @@ -617,7 +660,8 @@
    - {{ resource.isodisplaytext || resource.isoname || resource.isoid }} + {{ resource.isodisplaytext || resource.isoname || resource.isoid }} + {{ resource.isodisplaytext || resource.isoname || resource.isoid }}
    @@ -794,6 +838,14 @@ {{ resource.account }}
    +
    +
    {{ $t('label.user') }}
    +
    + + {{ resource.username }} + {{ resource.username }} +
    +
    {{ $t('label.role') }}
    @@ -881,54 +933,6 @@ :osCategoryId="osCategoryId" />
    - - - - -
    +
    @@ -607,7 +607,7 @@ export default { const cluster = this.clusters.find(cluster => cluster.id === this.form.cluster) this.hypervisorType = cluster.hypervisortype if (this.hypervisorType === 'KVM') { - this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'Gluster', 'Linstor', 'custom', 'FiberChannel'] + this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'CLVM_NG', 'Gluster', 'Linstor', 'custom', 'FiberChannel'] if (this.form.scope === 'host') { this.protocols.push('Filesystem') } @@ -729,6 +729,15 @@ export default { } return url }, + clvmNgURL (vgname) { + var url + if (vgname.indexOf('://') === -1) { + url = 'clvm_ng://localhost/' + vgname + } else { + url = vgname + } + return url + }, vmfsURL (server, path) { var url if (server.indexOf('://') === -1) { @@ -853,6 +862,9 @@ export default { } else if (values.protocol === 'CLVM') { var vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup url = this.clvmURL(vg) + } else if (values.protocol === 'CLVM_NG') { + vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup + url = this.clvmNgURL(vg) } else if (values.protocol === 'RBD') { url = this.rbdURL(values.radosmonitor, values.radospool, values.radosuser, values.radossecret) if (values.datapool) { diff --git a/ui/src/views/infra/HostUpdate.vue b/ui/src/views/infra/HostUpdate.vue index 1fee7a6846c0..5ef762443e9d 100644 --- a/ui/src/views/infra/HostUpdate.vue +++ b/ui/src/views/infra/HostUpdate.vue @@ -64,7 +64,22 @@ - + + + + + + + + + + @@ -149,6 +164,7 @@ export default { methods: { initForm () { this.formRef = ref() + const guestOsRule = this.resource?.guestosrule this.form = reactive({ name: this.resource.name, hosttags: this.resource.explicithosttags, @@ -157,7 +173,9 @@ export default { ? this.resource.storageaccessgroups.split(',') : [], oscategoryid: this.resource.oscategoryid, - externaldetails: this.resourceExternalDetails + externaldetails: this.resourceExternalDetails, + guestosasrule: guestOsRule !== undefined, + guestosrule: guestOsRule }) this.rules = reactive({}) }, @@ -194,7 +212,11 @@ export default { params.id = this.resource.id params.name = values.name params.hosttags = values.hosttags - params.oscategoryid = values.oscategoryid + if (values.guestosasrule === true) { + params.guestosrule = values.guestosrule + } else { + params.oscategoryid = values.oscategoryid || this.osCategories.opts.filter(os => os.name === 'None')[0]?.id + } if (values.istagarule !== undefined) { params.istagarule = values.istagarule } @@ -205,9 +227,11 @@ export default { } else { params.cleanupexternaldetails = true } + + Object.keys(params).forEach((key) => (params[key] == null) && delete params[key]) this.loading = true - postAPI('updateHost', params).then(json => { + postAPI('updateHost', params).then(() => { this.$message.success({ content: `${this.$t('label.action.update.host')} - ${values.name}`, duration: 2 diff --git a/ui/src/views/network/NicsTab.vue b/ui/src/views/network/NicsTab.vue index 4c99f4f1c511..8e21db5b34d8 100644 --- a/ui/src/views/network/NicsTab.vue +++ b/ui/src/views/network/NicsTab.vue @@ -222,6 +222,10 @@ :placeholder="$t('label.new.secondaryip.description')" v-model:value="newSecondaryIp" v-focus="editNicResource.type!=='Shared'"> + +
    @@ -326,6 +330,7 @@ export default { secondaryIPs: [], selectedNicId: '', newSecondaryIp: '', + newSecondaryIpDescription: '', editNicResource: {}, listIps: { loading: false, @@ -401,6 +406,7 @@ export default { this.addNetworkData.makedefault = false this.editIpAddressValue = '' this.newSecondaryIp = '' + this.newSecondaryIpDescription = '' }, onChangeIPAddress (record) { this.editNicResource = record.nic @@ -589,6 +595,7 @@ export default { if (this.newSecondaryIp) { params.ipaddress = this.newSecondaryIp } + params.description = this.newSecondaryIpDescription postAPI('addIpToNic', params).then(response => { this.$pollJob({ @@ -616,6 +623,7 @@ export default { this.loadingNic = false }).finally(() => { this.newSecondaryIp = null + this.newSecondaryIpDescription = null this.fetchPublicIps(this.editNetworkId) }) }, diff --git a/ui/src/views/network/NicsTable.vue b/ui/src/views/network/NicsTable.vue index 11ba135e39a2..ec4a25afb3d0 100644 --- a/ui/src/views/network/NicsTable.vue +++ b/ui/src/views/network/NicsTable.vue @@ -39,7 +39,7 @@ {{ record.traffictype }} - {{ record.secondaryip.map(x => x.ipaddress).join(', ') }} + {{ record.secondaryip.map(secondaryIp => secondaryIp.description ? (secondaryIp.ipaddress + ': ' + secondaryIp.description) : secondaryIp.ipaddress).join(', ') }} {{ record.ip6address }} diff --git a/utils/src/main/java/com/cloud/utils/UriUtils.java b/utils/src/main/java/com/cloud/utils/UriUtils.java index a4654091e0cb..afbaa386c1ea 100644 --- a/utils/src/main/java/com/cloud/utils/UriUtils.java +++ b/utils/src/main/java/com/cloud/utils/UriUtils.java @@ -641,6 +641,9 @@ public static UriInfo getUriInfo(String url) { if (url.startsWith("rbd://")) { return getRbdUrlInfo(url); } + if (url.toLowerCase().startsWith("clvm://") || url.toLowerCase().startsWith("clvm_ng://")) { + return getClvmUrlInfo(url); + } URI uri = new URI(UriUtils.encodeURIComponent(url)); return new UriInfo(uri.getScheme(), uri.getHost(), uri.getPath(), uri.getUserInfo(), uri.getPort()); } catch (URISyntaxException e) { @@ -678,6 +681,36 @@ private static UriInfo getRbdUrlInfo(String url) { } } + private static UriInfo getClvmUrlInfo(String url) { + if (url == null || (!url.toLowerCase().startsWith("clvm://") && !url.toLowerCase().startsWith("clvm_ng://"))) { + throw new CloudRuntimeException("CLVM URL must start with \"clvm://\" or \"clvm_ng://\""); + } + + String scheme; + String remainder; + if (url.toLowerCase().startsWith("clvm_ng://")) { + scheme = "clvm_ng"; + remainder = url.substring(10); + } else { + scheme = "clvm"; + remainder = url.substring(7); + } + + int firstSlash = remainder.indexOf('/'); + String host = (firstSlash == -1) ? remainder : remainder.substring(0, firstSlash); + String path = (firstSlash == -1) ? "/" : remainder.substring(firstSlash); + + if (host.isEmpty()) { + host = "localhost"; + } + + while (path.startsWith("//")) { + path = path.substring(1); + } + + return new UriInfo(scheme, host, path, null, -1); + } + public static boolean isUrlForCompressedFile(String url) { return UriUtils.COMPRESSION_FORMATS.stream().anyMatch(f -> url.toLowerCase().endsWith(f)); } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java similarity index 61% rename from utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java rename to utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java index b30135673524..506bbc1ea5c3 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/TagAsRuleHelper.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/GenericRuleHelper.java @@ -16,41 +16,60 @@ //under the License. package org.apache.cloudstack.utils.jsinterpreter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import com.cloud.utils.StringUtils; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.cloud.utils.exception.CloudRuntimeException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; -public class TagAsRuleHelper { +public class GenericRuleHelper { - protected static Logger LOGGER = LogManager.getLogger(TagAsRuleHelper.class); + protected static Logger LOGGER = LogManager.getLogger(GenericRuleHelper.class); - public static boolean interpretTagAsRule(String rule, String tags, long timeout) { + public static boolean interpretTagAsRule(String rule, String tags, long timeout, String configName) { List tagsPresetVariable = new ArrayList<>(); if (!StringUtils.isEmpty(tags)) { tagsPresetVariable.addAll(Arrays.asList(tags.split(","))); } - try (JsInterpreter jsInterpreter = new JsInterpreter(timeout)) { - jsInterpreter.injectVariable("tags", tagsPresetVariable); - Object scriptReturn = jsInterpreter.executeScript(rule); + Boolean scriptReturn = interpretRule("tags", tagsPresetVariable, timeout, rule, configName); + + if (scriptReturn != null) { + return scriptReturn; + } + + LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", rule); + return false; + } + + public static boolean interpretGuestOsRule(String rule, String vmGuestOs, long timeout, String configName) { + Boolean scriptReturn = interpretRule("vmGuestOs", vmGuestOs, timeout, rule, configName); + + if (scriptReturn != null) { + return scriptReturn; + } + + LOGGER.debug("Result of guest OS rule [{}] was not a boolean, returning false.", rule); + return false; + } + + private static Boolean interpretRule(String variableName, Object variableValue, long timeout, String script, String configName) { + try (JsInterpreter jsInterpreter = new JsInterpreter(timeout, configName)) { + jsInterpreter.injectVariable(variableName, variableValue); + Object scriptReturn = jsInterpreter.executeScript(script); if (scriptReturn instanceof Boolean) { - return (Boolean)scriptReturn; + return (Boolean) scriptReturn; } } catch (IOException ex) { - String message = String.format("Error while executing script [%s].", rule); + String message = String.format("Error while executing script [%s].", script); LOGGER.error(message, ex); throw new CloudRuntimeException(message, ex); } - - LOGGER.debug("Result of tag rule [{}] was not a boolean, returning false.", rule); - return false; + return null; } } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java index 6e6ef2bbe599..85799e0f5614 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreter.java @@ -86,10 +86,10 @@ static final class DenyAllClassFilter implements ClassFilter { */ protected JsInterpreter() { } - public JsInterpreter(long timeout) { + public JsInterpreter(long timeout, String configName) { this.timeout = timeout; - this.timeoutDefaultMessage = String.format( - "Timeout (in milliseconds) defined in the global setting [quota.activationrule.timeout]: [%s].", this.timeout); + this.timeoutDefaultMessage = String.format("Timeout (in milliseconds) defined in the global setting [%s]: [%s].", + configName, this.timeout); if (System.getProperty("nashorn.args") == null) { System.setProperty("nashorn.args", "--no-java --no-syntax-extensions"); diff --git a/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java b/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java index 14d24dbb6410..52642cf03707 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java @@ -30,8 +30,11 @@ public class ServerPropertiesUtil { private static final Logger logger = LoggerFactory.getLogger(ServerPropertiesUtil.class); + protected static final String PROPERTIES_FILE = "server.properties"; protected static final AtomicReference propertiesRef = new AtomicReference<>(); + protected static final String KEYSTORE_FILE = "https.keystore"; + protected static final String KEYSTORE_PASSWORD = "https.keystore.password"; public static String getProperty(String name) { Properties props = propertiesRef.get(); @@ -55,4 +58,12 @@ public static String getProperty(String name) { } return tempProps.getProperty(name); } + + public static String getKeystoreFile() { + return getProperty(KEYSTORE_FILE); + } + + public static String getKeystorePassword() { + return getProperty(KEYSTORE_PASSWORD); + } } diff --git a/utils/src/test/java/com/cloud/utils/UriUtilsTest.java b/utils/src/test/java/com/cloud/utils/UriUtilsTest.java index 8e6408f77835..c6c059b2ebe2 100644 --- a/utils/src/test/java/com/cloud/utils/UriUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/UriUtilsTest.java @@ -283,4 +283,180 @@ public void validateUrl() { Pair url2 = UriUtils.validateUrl("https://www.apache.org"); Assert.assertEquals(url2.first(), "www.apache.org"); } + + @Test + public void testGetClvmUriInfoBasic() { + String host = "10.11.12.13"; + + String url1 = String.format("clvm://%s/vg0/lv0", host); + String url2 = String.format("clvm://%s/vg0", host); + String url3 = String.format("clvm://%s", host); + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm", info1.getScheme()); + Assert.assertEquals(host, info1.getStorageHost()); + Assert.assertEquals("/vg0/lv0", info1.getStoragePath()); + Assert.assertNull(info1.getUserInfo()); + Assert.assertEquals(-1, info1.getPort()); + Assert.assertEquals(url1, info1.toString()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm", info2.getScheme()); + Assert.assertEquals(host, info2.getStorageHost()); + Assert.assertEquals("/vg0", info2.getStoragePath()); + + UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3); + Assert.assertEquals("clvm", info3.getScheme()); + Assert.assertEquals(host, info3.getStorageHost()); + Assert.assertEquals("/", info3.getStoragePath()); + } + + @Test + public void testGetClvmNgUriInfoBasic() { + String host = "10.11.12.13"; + + String url1 = String.format("clvm_ng://%s/vg0/lv0", host); + String url2 = String.format("clvm_ng://%s/vg0", host); + String url3 = String.format("clvm_ng://%s", host); + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm_ng", info1.getScheme()); + Assert.assertEquals(host, info1.getStorageHost()); + Assert.assertEquals("/vg0/lv0", info1.getStoragePath()); + Assert.assertNull(info1.getUserInfo()); + Assert.assertEquals(-1, info1.getPort()); + Assert.assertEquals(url1, info1.toString()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm_ng", info2.getScheme()); + Assert.assertEquals(host, info2.getStorageHost()); + Assert.assertEquals("/vg0", info2.getStoragePath()); + + UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3); + Assert.assertEquals("clvm_ng", info3.getScheme()); + Assert.assertEquals(host, info3.getStorageHost()); + Assert.assertEquals("/", info3.getStoragePath()); + } + + @Test + public void testGetClvmUriInfoNoHost() { + String url1 = "clvm:///vg0/lv0"; + String url2 = "clvm_ng:///vg0/lv0"; + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm", info1.getScheme()); + Assert.assertEquals("localhost", info1.getStorageHost()); + Assert.assertEquals("/vg0/lv0", info1.getStoragePath()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm_ng", info2.getScheme()); + Assert.assertEquals("localhost", info2.getStorageHost()); + Assert.assertEquals("/vg0/lv0", info2.getStoragePath()); + } + + @Test + public void testGetClvmUriInfoMultipleSlashes() { + String url1 = "clvm://host1//vg0//lv0"; + String url2 = "clvm_ng://host2///vg1///lv1"; + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm", info1.getScheme()); + Assert.assertEquals("host1", info1.getStorageHost()); + Assert.assertEquals("/vg0//lv0", info1.getStoragePath()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm_ng", info2.getScheme()); + Assert.assertEquals("host2", info2.getStorageHost()); + Assert.assertEquals("/vg1///lv1", info2.getStoragePath()); + } + + @Test + public void testGetClvmUriInfoComplexPaths() { + String host = "storage-node1"; + String url1 = String.format("clvm://%s/vg-name-with-dashes/lv_name_with_underscores", host); + String url2 = String.format("clvm_ng://%s/vg.name.with.dots/lv-123-456", host); + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm", info1.getScheme()); + Assert.assertEquals(host, info1.getStorageHost()); + Assert.assertEquals("/vg-name-with-dashes/lv_name_with_underscores", info1.getStoragePath()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm_ng", info2.getScheme()); + Assert.assertEquals(host, info2.getStorageHost()); + Assert.assertEquals("/vg.name.with.dots/lv-123-456", info2.getStoragePath()); + } + + @Test + public void testGetClvmUriInfoHostnames() { + String[] hosts = { + "localhost", + "node1", + "storage-node-1", + "storage.example.com", + "10.0.0.1", + "192.168.1.100" + }; + + for (String host : hosts) { + String clvmUrl = String.format("clvm://%s/vg0/lv0", host); + String clvmNgUrl = String.format("clvm_ng://%s/vg0/lv0", host); + + UriUtils.UriInfo clvmInfo = UriUtils.getUriInfo(clvmUrl); + Assert.assertEquals("clvm", clvmInfo.getScheme()); + Assert.assertEquals(host, clvmInfo.getStorageHost()); + Assert.assertEquals("/vg0/lv0", clvmInfo.getStoragePath()); + + UriUtils.UriInfo clvmNgInfo = UriUtils.getUriInfo(clvmNgUrl); + Assert.assertEquals("clvm_ng", clvmNgInfo.getScheme()); + Assert.assertEquals(host, clvmNgInfo.getStorageHost()); + Assert.assertEquals("/vg0/lv0", clvmNgInfo.getStoragePath()); + } + } + + @Test + public void testGetClvmUriInfoToString() { + String url1 = "clvm://host1/vg0/lv0"; + String url2 = "clvm_ng://host2/vg1/lv1"; + String url3 = "clvm://localhost/vg0"; + + Assert.assertEquals(url1, UriUtils.getUriInfo(url1).toString()); + Assert.assertEquals(url2, UriUtils.getUriInfo(url2).toString()); + Assert.assertEquals(url3, UriUtils.getUriInfo(url3).toString()); + } + + @Test + public void testGetClvmUriInfoCaseInsensitive() { + String url1 = "CLVM://host1/vg0/lv0"; + String url2 = "ClVm://host2/vg1/lv1"; + String url3 = "CLVM_NG://host3/vg2/lv2"; + String url4 = "clvm_NG://host4/vg3/lv3"; + + UriUtils.UriInfo info1 = UriUtils.getUriInfo(url1); + Assert.assertEquals("clvm", info1.getScheme()); + Assert.assertEquals("host1", info1.getStorageHost()); + + UriUtils.UriInfo info2 = UriUtils.getUriInfo(url2); + Assert.assertEquals("clvm", info2.getScheme()); + Assert.assertEquals("host2", info2.getStorageHost()); + + UriUtils.UriInfo info3 = UriUtils.getUriInfo(url3); + Assert.assertEquals("clvm_ng", info3.getScheme()); + Assert.assertEquals("host3", info3.getStorageHost()); + + UriUtils.UriInfo info4 = UriUtils.getUriInfo(url4); + Assert.assertEquals("clvm_ng", info4.getScheme()); + Assert.assertEquals("host4", info4.getStorageHost()); + } + + @Test + public void testGetClvmUriInfoIntegration() { + String host = "clvm-host"; + + String clvmUrl = String.format("clvm://%s/vg0/lv0", host); + String clvmNgUrl = String.format("clvm_ng://%s/vg1/lv1", host); + + testGetUriInfoInternal(clvmUrl, host); + testGetUriInfoInternal(clvmNgUrl, host); + } } diff --git a/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java b/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java index b7df14dc85d0..e50eefd466bf 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/imagestore/ImageStoreUtilTest.java @@ -27,7 +27,7 @@ public class ImageStoreUtilTest { @Test public void testgenerateHttpsPostUploadUrl() throws MalformedURLException { - String ssvmdomain = "*.realhostip.com"; + String ssvmdomain = "*.example.com"; String ipAddress = "10.147.28.14"; String uuid = UUID.randomUUID().toString(); String protocol = "https"; @@ -47,7 +47,7 @@ public void testgenerateHttpsPostUploadUrl() throws MalformedURLException { @Test public void testgenerateHttpPostUploadUrl() throws MalformedURLException { - String ssvmdomain = "*.realhostip.com"; + String ssvmdomain = "*.example.com"; String ipAddress = "10.147.28.14"; String uuid = UUID.randomUUID().toString(); String protocol = "http"; diff --git a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java index e6ef43f2fc10..e4764b2af413 100644 --- a/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java +++ b/utils/src/test/java/org/apache/cloudstack/utils/jsinterpreter/JsInterpreterTest.java @@ -161,7 +161,7 @@ public void setScriptEngineDisablingJavaLanguageTest() { @Test public void executeScriptTestValidScriptShouldPassWithMixedVariables() { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.injectVariable("x", 10); jsInterpreter.injectVariable("y", "hello"); jsInterpreter.injectVariable("z", true); @@ -174,7 +174,7 @@ public void executeScriptTestValidScriptShouldPassWithMixedVariables() { } private void runMaliciousScriptFileTest(String script, String filename) { - try (JsInterpreter jsInterpreter = new JsInterpreter(1000)) { + try (JsInterpreter jsInterpreter = new JsInterpreter(1000, "timeout.configuration")) { jsInterpreter.executeScript(script); } catch (CloudRuntimeException ex) { Assert.assertTrue(ex.getMessage().contains("Unable to execute script"));