Merge remote-tracking branch 'origin/main' into quota-ui

This commit is contained in:
Fabricio Duarte 2026-06-24 22:15:40 -03:00
commit 54ce551db6
237 changed files with 20351 additions and 2931 deletions

View File

@ -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

View File

@ -1,5 +0,0 @@
**Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures.
```json
{"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}}
```

View File

@ -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 `<details><summary>Section Name</summary>` 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 `<details><summary>Section Name</summary>` 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]
<details>
<summary>View Detailed Results</summary>
[Comprehensive details, logs, traces]
</details>
<details>
<summary>View All Warnings</summary>
[Minor issues and potential problems]
</details>
### 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)

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d4b8988df7c60cd416200769cc6bc7f1aab5bb3128df0b9a83a35e061b4da111","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,19 +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/noop-reminder.md@359795d49ada21681ab616bd4cbcb144a7387115
# - github/gh-aw/.github/workflows/shared/reporting.md@359795d49ada21681ab616bd4cbcb144a7387115
# Source: githubnext/agentics/workflows/daily-issue-triage.md@d7c1dc4b72b00607a67caaffdcc216cb64379cf9
#
# Secrets used:
# - COPILOT_GITHUB_TOKEN
@ -37,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
@ -53,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:
@ -71,7 +68,7 @@ permissions: {}
concurrency:
group: "gh-aw-${{ github.workflow }}"
run-name: "Issue Triage Agent"
run-name: "Daily Issue Triage"
jobs:
activation:
@ -93,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"
@ -113,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"
@ -122,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:
@ -139,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: |
@ -164,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: |
@ -198,20 +194,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF'
cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF'
<system>
GH_AW_PROMPT_e560c36b9148ef78_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_e560c36b9148ef78_EOF'
cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF'
<safe-output-tools>
Tools: add_comment, add_labels, missing_tool, missing_data, noop
Tools: add_comment(max:10), add_labels(max:10), missing_tool, missing_data, noop
</safe-output-tools>
GH_AW_PROMPT_e560c36b9148ef78_EOF
GH_AW_PROMPT_7c51e8f15cc7af75_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF'
cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@ -240,14 +236,12 @@ jobs:
{{/if}}
</github-context>
GH_AW_PROMPT_e560c36b9148ef78_EOF
GH_AW_PROMPT_7c51e8f15cc7af75_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
cat << 'GH_AW_PROMPT_e560c36b9148ef78_EOF'
cat << 'GH_AW_PROMPT_7c51e8f15cc7af75_EOF'
</system>
{{#runtime-import .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_reporting.md}}
{{#runtime-import .github/aw/imports/github/gh-aw/359795d49ada21681ab616bd4cbcb144a7387115/.github_workflows_shared_noop-reminder.md}}
{{#runtime-import .github/workflows/issue-triage-agent.md}}
GH_AW_PROMPT_e560c36b9148ef78_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
@ -327,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:
@ -337,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 }}
@ -355,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"
@ -377,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
@ -399,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"
@ -436,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_c8eaf0ada4607ff7_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_c8eaf0ada4607ff7_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": []
@ -620,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
@ -640,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
@ -654,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_88b8311e90d25032_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": {
@ -668,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 }}
}
}
},
@ -695,7 +705,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
GH_AW_MCP_CONFIG_88b8311e90d25032_EOF
GH_AW_MCP_CONFIG_37cac1d5ee0c175c_EOF
- name: Mount MCP servers as CLIs
id: mount-mcp-clis
continue-on-error: true
@ -720,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
@ -907,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
@ -938,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:
@ -949,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"
@ -982,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"
@ -1000,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 }}
@ -1019,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: |
@ -1035,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: |
@ -1051,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 }}
@ -1073,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 }}
@ -1099,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"
@ -1128,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 ---
@ -1176,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: |
@ -1300,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 }}
@ -1324,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"
@ -1369,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: |

171
.github/workflows/daily-issue-triage.md vendored Normal file
View File

@ -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
<!-- Note - this file can be customized to your needs. Replace this section directly, or add further instructions here. After editing run 'gh aw compile' -->
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]
<details><summary>💡 Notes and suggestions</summary>
{Debugging strategies, reproduction steps, resource links, sub-task checklists, nudges for the team.}
</details>
```
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.

View File

@ -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'
<system>
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'
<safe-output-tools>
Tools: create_issue, missing_tool, missing_data, noop
</safe-output-tools>
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'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@ -237,12 +237,12 @@ jobs:
{{/if}}
</github-context>
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'
</system>
{{#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 }}

View File

@ -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

View File

@ -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

View File

@ -1,91 +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
- github/gh-aw/.github/workflows/shared/noop-reminder.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}
<details>
<summary>View Triage Details</summary>
#### 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}
</details>
**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 `<details>` 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.

View File

@ -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
@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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
@ -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');

View File

@ -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

View File

@ -43,7 +43,7 @@ import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.storage.object.ObjectStore;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.usage.Usage;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.schedule.ResourceSchedule;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterGuestIpv6Prefix;
@ -125,17 +125,18 @@ public class EventTypes {
public static final String EVENT_VM_UNMANAGE = "VM.UNMANAGE";
public static final String EVENT_VM_RECOVER = "VM.RECOVER";
// VM Schedule
public static final String EVENT_VM_SCHEDULE_CREATE = "VM.SCHEDULE.CREATE";
public static final String EVENT_VM_SCHEDULE_UPDATE = "VM.SCHEDULE.UPDATE";
public static final String EVENT_VM_SCHEDULE_DELETE = "VM.SCHEDULE.DELETE";
// VM Schedule action-execution events (fired when a scheduled action runs).
public static final String EVENT_VM_SCHEDULE_START = "VM.SCHEDULE.START";
public static final String EVENT_VM_SCHEDULE_STOP = "VM.SCHEDULE.STOP";
public static final String EVENT_VM_SCHEDULE_REBOOT = "VM.SCHEDULE.REBOOT";
public static final String EVENT_VM_SCHEDULE_FORCE_STOP = "VM.SCHEDULE.FORCE.STOP";
public static final String EVENT_VM_SCHEDULE_FORCE_REBOOT = "VM.SCHEDULE.FORCE.REBOOT";
// Generic Resource Schedule CRUD events (apply to all resource types).
public static final String EVENT_SCHEDULE_CREATE = "SCHEDULE.CREATE";
public static final String EVENT_SCHEDULE_UPDATE = "SCHEDULE.UPDATE";
public static final String EVENT_SCHEDULE_DELETE = "SCHEDULE.DELETE";
// Domain Router
public static final String EVENT_ROUTER_CREATE = "ROUTER.CREATE";
public static final String EVENT_ROUTER_DESTROY = "ROUTER.DESTROY";
@ -676,6 +677,7 @@ public class EventTypes {
public static final String EVENT_AUTOSCALEVMGROUP_DISABLE = "AUTOSCALEVMGROUP.DISABLE";
public static final String EVENT_AUTOSCALEVMGROUP_SCALEDOWN = "AUTOSCALEVMGROUP.SCALEDOWN";
public static final String EVENT_AUTOSCALEVMGROUP_SCALEUP = "AUTOSCALEVMGROUP.SCALEUP";
public static final String EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE = "AUTOSCALEVMGROUP.SCHEDULE.UPDATE";
public static final String EVENT_BAREMETAL_DHCP_SERVER_ADD = "PHYSICAL.DHCP.ADD";
public static final String EVENT_BAREMETAL_DHCP_SERVER_DELETE = "PHYSICAL.DHCP.DELETE";
@ -888,15 +890,18 @@ public class EventTypes {
entityEventDetails.put(EVENT_VM_IMPORT, VirtualMachine.class);
entityEventDetails.put(EVENT_VM_UNMANAGE, VirtualMachine.class);
// VMSchedule
entityEventDetails.put(EVENT_VM_SCHEDULE_CREATE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_DELETE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_UPDATE, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_START, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_STOP, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, VMSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, VMSchedule.class);
// VMSchedule action-execution events
entityEventDetails.put(EVENT_VM_SCHEDULE_START, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_STOP, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_REBOOT, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_STOP, ResourceSchedule.class);
entityEventDetails.put(EVENT_VM_SCHEDULE_FORCE_REBOOT, ResourceSchedule.class);
entityEventDetails.put(EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE, ResourceSchedule.class);
// Generic Resource Schedule
entityEventDetails.put(EVENT_SCHEDULE_CREATE, ResourceSchedule.class);
entityEventDetails.put(EVENT_SCHEDULE_UPDATE, ResourceSchedule.class);
entityEventDetails.put(EVENT_SCHEDULE_DELETE, ResourceSchedule.class);
entityEventDetails.put(EVENT_ROUTER_CREATE, VirtualRouter.class);
entityEventDetails.put(EVENT_ROUTER_DESTROY, VirtualRouter.class);

View File

@ -63,6 +63,9 @@ public interface Host extends StateObject<Status>, Identity, Partition, HAResour
String HOST_OVFTOOL_VERSION = "host.ovftool.version";
String HOST_VIRTV2V_VERSION = "host.virtv2v.version";
String HOST_SSH_PORT = "host.ssh.port";
String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count";
String GUEST_OS_CATEGORY_ID = "guest.os.category.id";
String GUEST_OS_RULE = "guest.os.rule";
int DEFAULT_SSH_PORT = 22;

View File

@ -232,7 +232,7 @@ public interface NetworkService {
/**
* 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);

View File

@ -170,6 +170,7 @@ public class Storage {
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

View File

@ -38,6 +38,8 @@ public interface NicSecondaryIp extends ControlledEntity, Identity, InternalIden
String getIp6Address();
String getDescription();
long getNetworkId();
long getVmId();

View File

@ -450,6 +450,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";
@ -1343,8 +1344,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";

View File

@ -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 class UpdateHostCmd extends BaseCmd {
return osCategoryId;
}
public String getGuestOsRule() {
return guestOsRule;
}
public String getAllocationState() {
return allocationState;
}

View File

@ -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 class RegisterUserKeysCmd extends BaseAsyncCmd {
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);
}

View File

@ -27,6 +27,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.command.user.vm.DeployVMCmd;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import com.cloud.event.EventTypes;
@ -51,6 +52,10 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd {
description = "If true, ejects the ISO before detaching on VMware. Default: false", since = "4.15.1")
protected Boolean forced;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class,
description = "The ID of the ISO to detach. Required when the Instance has more than one ISO attached.", since = "4.23.0")
protected Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -104,7 +109,7 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd {
@Override
public void execute() {
boolean result = _templateService.detachIso(virtualMachineId, null, isForced());
boolean result = _templateService.detachIso(virtualMachineId, id, isForced());
if (result) {
UserVm userVm = _entityMgr.findById(UserVm.class, virtualMachineId);
UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", userVm).get(0);

View File

@ -0,0 +1,132 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.schedule;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.apache.commons.lang3.EnumUtils;
import javax.inject.Inject;
import java.util.Date;
import java.util.Map;
@APICommand(name = "createResourceSchedule", description = "Create Resource Schedule", responseObject = ResourceScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class CreateResourceScheduleCmd extends BaseCmd {
@Inject
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource")
private String resourceType;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedule is to be defined")
private String resourceId;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the schedule")
private String description;
@Parameter(name = ApiConstants.SCHEDULE, type = CommandType.STRING, required = true, description = "Schedule for action on resource in cron format. e.g. '0 15 10 * *' for 'at 15:00 on 10th day of every month'")
private String schedule;
@Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, required = true, description = "Specifies a timezone for this command. For more information on the timezone parameter, see TimeZone Format.")
private String timeZone;
@Parameter(name = ApiConstants.ACTION, type = CommandType.STRING, required = true, description = "Action to take on the resource.")
private String action;
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active. Defaults to current date plus 1 minute. (Format \"yyyy-MM-dd hh:mm:ss\")")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive. (Format \"yyyy-MM-dd hh:mm:ss\")")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Enable schedule. Defaults to true")
private Boolean enabled;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = false, description = "Map of (key/value pairs) details for the schedule.")
private Map details;
public ApiCommandResourceType getResourceType() {
ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType);
if (type == null) {
throw new InvalidParameterValueException("Unknown resource type: " + resourceType);
}
return type;
}
public String getResourceId() {
return resourceId;
}
public String getDescription() {
return description;
}
public String getSchedule() {
return schedule;
}
public String getTimeZone() {
return timeZone;
}
public String getAction() {
return action;
}
public Date getStartDate() {
return startDate;
}
public Date getEndDate() {
return endDate;
}
public Boolean getEnabled() {
if (enabled == null) {
enabled = true;
}
return enabled;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
@Override
public void execute() {
ResourceScheduleResponse response = resourceScheduleManager.createSchedule(getResourceType(), getResourceId(),
getDescription(), getSchedule(), getTimeZone(), getAction(), getStartDate(), getEndDate(), getEnabled(), getDetails());
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getAccountId();
}
}

View File

@ -0,0 +1,86 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.schedule;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.apache.commons.lang3.EnumUtils;
import javax.inject.Inject;
import java.util.List;
@APICommand(name = "deleteResourceSchedule", description = "Delete Resource Schedule", responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class DeleteResourceScheduleCmd extends BaseCmd {
@Inject
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource")
private String resourceType;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedules are to be deleted")
private String resourceId;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "ID of the schedule to be deleted")
private Long id;
@Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "comma separated list of schedule ids to be deleted")
private List<Long> ids;
public ApiCommandResourceType getResourceType() {
ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType);
if (type == null) {
throw new InvalidParameterValueException("Unknown resource type: " + resourceType);
}
return type;
}
public String getResourceId() {
return resourceId;
}
public Long getId() {
return id;
}
public List<Long> getIds() {
return ids;
}
@Override
public void execute() {
resourceScheduleManager.removeSchedule(getResourceType(), getResourceId(), getId(), getIds());
SuccessResponse response = new SuccessResponse(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getAccountId();
}
}

View File

@ -0,0 +1,97 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.schedule;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.apache.commons.lang3.EnumUtils;
import javax.inject.Inject;
import java.util.List;
@APICommand(name = "listResourceSchedule", description = "List Resource Schedules", responseObject = ResourceScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ListResourceScheduleCmd extends BaseListCmd {
@Inject
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "ID of the schedule")
private Long id;
@Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = false, description = "comma separated list of schedule ids")
private List<Long> ids;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true, description = "Type of the resource.")
private String resourceType;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true, description = "ID of the resource for which schedules are to be listed.")
private String resourceId;
@Parameter(name = ApiConstants.ACTION, type = CommandType.STRING, required = false, description = "Action to take on the resource.")
private String action;
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Filter by enabled status.")
private Boolean enabled;
public Long getId() {
return id;
}
public List<Long> getIds() {
return ids;
}
public ApiCommandResourceType getResourceType() {
ApiCommandResourceType type = EnumUtils.getEnumIgnoreCase(ApiCommandResourceType.class, resourceType);
if (type == null) {
throw new InvalidParameterValueException("Unknown resource type: " + resourceType);
}
return type;
}
public String getResourceId() {
return resourceId;
}
public String getAction() {
return action;
}
public Boolean getEnabled() {
return enabled;
}
@Override
public void execute() {
ListResponse<ResourceScheduleResponse> response = resourceScheduleManager.listSchedule(
getId(), getIds(), getResourceType(), getResourceId(), getAction(), getEnabled(),
getStartIndex(), getPageSizeVal()
);
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -0,0 +1,108 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.schedule;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import javax.inject.Inject;
import java.util.Date;
import java.util.Map;
@APICommand(name = "updateResourceSchedule", description = "Update Resource Schedule", responseObject = ResourceScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class UpdateResourceScheduleCmd extends BaseCmd {
@Inject
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ResourceScheduleResponse.class, required = true, description = "ID of the schedule to be updated")
private Long id;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = false, description = "Description of the schedule")
private String description;
@Parameter(name = ApiConstants.SCHEDULE, type = CommandType.STRING, required = false, description = "Schedule for action on resource in cron format.")
private String schedule;
@Parameter(name = ApiConstants.TIMEZONE, type = CommandType.STRING, required = false, description = "Specifies a timezone for this command.")
private String timeZone;
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, required = false, description = "Start date from which the schedule becomes active.")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, required = false, description = "End date after which the schedule becomes inactive.")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, required = false, description = "Enable or disable the schedule.")
private Boolean enabled;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = false, description = "Map of (key/value pairs) details for the schedule.")
private Map details;
public Long getId() {
return id;
}
public String getDescription() {
return description;
}
public String getSchedule() {
return schedule;
}
public String getTimeZone() {
return timeZone;
}
public Date getStartDate() {
return startDate;
}
public Date getEndDate() {
return endDate;
}
public Boolean getEnabled() {
return enabled;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
@Override
public void execute() {
ResourceScheduleResponse response = resourceScheduleManager.updateSchedule(getId(), getDescription(), getSchedule(),
getTimeZone(), getStartDate(), getEndDate(), getEnabled(), getDetails());
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getAccountId();
}
}

View File

@ -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 class AddIpToVmNicCmd extends BaseAsyncCreateCmd {
}
try {
result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair);
result = _networkService.allocateSecondaryGuestIP(getNicId(), requestedIpPair, description);
if (result != null) {
setEntityId(result.getId());
setEntityUuid(result.getUuid());

View File

@ -22,23 +22,26 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import javax.inject.Inject;
import java.util.Date;
@Deprecated
@APICommand(name = "createVMSchedule", description = "Create Instance Schedule", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class CreateVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
@ -75,14 +78,14 @@ public class CreateVMScheduleCmd extends BaseCmd {
type = CommandType.DATE,
required = false,
description = "Start date from which the schedule becomes active. Defaults to current date plus 1 minute."
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
+ "(Format \"yyyy-MM-dd hh:mm:ss\")")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE,
type = CommandType.DATE,
required = false,
description = "End date after which the schedule becomes inactive"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
+ "(Format \"yyyy-MM-dd hh:mm:ss\")")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED,
@ -91,9 +94,9 @@ public class CreateVMScheduleCmd extends BaseCmd {
description = "Enable Instance schedule. Defaults to true")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////////// Accessors ///////////////////////
/// //////////////////////////////////////////////////
public Long getVmId() {
return vmId;
@ -130,13 +133,19 @@ public class CreateVMScheduleCmd extends BaseCmd {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////// API Implementation///////////////////
/// //////////////////////////////////////////////////
@Override
public void execute() {
VMScheduleResponse response = vmScheduleManager.createSchedule(this);
String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null;
ResourceScheduleResponse scheduleResponse = resourceScheduleManager.createSchedule(
ApiCommandResourceType.VirtualMachine,
resourceIdStr, getDescription(), getSchedule(), getTimeZone(), getAction(),
getStartDate(), getEndDate(), getEnabled(), null);
VMScheduleResponse response = new VMScheduleResponse(scheduleResponse);
response.setResponseName(getCommandName());
setResponseObject(response);
}

View File

@ -22,6 +22,7 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
@ -30,19 +31,19 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
@Deprecated
@APICommand(name = "deleteVMSchedule", description = "Delete Instance Schedule.", responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class DeleteVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
@ -50,12 +51,14 @@ public class DeleteVMScheduleCmd extends BaseCmd {
required = true,
description = "ID of Instance")
private Long vmId;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = VMScheduleResponse.class,
required = false,
description = "ID of Instance schedule")
private Long id;
@Parameter(name = ApiConstants.IDS,
type = CommandType.LIST,
collectionType = CommandType.UUID,
@ -64,9 +67,9 @@ public class DeleteVMScheduleCmd extends BaseCmd {
description = "IDs of Instance schedule")
private List<Long> ids;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////////// Accessors ///////////////////////
/// //////////////////////////////////////////////////
public Long getId() {
return id;
@ -83,18 +86,21 @@ public class DeleteVMScheduleCmd extends BaseCmd {
return vmId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////// API Implementation///////////////////
/// //////////////////////////////////////////////////
@Override
public void execute() {
long rowsRemoved = vmScheduleManager.removeSchedule(this);
String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null;
long rowsRemoved = resourceScheduleManager.removeSchedule(
ApiCommandResourceType.VirtualMachine,
resourceIdStr, getId(), getIds());
if (rowsRemoved > 0) {
final SuccessResponse response = new SuccessResponse();
response.setResponseName(getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
response.setObjectName("vmschedule");
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete Instance Schedules");

View File

@ -20,23 +20,27 @@ package org.apache.cloudstack.api.command.user.vm;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@Deprecated
@APICommand(name = "listVMSchedule", description = "List Instance Schedules.", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ListVMScheduleCmd extends BaseListCmd {
@Inject
VMScheduleManager vmScheduleManager;
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
@ -61,12 +65,12 @@ public class ListVMScheduleCmd extends BaseListCmd {
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
required = false,
description = "ID of Instance schedule")
description = "Filter by enabled status")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////////// Accessors ///////////////////////
/// //////////////////////////////////////////////////
public Long getVmId() {
return vmId;
@ -84,14 +88,26 @@ public class ListVMScheduleCmd extends BaseListCmd {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////// API Implementation///////////////////
/// //////////////////////////////////////////////////
@Override
public void execute() {
ListResponse<VMScheduleResponse> response = vmScheduleManager.listSchedule(this);
String resourceIdStr = getVmId() != null ? String.valueOf(getVmId()) : null;
ListResponse<ResourceScheduleResponse> scheduleResponse = resourceScheduleManager.listSchedule(
getId(), null, ApiCommandResourceType.VirtualMachine, resourceIdStr, getAction(), getEnabled(),
getStartIndex(), getPageSizeVal()
);
List<VMScheduleResponse> vmScheduleResponses = new ArrayList<>();
for (ResourceScheduleResponse resourceScheduleResponse : scheduleResponse.getResponses()) {
vmScheduleResponses.add(new VMScheduleResponse(resourceScheduleResponse));
}
ListResponse<VMScheduleResponse> response = new ListResponse<>();
response.setResponses(vmScheduleResponses, scheduleResponse.getCount());
response.setResponseName(getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
response.setObjectName("vmschedule");
setResponseObject(response);
}
}

View File

@ -22,22 +22,25 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceSchedule;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import javax.inject.Inject;
import java.util.Date;
@Deprecated
@APICommand(name = "updateVMSchedule", description = "Update Instance Schedule.", responseObject = VMScheduleResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class UpdateVMScheduleCmd extends BaseCmd {
@Inject
VMScheduleManager vmScheduleManager;
ResourceScheduleManager resourceScheduleManager;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
@ -68,14 +71,14 @@ public class UpdateVMScheduleCmd extends BaseCmd {
type = CommandType.DATE,
required = false,
description = "Start date from which the schedule becomes active"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
+ "(Format \"yyyy-MM-dd hh:mm:ss\")")
private Date startDate;
@Parameter(name = ApiConstants.END_DATE,
type = CommandType.DATE,
required = false,
description = "End date after which the schedule becomes inactive"
+ "Use format \"yyyy-MM-dd hh:mm:ss\")")
+ "(Format \"yyyy-MM-dd hh:mm:ss\")")
private Date endDate;
@Parameter(name = ApiConstants.ENABLED,
@ -84,9 +87,9 @@ public class UpdateVMScheduleCmd extends BaseCmd {
description = "Enable Instance schedule")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////////// Accessors ///////////////////////
/// //////////////////////////////////////////////////
public Long getId() {
return id;
@ -116,24 +119,29 @@ public class UpdateVMScheduleCmd extends BaseCmd {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
/// //////////////////////////////////////////////////
/// //////////// API Implementation///////////////////
/// //////////////////////////////////////////////////
@Override
public void execute() {
VMScheduleResponse response = vmScheduleManager.updateSchedule(this);
ResourceScheduleResponse scheduleResponse = resourceScheduleManager.updateSchedule(
getId(), getDescription(), getSchedule(), getTimeZone(), getStartDate(), getEndDate(), getEnabled(), null);
VMScheduleResponse response = new VMScheduleResponse(scheduleResponse);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
VMSchedule vmSchedule = _entityMgr.findById(VMSchedule.class, getId());
if (vmSchedule == null) {
throw new InvalidParameterValueException(String.format("Unable to find vmSchedule by id=%d", getId()));
ResourceSchedule schedule = _entityMgr.findById(ResourceSchedule.class, getId());
if (schedule == null || !ApiCommandResourceType.VirtualMachine.equals(schedule.getResourceType())) {
throw new InvalidParameterValueException(String.format("Unable to find VM schedule by id=%d", getId()));
}
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, schedule.getResourceId());
if (vm == null) {
throw new InvalidParameterValueException(String.format("Unable to find VM schedule by id=%d", getId()));
}
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, vmSchedule.getVmId());
return vm.getAccountId();
}
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.response;
import org.apache.cloudstack.api.BaseResponse;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
public class AttachedIsoResponse extends BaseResponse {
@SerializedName("id")
@Param(description = "The ID of the attached ISO")
private String id;
@SerializedName("name")
@Param(description = "The name of the attached ISO")
private String name;
@SerializedName("displaytext")
@Param(description = "The display text of the attached ISO")
private String displayText;
@SerializedName("deviceseq")
@Param(description = "The cdrom slot that holds this ISO (3=hdc, 4=hdd, ...)")
private Integer deviceSeq;
@SerializedName("bootable")
@Param(description = "Whether this is the bootable ISO for the VM")
private Boolean bootable;
public AttachedIsoResponse() {
}
public AttachedIsoResponse(String id, String name, String displayText, Integer deviceSeq, boolean bootable) {
this.id = id;
this.name = name;
this.displayText = displayText;
this.deviceSeq = deviceSeq;
this.bootable = bootable;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getDisplayText() {
return displayText;
}
public Integer getDeviceSeq() {
return deviceSeq;
}
public Boolean getBootable() {
return bootable;
}
}

View File

@ -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 class HostResponse extends BaseResponseWithAnnotations {
public String getExtensionName() {
return extensionName;
}
public String getGuestOsRule() {
return guestOsRule;
}
public void setGuestOsRule(String guestOsRule) {
this.guestOsRule = guestOsRule;
}
}

View File

@ -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 class NicSecondaryIpResponse extends BaseResponse {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<NicSecondaryIpResponse> getSecondaryIpsList() {
return secondaryIpsList;
}

View File

@ -0,0 +1,178 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.api.response;
import java.util.Date;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.schedule.ResourceSchedule;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = ResourceSchedule.class)
public class ResourceScheduleResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "The ID of resource schedule")
private String id;
@SerializedName(ApiConstants.RESOURCE_TYPE)
@Param(description = "Type of the resource")
private ApiCommandResourceType resourceType;
@SerializedName(ApiConstants.RESOURCE_ID)
@Param(description = "ID of the resource")
private String resourceId;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "Description of resource schedule")
private String description;
@SerializedName(ApiConstants.SCHEDULE)
@Param(description = "Cron formatted resource schedule")
private String schedule;
@SerializedName(ApiConstants.TIMEZONE)
@Param(description = "Timezone of the schedule")
private String timeZone;
@SerializedName(ApiConstants.ACTION)
@Param(description = "Action")
private ResourceSchedule.Action action;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "Resource schedule is enabled")
private boolean enabled;
@SerializedName(ApiConstants.START_DATE)
@Param(description = "Date from which the schedule is active")
private Date startDate;
@SerializedName(ApiConstants.END_DATE)
@Param(description = "Date after which the schedule becomes inactive")
private Date endDate;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "Schedule details")
private Map<String, String> details;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Date when the schedule was created")
private Date created;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setResourceType(ApiCommandResourceType resourceType) {
this.resourceType = resourceType;
}
public ApiCommandResourceType getResourceType() {
return resourceType;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public String getResourceId() {
return resourceId;
}
public void setDescription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public void setSchedule(String schedule) {
this.schedule = schedule;
}
public String getSchedule() {
return schedule;
}
public void setTimeZone(String timeZone) {
this.timeZone = timeZone;
}
public String getTimeZone() {
return timeZone;
}
public void setAction(ResourceSchedule.Action action) {
this.action = action;
}
public ResourceSchedule.Action getAction() {
return action;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean getEnabled() {
return enabled;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public Date getStartDate() {
return startDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public Date getEndDate() {
return endDate;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
public Map<String, String> getDetails() {
return details;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getCreated() {
return created;
}
}

View File

@ -166,6 +166,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "An alternate display text of the ISO attached to the Instance")
private String isoDisplayText;
@SerializedName("isos")
@Param(description = "All ISOs attached to the Instance, keyed by cdrom slot. The first entry mirrors isoid/isoname for back-compat.", responseObject = AttachedIsoResponse.class, since = "4.23.0")
private List<AttachedIsoResponse> isos;
@SerializedName("isomaxcount")
@Param(description = "Maximum number of ISOs that may be attached to this Instance, after applying the cluster-scoped vm.iso.max.count and the hypervisor's own cap.", since = "4.23.0")
private Integer isoMaxCount;
@SerializedName(ApiConstants.SERVICE_OFFERING_ID)
@Param(description = "The ID of the service offering of the Instance")
private String serviceOfferingId;
@ -871,6 +879,22 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
this.isoId = isoId;
}
public void setIsos(List<AttachedIsoResponse> isos) {
this.isos = isos;
}
public List<AttachedIsoResponse> getIsos() {
return isos;
}
public void setIsoMaxCount(Integer isoMaxCount) {
this.isoMaxCount = isoMaxCount;
}
public Integer getIsoMaxCount() {
return isoMaxCount;
}
public void setIsoName(String isoName) {
this.isoName = isoName;
}

View File

@ -23,11 +23,11 @@ import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.schedule.ResourceSchedule;
import java.util.Date;
@EntityReference(value = VMSchedule.class)
@EntityReference(value = ResourceSchedule.class)
public class VMScheduleResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "The ID of Instance schedule")
@ -51,7 +51,7 @@ public class VMScheduleResponse extends BaseResponse {
@SerializedName(ApiConstants.ACTION)
@Param(description = "Action")
private VMSchedule.Action action;
private ResourceSchedule.Action action;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "VM schedule is enabled")
@ -69,6 +69,20 @@ public class VMScheduleResponse extends BaseResponse {
@Param(description = "Date when the schedule was created")
private Date created;
public VMScheduleResponse(ResourceScheduleResponse scheduleResponse) {
super("vmschedule");
this.id = scheduleResponse.getId();
this.vmId = scheduleResponse.getResourceId();
this.description = scheduleResponse.getDescription();
this.schedule = scheduleResponse.getSchedule();
this.timeZone = scheduleResponse.getTimeZone();
this.action = scheduleResponse.getAction();
this.enabled = scheduleResponse.getEnabled();
this.startDate = scheduleResponse.getStartDate();
this.endDate = scheduleResponse.getEndDate();
this.created = scheduleResponse.getCreated();
}
public void setId(String id) {
this.id = id;
}
@ -89,7 +103,7 @@ public class VMScheduleResponse extends BaseResponse {
this.timeZone = timeZone;
}
public void setAction(VMSchedule.Action action) {
public void setAction(ResourceSchedule.Action action) {
this.action = action;
}
@ -105,5 +119,7 @@ public class VMScheduleResponse extends BaseResponse {
this.endDate = endDate;
}
public void setCreated(Date created) {this.created = created;}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -16,20 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule;
package org.apache.cloudstack.schedule;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.time.ZoneId;
import java.util.Date;
public interface VMSchedule extends Identity, InternalIdentity {
enum Action {
START, STOP, REBOOT, FORCE_STOP, FORCE_REBOOT
public interface ResourceSchedule extends Identity, InternalIdentity {
/**
* Common contract for scheduler actions. Each provider defines its own enum
* implementing this interface so the generic machinery can call {@link #name()}
* and {@link #getEventType()} without knowing the concrete type.
*/
interface Action {
String name();
String getEventType();
}
long getVmId();
ApiCommandResourceType getResourceType();
long getResourceId();
String getDescription();
@ -37,7 +48,7 @@ public interface VMSchedule extends Identity, InternalIdentity {
String getTimeZone();
Action getAction();
String getActionName();
boolean getEnabled();

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.schedule;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import java.util.Date;
import java.util.List;
import java.util.Map;
public interface ResourceScheduleManager {
ResourceScheduleResponse createSchedule(ApiCommandResourceType resourceType, String resourceUuid,
String description, String schedule, String timeZone, String action,
Date startDate, Date endDate, boolean enabled, Map<String, String> details);
ResourceScheduleResponse updateSchedule(Long id, String description, String schedule, String timeZone,
Date startDate, Date endDate, Boolean enabled, Map<String, String> details);
ListResponse<ResourceScheduleResponse> listSchedule(Long id, List<Long> ids, ApiCommandResourceType resourceType,
String resourceUuid, String action, Boolean enabled,
Long startIndex, Long pageSize);
Long removeSchedule(ApiCommandResourceType resourceType, String resourceUuid, Long id, List<Long> ids);
void removeSchedulesForResource(ApiCommandResourceType resourceType, long resourceId);
}

View File

@ -16,23 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule;
package org.apache.cloudstack.schedule;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.util.Date;
public interface VMScheduledJob extends Identity, InternalIdentity {
long getVmId();
public interface ResourceScheduledJob extends Identity, InternalIdentity {
ApiCommandResourceType getResourceType();
long getVmScheduleId();
long getResourceId();
long getScheduleId();
Long getAsyncJobId();
void setAsyncJobId(long asyncJobId);
VMSchedule.Action getAction();
String getActionName();
Date getScheduledTime();
}

View File

@ -16,21 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule.dao;
package org.apache.cloudstack.schedule.autoscale;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.vm.schedule.VMScheduledJobVO;
import com.cloud.event.EventTypes;
import org.apache.cloudstack.schedule.ResourceSchedule;
import java.util.Date;
import java.util.List;
public interface VMScheduledJobDao extends GenericDao<VMScheduledJobVO, Long> {
List<VMScheduledJobVO> listJobsToStart(Date currentTimestamp);
int expungeJobsForSchedules(List<Long> scheduleId, Date dateAfter);
int expungeJobsBefore(Date currentTimestamp);
VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp);
public enum AutoScaleScheduleAction implements ResourceSchedule.Action {
UPDATE {
@Override
public String getEventType() {
return EventTypes.EVENT_AUTOSCALEVMGROUP_SCHEDULE_UPDATE;
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.schedule.vm;
import com.cloud.event.EventTypes;
import org.apache.cloudstack.schedule.ResourceSchedule;
public enum VMScheduleAction implements ResourceSchedule.Action {
START {
@Override
public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_START; }
},
STOP {
@Override
public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_STOP; }
},
REBOOT {
@Override
public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_REBOOT; }
},
FORCE_STOP {
@Override
public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_FORCE_STOP; }
},
FORCE_REBOOT {
@Override
public String getEventType() { return EventTypes.EVENT_VM_SCHEDULE_FORCE_REBOOT; }
};
}

View File

@ -1,40 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule;
import org.apache.cloudstack.api.command.user.vm.CreateVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.DeleteVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.ListVMScheduleCmd;
import org.apache.cloudstack.api.command.user.vm.UpdateVMScheduleCmd;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
public interface VMScheduleManager {
VMScheduleResponse createSchedule(CreateVMScheduleCmd createVMScheduleCmd);
VMScheduleResponse createResponse(VMSchedule vmSchedule);
ListResponse<VMScheduleResponse> listSchedule(ListVMScheduleCmd listVMScheduleCmd);
VMScheduleResponse updateSchedule(UpdateVMScheduleCmd updateVMScheduleCmd);
long removeScheduleByVmId(long vmId, boolean expunge);
Long removeSchedule(DeleteVMScheduleCmd deleteVMScheduleCmd);
}

View File

@ -59,7 +59,7 @@ public class AddIpToVmNicTest extends TestCase {
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 class AddIpToVmNicTest extends TestCase {
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;

View File

@ -21,8 +21,9 @@ package org.apache.cloudstack.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -34,9 +35,11 @@ import org.mockito.MockitoAnnotations;
public class CreateVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
public ResourceScheduleManager resourceScheduleManager;
@Mock
public EntityManager entityManager;
@InjectMocks
private CreateVMScheduleCmd createVMScheduleCmd = new CreateVMScheduleCmd();
@ -59,10 +62,19 @@ public class CreateVMScheduleCmdTest {
*/
@Test
public void testSuccessfulExecution() {
VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class);
Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenReturn(vmScheduleResponse);
ResourceScheduleResponse scheduleResponse = new ResourceScheduleResponse();
scheduleResponse.setId("schedule-uuid");
scheduleResponse.setResourceId("vm-uuid");
Mockito.when(resourceScheduleManager.createSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.anyBoolean(), Mockito.any()
)).thenReturn(scheduleResponse);
createVMScheduleCmd.execute();
Assert.assertEquals(vmScheduleResponse, createVMScheduleCmd.getResponseObject());
VMScheduleResponse response = (VMScheduleResponse) createVMScheduleCmd.getResponseObject();
Assert.assertNotNull(response);
Assert.assertEquals("schedule-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "id"));
Assert.assertEquals("vm-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "vmId"));
}
/**
@ -72,7 +84,11 @@ public class CreateVMScheduleCmdTest {
*/
@Test(expected = InvalidParameterValueException.class)
public void testInvalidParameterValueException() {
Mockito.when(vmScheduleManager.createSchedule(createVMScheduleCmd)).thenThrow(InvalidParameterValueException.class);
Mockito.when(resourceScheduleManager.createSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.anyBoolean(), Mockito.any()
)).thenThrow(new InvalidParameterValueException("Invalid schedule"));
createVMScheduleCmd.execute();
}

View File

@ -23,8 +23,7 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -36,7 +35,8 @@ import org.mockito.MockitoAnnotations;
public class DeleteVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
public ResourceScheduleManager resourceScheduleManager;
@Mock
public EntityManager entityManager;
@ -64,9 +64,11 @@ public class DeleteVMScheduleCmdTest {
public void testSuccessfulExecution() {
final SuccessResponse response = new SuccessResponse();
response.setResponseName(deleteVMScheduleCmd.getCommandName());
response.setObjectName(VMSchedule.class.getSimpleName().toLowerCase());
response.setObjectName("vmschedule");
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(1L);
Mockito.when(resourceScheduleManager.removeSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenReturn(1L);
deleteVMScheduleCmd.execute();
SuccessResponse actualResponse = (SuccessResponse) deleteVMScheduleCmd.getResponseObject();
Assert.assertEquals(response.getResponseName(), actualResponse.getResponseName());
@ -80,7 +82,9 @@ public class DeleteVMScheduleCmdTest {
*/
@Test(expected = ServerApiException.class)
public void testServerApiException() {
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenReturn(0L);
Mockito.when(resourceScheduleManager.removeSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenReturn(0L);
deleteVMScheduleCmd.execute();
}
@ -91,7 +95,9 @@ public class DeleteVMScheduleCmdTest {
*/
@Test(expected = InvalidParameterValueException.class)
public void testInvalidParameterValueException() {
Mockito.when(vmScheduleManager.removeSchedule(deleteVMScheduleCmd)).thenThrow(InvalidParameterValueException.class);
Mockito.when(resourceScheduleManager.removeSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenThrow(new InvalidParameterValueException("Invalid schedule"));
deleteVMScheduleCmd.execute();
}

View File

@ -20,8 +20,9 @@ package org.apache.cloudstack.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -36,7 +37,8 @@ import java.util.Collections;
public class ListVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
public ResourceScheduleManager resourceScheduleManager;
@InjectMocks
private ListVMScheduleCmd listVMScheduleCmd = new ListVMScheduleCmd();
private AutoCloseable closeable;
@ -58,12 +60,14 @@ public class ListVMScheduleCmdTest {
*/
@Test
public void testEmptyResponse() {
ListResponse<VMScheduleResponse> response = new ListResponse<VMScheduleResponse>();
response.setResponses(new ArrayList<VMScheduleResponse>());
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(response);
ListResponse<ResourceScheduleResponse> response = new ListResponse<ResourceScheduleResponse>();
response.setResponses(new ArrayList<ResourceScheduleResponse>());
Mockito.when(resourceScheduleManager.listSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenReturn(response);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
Assert.assertEquals(response, actualResponseObject);
Assert.assertEquals(0L, actualResponseObject.getResponses().size());
}
@ -74,15 +78,22 @@ public class ListVMScheduleCmdTest {
*/
@Test
public void testNonEmptyResponse() {
ListResponse<VMScheduleResponse> listResponse = new ListResponse<VMScheduleResponse>();
VMScheduleResponse response = Mockito.mock(VMScheduleResponse.class);
ListResponse<ResourceScheduleResponse> listResponse = new ListResponse<ResourceScheduleResponse>();
ResourceScheduleResponse response = new ResourceScheduleResponse();
response.setId("schedule-uuid");
response.setResourceId("vm-uuid");
listResponse.setResponses(Collections.singletonList(response));
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenReturn(listResponse);
Mockito.when(resourceScheduleManager.listSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenReturn(listResponse);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
Assert.assertEquals(listResponse, actualResponseObject);
Assert.assertEquals(1L, actualResponseObject.getResponses().size());
Assert.assertEquals(response, actualResponseObject.getResponses().get(0));
Assert.assertEquals("schedule-uuid",
org.springframework.test.util.ReflectionTestUtils.getField(actualResponseObject.getResponses().get(0), "id"));
Assert.assertEquals("vm-uuid",
org.springframework.test.util.ReflectionTestUtils.getField(actualResponseObject.getResponses().get(0), "vmId"));
}
/**
@ -92,7 +103,10 @@ public class ListVMScheduleCmdTest {
*/
@Test(expected = InvalidParameterValueException.class)
public void testInvalidParameterValueException() {
Mockito.when(vmScheduleManager.listSchedule(listVMScheduleCmd)).thenThrow(InvalidParameterValueException.class);
Mockito.when(resourceScheduleManager.listSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenThrow(InvalidParameterValueException.class);
listVMScheduleCmd.execute();
ListResponse<VMScheduleResponse> actualResponseObject = (ListResponse<VMScheduleResponse>) listVMScheduleCmd.getResponseObject();
}

View File

@ -21,9 +21,11 @@ package org.apache.cloudstack.api.command.user.vm;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.db.EntityManager;
import com.cloud.vm.VirtualMachine;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.response.ResourceScheduleResponse;
import org.apache.cloudstack.api.response.VMScheduleResponse;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleManager;
import org.apache.cloudstack.schedule.ResourceSchedule;
import org.apache.cloudstack.schedule.ResourceScheduleManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -35,9 +37,11 @@ import org.mockito.MockitoAnnotations;
public class UpdateVMScheduleCmdTest {
@Mock
public VMScheduleManager vmScheduleManager;
public ResourceScheduleManager resourceScheduleManager;
@Mock
public EntityManager entityManager;
@InjectMocks
private UpdateVMScheduleCmd updateVMScheduleCmd = new UpdateVMScheduleCmd();
@ -60,10 +64,18 @@ public class UpdateVMScheduleCmdTest {
*/
@Test
public void testSuccessfulExecution() {
VMScheduleResponse vmScheduleResponse = Mockito.mock(VMScheduleResponse.class);
Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenReturn(vmScheduleResponse);
ResourceScheduleResponse scheduleResponse = new ResourceScheduleResponse();
scheduleResponse.setId("schedule-uuid");
scheduleResponse.setResourceId("vm-uuid");
Mockito.when(resourceScheduleManager.updateSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenReturn(scheduleResponse);
updateVMScheduleCmd.execute();
Assert.assertEquals(vmScheduleResponse, updateVMScheduleCmd.getResponseObject());
VMScheduleResponse response = (VMScheduleResponse) updateVMScheduleCmd.getResponseObject();
Assert.assertNotNull(response);
Assert.assertEquals("schedule-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "id"));
Assert.assertEquals("vm-uuid", org.springframework.test.util.ReflectionTestUtils.getField(response, "vmId"));
}
/**
@ -73,7 +85,10 @@ public class UpdateVMScheduleCmdTest {
*/
@Test(expected = InvalidParameterValueException.class)
public void testInvalidParameterValueException() {
Mockito.when(vmScheduleManager.updateSchedule(updateVMScheduleCmd)).thenThrow(InvalidParameterValueException.class);
Mockito.when(resourceScheduleManager.updateSchedule(
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()
)).thenThrow(new InvalidParameterValueException("Invalid schedule"));
updateVMScheduleCmd.execute();
}
@ -84,11 +99,12 @@ public class UpdateVMScheduleCmdTest {
*/
@Test
public void testSuccessfulGetEntityOwnerId() {
VMSchedule vmSchedule = Mockito.mock(VMSchedule.class);
ResourceSchedule schedule = Mockito.mock(ResourceSchedule.class);
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(vmSchedule);
Mockito.when(entityManager.findById(VirtualMachine.class, vmSchedule.getVmId())).thenReturn(vm);
Mockito.when(schedule.getResourceType()).thenReturn(ApiCommandResourceType.VirtualMachine);
Mockito.when(entityManager.findById(ResourceSchedule.class, updateVMScheduleCmd.getId())).thenReturn(schedule);
Mockito.when(entityManager.findById(VirtualMachine.class, schedule.getResourceId())).thenReturn(vm);
long ownerId = updateVMScheduleCmd.getEntityOwnerId();
Assert.assertEquals(vm.getAccountId(), ownerId);
@ -101,8 +117,7 @@ public class UpdateVMScheduleCmdTest {
*/
@Test(expected = InvalidParameterValueException.class)
public void testFailureGetEntityOwnerId() {
VMSchedule vmSchedule = Mockito.mock(VMSchedule.class);
Mockito.when(entityManager.findById(VMSchedule.class, updateVMScheduleCmd.getId())).thenReturn(null);
long ownerId = updateVMScheduleCmd.getEntityOwnerId();
Mockito.when(entityManager.findById(ResourceSchedule.class, updateVMScheduleCmd.getId())).thenReturn(null);
updateVMScheduleCmd.getEntityOwnerId();
}
}

View File

@ -0,0 +1,46 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.response;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public final class AttachedIsoResponseTest {
@Test
public void testFullConstructorPopulatesAllFields() {
AttachedIsoResponse response = new AttachedIsoResponse("uuid-1", "alpine-iso", "Alpine boot", 3, true);
Assert.assertEquals("uuid-1", response.getId());
Assert.assertEquals("alpine-iso", response.getName());
Assert.assertEquals("Alpine boot", response.getDisplayText());
Assert.assertEquals(Integer.valueOf(3), response.getDeviceSeq());
Assert.assertTrue(response.getBootable());
}
@Test
public void testNoArgConstructorLeavesFieldsNull() {
AttachedIsoResponse response = new AttachedIsoResponse();
Assert.assertNull(response.getId());
Assert.assertNull(response.getName());
Assert.assertNull(response.getDisplayText());
Assert.assertNull(response.getDeviceSeq());
Assert.assertNull(response.getBootable());
}
}

View File

@ -26,6 +26,7 @@ import java.util.Map;
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<String, DpdkTO> dpdkInterfaceMapping = new HashMap<>();
private int newVmCpuShares;
private boolean clvmCrossPoolMigration;
Map<String, Boolean> vlanToPersistenceMap = new HashMap<>();
@ -149,6 +151,14 @@ public class MigrateCommand extends Command {
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 class MigrateCommand extends Command {
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 class MigrateCommand extends Command {
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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.
*
* <p>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.
*
* <p>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;
}
}

View File

@ -103,4 +103,21 @@ public interface VolumeInfo extends DownloadableDataInfo, Volume {
List<String> getCheckpointPaths();
Set<String> 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);
}

View File

@ -30,6 +30,7 @@ import com.cloud.exception.StorageAccessException;
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 @@ public interface VolumeService {
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);
}

View File

@ -64,6 +64,21 @@ public interface TemplateManager {
true,
ConfigKey.Scope.Global);
ConfigKey<Integer> VmIsoMaxCount = new ConfigKey<Integer>("Advanced",
Integer.class,
"vm.iso.max.count", "1",
"Maximum number of ISOs that may be attached to a VM.",
true,
ConfigKey.Scope.Cluster);
// KVM/libvirt maps deviceSeq=3 to hdc (hda/hdb are taken by the root volume on i440fx/IDE).
// user_vm.iso_id has always pointed at this slot; additional cdroms live in vm_iso_map.
int CDROM_PRIMARY_DEVICE_SEQ = 3;
// Fallback per-VM cdrom cap when the placement host hasn't advertised host.cdrom.max.count
// (older agent, never-deployed VM, etc.).
int DEFAULT_CDROM_MAX_PER_VM = 1;
static final String VMWARE_TOOLS_ISO = "vmware-tools.iso";
static final String XS_TOOLS_ISO = "xs-tools.iso";

View File

@ -50,6 +50,8 @@ import javax.inject.Inject;
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.PrepareExternalProvisioningAnswer;
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.StoragePoolHostDao;
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;
@ -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);
@ -3150,6 +3158,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
updateOverCommitRatioForVmProfile(profile, dest.getHost().getClusterId());
final VirtualMachineTO to = toVmTO(profile);
executePreMigrationCommand(vm, to, srcHostId);
final PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(to);
setVmNetworkDetails(vm, to);
@ -3281,6 +3292,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
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);
@ -3320,6 +3332,30 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
}
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.
*/
@ -3366,6 +3402,27 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
_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<VolumeVO> 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)}
@ -4922,6 +4979,12 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
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());
@ -5006,6 +5069,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
migrated = true;
executePostMigrationCommand(vm, to, dstHostId);
} finally {
if (!migrated) {
logger.info("Migration was unsuccessful. Cleaning up: {}", vm);
@ -6441,6 +6505,37 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
return findClusterAndHostIdForVm(vm, false);
}
private boolean hasClvmVolumes(long vmId) {
List<VolumeVO> 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<Long, Long> findClusterAndHostIdForVm(long vmId) {
VMInstanceVO vm = _vmDao.findById(vmId);

View File

@ -38,8 +38,10 @@ import java.util.stream.Collectors;
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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
ConfigurationDao configurationDao;
@Inject
VMInstanceDao vmInstanceDao;
@Inject
ClvmPoolManager clvmPoolManager;
@Inject
AgentManager _agentMgr;
@Inject
protected SnapshotHelper snapshotHelper;
@ -747,6 +753,17 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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<VolumeApiResult> future = null;
@ -788,6 +805,122 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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<VolumeVO> 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<VolumeVO> 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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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<StoragePoolHostVO> 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<StoragePoolHostVO> 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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
_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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
logger.error(msg);
throw new CloudRuntimeException(msg);
}
for (Map.Entry<Volume, StoragePool> 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 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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 class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
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<VolumeVO> 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<VolumeTask> tasks = getTasks(vols, dest.getStorageForDisks(), vm);
Volume vol = null;
PrimaryDataStore store;

View File

@ -44,6 +44,8 @@
value="#{storagePoolAllocatorsRegistry.registered}" />
</bean>
<bean id="clvmPoolManager" class="com.cloud.storage.clvm.ClvmPoolManager" />
<bean id="storageOrchestrator"
class="org.apache.cloudstack.engine.orchestration.StorageOrchestrator"/>
<bean id="dataMigrationHelper"

View File

@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify;
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.ha.HighAvailabilityManager;
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 class VirtualMachineManagerImplTest {
}
}
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 class VirtualMachineManagerImplTest {
}
}
@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);
}
}

View File

@ -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.Host;
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.user.ResourceLimitService;
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.Assert;
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 class VolumeOrchestratorTest {
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<VolumeVO>(), 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;
}
}

View File

@ -20,6 +20,7 @@ import java.util.Date;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import com.cloud.alert.AlertVO;
@ -28,7 +29,7 @@ import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.UpdateBuilder;
@Component
public class AlertDaoImpl extends GenericDaoBase<AlertVO, Long> implements AlertDao {
@ -107,25 +108,20 @@ public class AlertDaoImpl extends GenericDaoBase<AlertVO, Long> implements Alert
}
sc.setParameters("archived", false);
boolean result = true;
;
List<AlertVO> alerts = listBy(sc);
if (ids != null && alerts.size() < ids.size()) {
result = false;
return result;
return false;
}
if (alerts != null && !alerts.isEmpty()) {
TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
for (AlertVO alert : alerts) {
alert = lockRow(alert.getId(), true);
alert.setArchived(true);
update(alert.getId(), alert);
txn.commit();
}
txn.close();
if (CollectionUtils.isEmpty(alerts)) {
return true;
}
return result;
AlertVO alertForUpdate = createForUpdate();
alertForUpdate.setArchived(true);
UpdateBuilder ub = getUpdateBuilder(alertForUpdate);
update(ub, sc, null);
return true;
}
@Override

View File

@ -18,8 +18,10 @@ package com.cloud.event.dao;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import com.cloud.event.Event.State;
@ -29,12 +31,13 @@ import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.UpdateBuilder;
@Component
public class EventDaoImpl extends GenericDaoBase<EventVO, Long> implements EventDao {
protected final SearchBuilder<EventVO> CompletedEventSearch;
protected final SearchBuilder<EventVO> ToArchiveOrDeleteEventSearch;
protected final SearchBuilder<EventVO> ArchiveByIdsSearch;
public EventDaoImpl() {
CompletedEventSearch = createSearchBuilder();
@ -51,6 +54,10 @@ public class EventDaoImpl extends GenericDaoBase<EventVO, Long> implements Event
ToArchiveOrDeleteEventSearch.and("createdDateL", ToArchiveOrDeleteEventSearch.entity().getCreateDate(), Op.LTEQ);
ToArchiveOrDeleteEventSearch.and("archived", ToArchiveOrDeleteEventSearch.entity().getArchived(), Op.EQ);
ToArchiveOrDeleteEventSearch.done();
ArchiveByIdsSearch = createSearchBuilder();
ArchiveByIdsSearch.and("id", ArchiveByIdsSearch.entity().getId(), Op.IN);
ArchiveByIdsSearch.done();
}
@Override
@ -100,16 +107,16 @@ public class EventDaoImpl extends GenericDaoBase<EventVO, Long> implements Event
@Override
public void archiveEvents(List<EventVO> events) {
if (events != null && !events.isEmpty()) {
TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
for (EventVO event : events) {
event = lockRow(event.getId(), true);
event.setArchived(true);
update(event.getId(), event);
txn.commit();
}
txn.close();
if (CollectionUtils.isEmpty(events)) {
return;
}
List<Long> ids = events.stream().map(EventVO::getId).collect(Collectors.toList());
SearchCriteria<EventVO> sc = ArchiveByIdsSearch.create();
sc.setParameters("id", ids.toArray(new Object[ids.size()]));
EventVO eventForUpdate = createForUpdate();
eventForUpdate.setArchived(true);
UpdateBuilder ub = getUpdateBuilder(eventForUpdate);
update(ub, sc, null);
}
}

View File

@ -45,7 +45,7 @@ import javax.persistence.Transient;
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 class HostVO implements Host {
}
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())) {

View File

@ -32,12 +32,17 @@ import com.cloud.utils.Pair;
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<HostVO, Long>, StateDao<Status, Status.Event, Host> {
ConfigKey<Long> 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 @@ public interface HostDao extends GenericDao<HostVO, Long>, StateDao<Status, Stat
List<HostVO> findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags);
List<HostVO> findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName);
List<Long> findClustersThatMatchHostTagRule(String computeOfferingTags);
List<Long> listSsvmHostsWithPendingMigrateJobsOrderedByJobCount();

View File

@ -37,7 +37,9 @@ import javax.persistence.TableGenerator;
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 @@ import org.apache.commons.lang3.ObjectUtils;
@DB
@TableGenerator(name = "host_req_sq", table = "op_host", pkColumnName = "id", valueColumnName = "sequence", allocationSize = 1)
public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao { //FIXME: , ExternalIdDao {
public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao, Configurable {
private static final String LIST_HOST_IDS_BY_HOST_TAGS = "SELECT filtered.host_id, COUNT(filtered.tag) AS tag_count "
+ "FROM (SELECT host_id, tag, is_tag_a_rule FROM host_tags GROUP BY host_id,tag,is_tag_a_rule) AS filtered "
@ -644,16 +646,22 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao
sc.setParameters("lastPinged", lastPingSecondsAfter);
sc.setParameters("status", Status.Disconnected, Status.Down, Status.Alert);
StringBuilder sb = new StringBuilder();
List<HostVO> hosts = lockRows(sc, null, true); // exclusive lock
for (HostVO host : hosts) {
host.setManagementServerId(null);
update(host.getId(), host);
sb.append(host.getId());
sb.append(" ");
// SELECT before bulk UPDATE to preserve per-host-ID trace logging the bulk UPDATE
// cannot return which rows it matched since the WHERE column is being set to NULL
if (logger.isTraceEnabled()) {
List<HostVO> hosts = listBy(sc);
StringBuilder sb = new StringBuilder();
for (HostVO host : hosts) {
sb.append(host.getId());
sb.append(" ");
}
logger.trace("Following hosts will be reset: {}", sb);
}
logger.trace("Following hosts got reset: {}", sb);
HostVO host = createForUpdate();
host.setManagementServerId(null);
UpdateBuilder ub = getUpdateBuilder(host);
update(ub, sc, null);
}
/*
@ -1520,11 +1528,13 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao
}
}
@Override
public List<HostVO> findHostsWithTagRuleThatMatchComputeOfferingTags(String computeOfferingTags) {
List<HostTagVO> hostTagVOList = _hostTagsDao.findHostRuleTags();
List<HostVO> 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 +1542,23 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao
return result;
}
@Override
public List<HostVO> findHostsWithGuestOsRulesThatDidNotMatchOsOfGuestVm(String templateGuestOSName) {
List<DetailVO> hostIdsWithGuestOsRule = _detailsDao.findByName(Host.GUEST_OS_RULE);
List<HostVO> 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<Long> findClustersThatMatchHostTagRule(String computeOfferingTags) {
Set<Long> result = new HashSet<>();
List<HostVO> hosts = findHostsWithTagRuleThatMatchComputeOfferingTags(computeOfferingTags);
@ -1984,4 +2011,14 @@ public class HostDaoImpl extends GenericDaoBase<HostVO, Long> implements HostDao
return customSearch(sc, null);
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {guestOsRuleExecutionTimeout};
}
@Override
public String getConfigComponentName() {
return HostDaoImpl.class.getSimpleName();
}
}

View File

@ -116,7 +116,7 @@ public class SecurityGroupWorkDaoImpl extends GenericDaoBase<SecurityGroupWorkVO
//ensure that there is no job in Processing state for the same VM
processing = true;
if (logger.isTraceEnabled()) {
logger.trace("Security Group work take: found a job in Scheduled and Processing vmid=" + work.getInstanceId());
logger.trace("Security Group work take: found a job in Scheduled and Processing vmid={}", work.getInstanceId());
}
}
work.setServerId(serverId);
@ -141,26 +141,16 @@ public class SecurityGroupWorkDaoImpl extends GenericDaoBase<SecurityGroupWorkVO
}
@Override
@DB
public void updateStep(Long vmId, Long logSequenceNumber, Step step) {
final TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
SearchCriteria<SecurityGroupWorkVO> sc = VmIdSeqNumSearch.create();
sc.setParameters("vmId", vmId);
sc.setParameters("seqno", logSequenceNumber);
final Filter filter = new Filter(SecurityGroupWorkVO.class, null, true, 0l, 1l);
final List<SecurityGroupWorkVO> vos = lockRows(sc, filter, true);
if (vos.size() == 0) {
txn.commit();
return;
}
SecurityGroupWorkVO work = vos.get(0);
work.setStep(step);
update(work.getId(), work);
txn.commit();
SecurityGroupWorkVO workForUpdate = createForUpdate();
workForUpdate.setStep(step);
// LIMIT 1 preserves the original single-row semantics: op_nwgrp_work has no
// uniqueness on (instance_id, seq_no), so without it duplicate rows would all be updated.
update(workForUpdate, sc, 1);
}
@Override
@ -172,21 +162,10 @@ public class SecurityGroupWorkDaoImpl extends GenericDaoBase<SecurityGroupWorkVO
}
@Override
@DB
public void updateStep(Long workId, Step step) {
final TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
SecurityGroupWorkVO work = lockRow(workId, true);
if (work == null) {
txn.commit();
return;
}
work.setStep(step);
update(work.getId(), work);
txn.commit();
SecurityGroupWorkVO workForUpdate = createForUpdate();
workForUpdate.setStep(step);
update(workId, workForUpdate);
}
@Override

View File

@ -0,0 +1,83 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.vm;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.apache.cloudstack.api.InternalIdentity;
@Entity
@Table(name = "vm_iso_map")
public class VmIsoMapVO implements InternalIdentity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "vm_id")
private long vmId;
@Column(name = "iso_id")
private long isoId;
@Column(name = "device_seq")
private int deviceSeq;
@Column(name = "created")
@Temporal(TemporalType.TIMESTAMP)
private Date created;
public VmIsoMapVO() {
}
public VmIsoMapVO(long vmId, long isoId, int deviceSeq) {
this.vmId = vmId;
this.isoId = isoId;
this.deviceSeq = deviceSeq;
this.created = new Date();
}
@Override
public long getId() {
return id;
}
public long getVmId() {
return vmId;
}
public long getIsoId() {
return isoId;
}
public int getDeviceSeq() {
return deviceSeq;
}
public Date getCreated() {
return created;
}
}

View File

@ -43,7 +43,7 @@ public class NicSecondaryIpVO implements NicSecondaryIp {
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 class NicSecondaryIpVO implements NicSecondaryIp {
this.accountId = accountId;
this.domainId = domainId;
this.networkId = networkId;
this.description = description;
}
protected NicSecondaryIpVO() {
@ -88,6 +89,18 @@ public class NicSecondaryIpVO implements NicSecondaryIp {
@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",

View File

@ -0,0 +1,34 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.vm.dao;
import java.util.List;
import com.cloud.utils.db.GenericDao;
import com.cloud.vm.VmIsoMapVO;
public interface VmIsoMapDao extends GenericDao<VmIsoMapVO, Long> {
List<VmIsoMapVO> listByVmId(long vmId);
List<VmIsoMapVO> listByIsoId(long isoId);
VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq);
VmIsoMapVO findByVmIdIsoId(long vmId, long isoId);
int removeByVmId(long vmId);
}

View File

@ -0,0 +1,92 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.vm.dao;
import java.util.List;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.VmIsoMapVO;
@Component
public class VmIsoMapDaoImpl extends GenericDaoBase<VmIsoMapVO, Long> implements VmIsoMapDao {
private SearchBuilder<VmIsoMapVO> ListByVmId;
private SearchBuilder<VmIsoMapVO> ListByIsoId;
private SearchBuilder<VmIsoMapVO> ByVmIdDeviceSeq;
private SearchBuilder<VmIsoMapVO> ByVmIdIsoId;
protected VmIsoMapDaoImpl() {
ListByVmId = createSearchBuilder();
ListByVmId.and("vmId", ListByVmId.entity().getVmId(), SearchCriteria.Op.EQ);
ListByVmId.done();
ListByIsoId = createSearchBuilder();
ListByIsoId.and("isoId", ListByIsoId.entity().getIsoId(), SearchCriteria.Op.EQ);
ListByIsoId.done();
ByVmIdDeviceSeq = createSearchBuilder();
ByVmIdDeviceSeq.and("vmId", ByVmIdDeviceSeq.entity().getVmId(), SearchCriteria.Op.EQ);
ByVmIdDeviceSeq.and("deviceSeq", ByVmIdDeviceSeq.entity().getDeviceSeq(), SearchCriteria.Op.EQ);
ByVmIdDeviceSeq.done();
ByVmIdIsoId = createSearchBuilder();
ByVmIdIsoId.and("vmId", ByVmIdIsoId.entity().getVmId(), SearchCriteria.Op.EQ);
ByVmIdIsoId.and("isoId", ByVmIdIsoId.entity().getIsoId(), SearchCriteria.Op.EQ);
ByVmIdIsoId.done();
}
@Override
public List<VmIsoMapVO> listByVmId(long vmId) {
SearchCriteria<VmIsoMapVO> sc = ListByVmId.create();
sc.setParameters("vmId", vmId);
return listBy(sc);
}
@Override
public List<VmIsoMapVO> listByIsoId(long isoId) {
SearchCriteria<VmIsoMapVO> sc = ListByIsoId.create();
sc.setParameters("isoId", isoId);
return listBy(sc);
}
@Override
public VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq) {
SearchCriteria<VmIsoMapVO> sc = ByVmIdDeviceSeq.create();
sc.setParameters("vmId", vmId);
sc.setParameters("deviceSeq", deviceSeq);
return findOneBy(sc);
}
@Override
public VmIsoMapVO findByVmIdIsoId(long vmId, long isoId) {
SearchCriteria<VmIsoMapVO> sc = ByVmIdIsoId.create();
sc.setParameters("vmId", vmId);
sc.setParameters("isoId", isoId);
return findOneBy(sc);
}
@Override
public int removeByVmId(long vmId) {
SearchCriteria<VmIsoMapVO> sc = ListByVmId.create();
sc.setParameters("vmId", vmId);
return remove(sc);
}
}

View File

@ -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)

View File

@ -0,0 +1,82 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.schedule;
import org.apache.cloudstack.api.ResourceDetail;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "resource_schedule_details")
public class ResourceScheduleDetailVO implements ResourceDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "schedule_id")
private long resourceId;
@Column(name = "name")
private String name;
@Column(name = "value", length = 1024)
private String value;
@Column(name = "display")
private boolean display = true;
public ResourceScheduleDetailVO() {
}
public ResourceScheduleDetailVO(long scheduleId, String name, String value, boolean display) {
this.resourceId = scheduleId;
this.name = name;
this.value = value;
this.display = display;
}
@Override
public long getId() {
return id;
}
@Override
public long getResourceId() {
return resourceId;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
@Override
public boolean isDisplay() {
return display;
}
}

View File

@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule;
package org.apache.cloudstack.schedule;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
@ -37,8 +38,8 @@ import java.util.TimeZone;
import java.util.UUID;
@Entity
@Table(name = "vm_schedule")
public class VMScheduleVO implements VMSchedule {
@Table(name = "resource_schedule")
public class ResourceScheduleVO implements ResourceSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
@ -50,8 +51,12 @@ public class VMScheduleVO implements VMSchedule {
@Column(name = "description")
String description;
@Column(name = "vm_id", nullable = false)
long vmId;
@Enumerated(value = EnumType.STRING)
@Column(name = "resource_type", nullable = false)
ApiCommandResourceType resourceType;
@Column(name = "resource_id", nullable = false)
long resourceId;
@Column(name = "schedule", nullable = false)
String schedule;
@ -60,8 +65,7 @@ public class VMScheduleVO implements VMSchedule {
String timeZone;
@Column(name = "action", nullable = false)
@Enumerated(value = EnumType.STRING)
Action action;
String actionName;
@Column(name = "enabled", nullable = false)
boolean enabled;
@ -80,17 +84,19 @@ public class VMScheduleVO implements VMSchedule {
@Column(name = GenericDao.REMOVED_COLUMN)
Date removed;
public VMScheduleVO() {
public ResourceScheduleVO() {
uuid = UUID.randomUUID().toString();
}
public VMScheduleVO(long vmId, String description, String schedule, String timeZone, Action action, Date startDate, Date endDate, boolean enabled) {
public ResourceScheduleVO(ApiCommandResourceType resourceType, long resourceId, String description, String schedule,
String timeZone, String action, Date startDate, Date endDate, boolean enabled) {
uuid = UUID.randomUUID().toString();
this.vmId = vmId;
this.resourceType = resourceType;
this.resourceId = resourceId;
this.description = description;
this.schedule = schedule;
this.timeZone = timeZone;
this.action = action;
this.actionName = action;
this.startDate = startDate;
this.endDate = endDate;
this.enabled = enabled;
@ -98,7 +104,8 @@ public class VMScheduleVO implements VMSchedule {
@Override
public String toString() {
return String.format("VMSchedule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "action", "description"));
return String.format("ResourceSchedule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "resourceType", "actionName", "description", "enabled"));
}
@Override
@ -111,14 +118,25 @@ public class VMScheduleVO implements VMSchedule {
return id;
}
public long getVmId() {
return vmId;
@Override
public ApiCommandResourceType getResourceType() {
return resourceType;
}
public void setVmId(long vmId) {
this.vmId = vmId;
public void setResourceType(ApiCommandResourceType resourceType) {
this.resourceType = resourceType;
}
@Override
public long getResourceId() {
return resourceId;
}
public void setResourceId(long resourceId) {
this.resourceId = resourceId;
}
@Override
public String getDescription() {
return description;
}
@ -127,6 +145,7 @@ public class VMScheduleVO implements VMSchedule {
this.description = description;
}
@Override
public String getSchedule() {
return schedule.substring(2);
}
@ -144,14 +163,16 @@ public class VMScheduleVO implements VMSchedule {
this.timeZone = timeZone;
}
public Action getAction() {
return action;
@Override
public String getActionName() {
return actionName;
}
public void setAction(Action action) {
this.action = action;
public void setActionName(String action) {
this.actionName = action;
}
@Override
public boolean getEnabled() {
return enabled;
}
@ -183,6 +204,7 @@ public class VMScheduleVO implements VMSchedule {
return TimeZone.getTimeZone(getTimeZone()).toZoneId();
}
@Override
public Date getCreated() {
return created;
}

View File

@ -16,8 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule;
package org.apache.cloudstack.schedule;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
@ -34,8 +35,8 @@ import java.util.Date;
import java.util.UUID;
@Entity
@Table(name = "vm_scheduled_job")
public class VMScheduledJobVO implements VMScheduledJob {
@Table(name = "resource_scheduled_job")
public class ResourceScheduledJobVO implements ResourceScheduledJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@ -44,41 +45,44 @@ public class VMScheduledJobVO implements VMScheduledJob {
@Column(name = "uuid", nullable = false)
String uuid;
@Column(name = "vm_id", nullable = false)
long vmId;
@Enumerated(value = EnumType.STRING)
@Column(name = "resource_type", nullable = false)
ApiCommandResourceType resourceType;
@Column(name = "vm_schedule_id", nullable = false)
long vmScheduleId;
@Column(name = "resource_id", nullable = false)
long resourceId;
@Column(name = "schedule_id", nullable = false)
long scheduleId;
@Column(name = "async_job_id")
Long asyncJobId;
@Column(name = "action", nullable = false)
@Enumerated(value = EnumType.STRING)
VMSchedule.Action action;
String actionName;
@Column(name = "scheduled_timestamp")
@Temporal(value = TemporalType.TIMESTAMP)
Date scheduledTime;
public VMScheduledJobVO() {
public ResourceScheduledJobVO() {
uuid = UUID.randomUUID().toString();
}
public VMScheduledJobVO(long vmId, long vmScheduleId, VMSchedule.Action action, Date scheduledTime) {
public ResourceScheduledJobVO(ApiCommandResourceType resourceType, long resourceId, long scheduleId, String action, Date scheduledTime) {
uuid = UUID.randomUUID().toString();
this.vmId = vmId;
this.vmScheduleId = vmScheduleId;
this.action = action;
this.resourceType = resourceType;
this.resourceId = resourceId;
this.scheduleId = scheduleId;
this.actionName = action;
this.scheduledTime = scheduledTime;
}
@Override
public String toString() {
return String.format("VMScheduledJob %s",
return String.format("ResourceScheduledJob %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "action", "vmScheduleId", "vmId", "asyncJobId"));
this, "id", "uuid", "resourceType", "actionName", "scheduleId", "resourceId", "asyncJobId"));
}
@Override
@ -92,13 +96,18 @@ public class VMScheduledJobVO implements VMScheduledJob {
}
@Override
public long getVmId() {
return vmId;
public ApiCommandResourceType getResourceType() {
return resourceType;
}
@Override
public long getVmScheduleId() {
return vmScheduleId;
public long getResourceId() {
return resourceId;
}
@Override
public long getScheduleId() {
return scheduleId;
}
@Override
@ -112,8 +121,12 @@ public class VMScheduledJobVO implements VMScheduledJob {
}
@Override
public VMSchedule.Action getAction() {
return action;
public String getActionName() {
return actionName;
}
public void setActionName(String action) {
this.actionName = action;
}
@Override

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.schedule.ResourceScheduleVO;
import java.util.List;
public interface ResourceScheduleDao extends GenericDao<ResourceScheduleVO, Long> {
List<ResourceScheduleVO> listAllActiveSchedules(ApiCommandResourceType resourceType);
long removeSchedulesForResourceAndIds(ApiCommandResourceType resourceType, long resourceId, List<Long> ids);
long removeAllSchedulesForResource(ApiCommandResourceType resourceType, long resourceId);
Pair<List<ResourceScheduleVO>, Integer> searchAndCount(List<Long> ids, ApiCommandResourceType resourceType, Long resourceId,
String action, Boolean enabled, Long offset, Long limit);
SearchCriteria<ResourceScheduleVO> getSearchCriteriaForResource(ApiCommandResourceType resourceType, long resourceId);
}

View File

@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.schedule.ResourceScheduleVO;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class ResourceScheduleDaoImpl extends GenericDaoBase<ResourceScheduleVO, Long> implements ResourceScheduleDao {
private final SearchBuilder<ResourceScheduleVO> activeScheduleSearch;
private final SearchBuilder<ResourceScheduleVO> allSearch;
static final String RESOURCE_TYPE = "resourceType";
static final String RESOURCE_ID = "resourceId";
public ResourceScheduleDaoImpl() {
super();
activeScheduleSearch = createSearchBuilder();
activeScheduleSearch.and(RESOURCE_TYPE, activeScheduleSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
activeScheduleSearch.and(ApiConstants.ENABLED, activeScheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
activeScheduleSearch.done();
allSearch = createSearchBuilder();
allSearch.and(ApiConstants.ID, allSearch.entity().getId(), SearchCriteria.Op.IN);
allSearch.and(RESOURCE_TYPE, allSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
allSearch.and(RESOURCE_ID, allSearch.entity().getResourceId(), SearchCriteria.Op.EQ);
allSearch.and(ApiConstants.ACTION, allSearch.entity().getActionName(), SearchCriteria.Op.EQ);
allSearch.and(ApiConstants.ENABLED, allSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
allSearch.done();
}
@Override
public List<ResourceScheduleVO> listAllActiveSchedules(ApiCommandResourceType resourceType) {
SearchCriteria<ResourceScheduleVO> sc = activeScheduleSearch.create();
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(ApiConstants.ENABLED, true);
return search(sc, null);
}
@Override
public long removeSchedulesForResourceAndIds(ApiCommandResourceType resourceType, long resourceId, List<Long> ids) {
SearchCriteria<ResourceScheduleVO> sc = allSearch.create();
if (CollectionUtils.isNotEmpty(ids)) {
sc.setParameters(ApiConstants.ID, ids.toArray());
}
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(RESOURCE_ID, resourceId);
return remove(sc);
}
@Override
public long removeAllSchedulesForResource(ApiCommandResourceType resourceType, long resourceId) {
SearchCriteria<ResourceScheduleVO> sc = allSearch.create();
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(RESOURCE_ID, resourceId);
return remove(sc);
}
@Override
public Pair<List<ResourceScheduleVO>, Integer> searchAndCount(List<Long> ids, ApiCommandResourceType resourceType, Long resourceId,
String action, Boolean enabled, Long offset, Long limit) {
SearchCriteria<ResourceScheduleVO> sc = allSearch.create();
if (CollectionUtils.isNotEmpty(ids)) {
sc.setParameters(ApiConstants.ID, ids.toArray());
}
sc.setParametersIfNotNull(ApiConstants.ENABLED, enabled);
sc.setParametersIfNotNull(ApiConstants.ACTION, action);
sc.setParametersIfNotNull(RESOURCE_TYPE, resourceType);
sc.setParametersIfNotNull(RESOURCE_ID, resourceId);
Filter filter = new Filter(ResourceScheduleVO.class, ApiConstants.ID, false, offset, limit);
return searchAndCount(sc, filter);
}
@Override
public SearchCriteria<ResourceScheduleVO> getSearchCriteriaForResource(ApiCommandResourceType resourceType, long resourceId) {
SearchCriteria<ResourceScheduleVO> sc = allSearch.create();
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(RESOURCE_ID, resourceId);
return sc;
}
}

View File

@ -0,0 +1,24 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.schedule.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
import org.apache.cloudstack.schedule.ResourceScheduleDetailVO;
public interface ResourceScheduleDetailsDao extends GenericDao<ResourceScheduleDetailVO, Long>, ResourceDetailsDao<ResourceScheduleDetailVO> {
}

View File

@ -0,0 +1,28 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.schedule.dao;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
import org.apache.cloudstack.schedule.ResourceScheduleDetailVO;
public class ResourceScheduleDetailsDaoImpl extends ResourceDetailsDaoBase<ResourceScheduleDetailVO> implements ResourceScheduleDetailsDao {
@Override
public void addDetail(long resourceId, String key, String value, boolean display) {
super.addDetail(new ResourceScheduleDetailVO(resourceId, key, value, display));
}
}

View File

@ -16,22 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule.dao;
package org.apache.cloudstack.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleVO;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.schedule.ResourceScheduledJobVO;
import java.util.Date;
import java.util.List;
public interface VMScheduleDao extends GenericDao<VMScheduleVO, Long> {
List<VMScheduleVO> listAllActiveSchedules();
public interface ResourceScheduledJobDao extends GenericDao<ResourceScheduledJobVO, Long> {
List<ResourceScheduledJobVO> listJobsToStart(ApiCommandResourceType resourceType, Date currentTimestamp);
long removeSchedulesForVmIdAndIds(Long vmId, List<Long> ids);
int expungeJobsForSchedules(List<Long> scheduleIds, Date dateAfter);
Pair<List<VMScheduleVO>, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit);
int expungeJobsBefore(ApiCommandResourceType resourceType, Date currentTimestamp);
SearchCriteria<VMScheduleVO> getSearchCriteriaForVMId(Long vmId);
ResourceScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp);
}

View File

@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule.dao;
package org.apache.cloudstack.schedule.dao;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.vm.schedule.VMScheduledJobVO;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.schedule.ResourceScheduledJobVO;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.stereotype.Component;
@ -31,62 +32,59 @@ import java.util.Date;
import java.util.List;
@Component
public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long> implements VMScheduledJobDao {
public class ResourceScheduledJobDaoImpl extends GenericDaoBase<ResourceScheduledJobVO, Long> implements ResourceScheduledJobDao {
private final SearchBuilder<VMScheduledJobVO> jobsToStartSearch;
private final SearchBuilder<ResourceScheduledJobVO> jobsToStartSearch;
private final SearchBuilder<ResourceScheduledJobVO> expungeJobsBeforeSearch;
private final SearchBuilder<ResourceScheduledJobVO> expungeJobForScheduleSearch;
private final SearchBuilder<ResourceScheduledJobVO> scheduleAndTimestampSearch;
private final SearchBuilder<VMScheduledJobVO> expungeJobsBeforeSearch;
static final String SCHEDULED_TIMESTAMP = "scheduledTimestamp";
static final String SCHEDULE_ID = "scheduleId";
static final String RESOURCE_TYPE = "resourceType";
private final SearchBuilder<VMScheduledJobVO> expungeJobForScheduleSearch;
private final SearchBuilder<VMScheduledJobVO> scheduleAndTimestampSearch;
static final String SCHEDULED_TIMESTAMP = "scheduled_timestamp";
static final String VM_SCHEDULE_ID = "vm_schedule_id";
public VMScheduledJobDaoImpl() {
public ResourceScheduledJobDaoImpl() {
super();
jobsToStartSearch = createSearchBuilder();
jobsToStartSearch.and(RESOURCE_TYPE, jobsToStartSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
jobsToStartSearch.and(SCHEDULED_TIMESTAMP, jobsToStartSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ);
jobsToStartSearch.and("async_job_id", jobsToStartSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL);
jobsToStartSearch.done();
expungeJobsBeforeSearch = createSearchBuilder();
expungeJobsBeforeSearch.and(RESOURCE_TYPE, expungeJobsBeforeSearch.entity().getResourceType(), SearchCriteria.Op.EQ);
expungeJobsBeforeSearch.and(SCHEDULED_TIMESTAMP, expungeJobsBeforeSearch.entity().getScheduledTime(), SearchCriteria.Op.LT);
expungeJobsBeforeSearch.done();
expungeJobForScheduleSearch = createSearchBuilder();
expungeJobForScheduleSearch.and(VM_SCHEDULE_ID, expungeJobForScheduleSearch.entity().getVmScheduleId(), SearchCriteria.Op.IN);
expungeJobForScheduleSearch.and(SCHEDULE_ID, expungeJobForScheduleSearch.entity().getScheduleId(), SearchCriteria.Op.IN);
expungeJobForScheduleSearch.and(SCHEDULED_TIMESTAMP, expungeJobForScheduleSearch.entity().getScheduledTime(), SearchCriteria.Op.GTEQ);
expungeJobForScheduleSearch.done();
scheduleAndTimestampSearch = createSearchBuilder();
scheduleAndTimestampSearch.and(VM_SCHEDULE_ID, scheduleAndTimestampSearch.entity().getVmScheduleId(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.and(SCHEDULE_ID, scheduleAndTimestampSearch.entity().getScheduleId(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.and(SCHEDULED_TIMESTAMP, scheduleAndTimestampSearch.entity().getScheduledTime(), SearchCriteria.Op.EQ);
scheduleAndTimestampSearch.done();
}
/**
* Execution of job wouldn't be at exact seconds. So, we round off and then execute.
*/
@Override
public List<VMScheduledJobVO> listJobsToStart(Date currentTimestamp) {
public List<ResourceScheduledJobVO> listJobsToStart(ApiCommandResourceType resourceType, Date currentTimestamp) {
if (currentTimestamp == null) {
currentTimestamp = new Date();
}
Date truncatedTs = DateUtils.round(currentTimestamp, Calendar.MINUTE);
SearchCriteria<VMScheduledJobVO> sc = jobsToStartSearch.create();
SearchCriteria<ResourceScheduledJobVO> sc = jobsToStartSearch.create();
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(SCHEDULED_TIMESTAMP, truncatedTs);
Filter filter = new Filter(VMScheduledJobVO.class, "vmScheduleId", true, null, null);
Filter filter = new Filter(ResourceScheduledJobVO.class, "scheduleId", true, null, null);
return search(sc, filter);
}
@Override
public int expungeJobsForSchedules(List<Long> vmScheduleIds, Date dateAfter) {
SearchCriteria<VMScheduledJobVO> sc = expungeJobForScheduleSearch.create();
sc.setParameters(VM_SCHEDULE_ID, vmScheduleIds.toArray());
public int expungeJobsForSchedules(List<Long> scheduleIds, Date dateAfter) {
SearchCriteria<ResourceScheduledJobVO> sc = expungeJobForScheduleSearch.create();
sc.setParameters(SCHEDULE_ID, scheduleIds.toArray());
if (dateAfter != null) {
sc.setParameters(SCHEDULED_TIMESTAMP, dateAfter);
}
@ -94,16 +92,17 @@ public class VMScheduledJobDaoImpl extends GenericDaoBase<VMScheduledJobVO, Long
}
@Override
public int expungeJobsBefore(Date date) {
SearchCriteria<VMScheduledJobVO> sc = expungeJobsBeforeSearch.create();
public int expungeJobsBefore(ApiCommandResourceType resourceType, Date date) {
SearchCriteria<ResourceScheduledJobVO> sc = expungeJobsBeforeSearch.create();
sc.setParameters(RESOURCE_TYPE, resourceType);
sc.setParameters(SCHEDULED_TIMESTAMP, date);
return expunge(sc);
}
@Override
public VMScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) {
SearchCriteria<VMScheduledJobVO> sc = scheduleAndTimestampSearch.create();
sc.setParameters(VM_SCHEDULE_ID, scheduleId);
public ResourceScheduledJobVO findByScheduleAndTimestamp(long scheduleId, Date scheduledTimestamp) {
SearchCriteria<ResourceScheduledJobVO> sc = scheduleAndTimestampSearch.create();
sc.setParameters(SCHEDULE_ID, scheduleId);
sc.setParameters(SCHEDULED_TIMESTAMP, scheduledTimestamp);
return findOneBy(sc);
}

View File

@ -1,108 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.vm.schedule.dao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.vm.schedule.VMSchedule;
import org.apache.cloudstack.vm.schedule.VMScheduleVO;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
public class VMScheduleDaoImpl extends GenericDaoBase<VMScheduleVO, Long> implements VMScheduleDao {
private final SearchBuilder<VMScheduleVO> activeScheduleSearch;
private final SearchBuilder<VMScheduleVO> scheduleSearchByVmIdAndIds;
private final SearchBuilder<VMScheduleVO> scheduleSearch;
public VMScheduleDaoImpl() {
super();
activeScheduleSearch = createSearchBuilder();
activeScheduleSearch.and(ApiConstants.ENABLED, activeScheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
activeScheduleSearch.and().op(activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.NULL);
activeScheduleSearch.or(ApiConstants.END_DATE, activeScheduleSearch.entity().getEndDate(), SearchCriteria.Op.GT);
activeScheduleSearch.cp();
activeScheduleSearch.done();
scheduleSearchByVmIdAndIds = createSearchBuilder();
scheduleSearchByVmIdAndIds.and(ApiConstants.ID, scheduleSearchByVmIdAndIds.entity().getId(), SearchCriteria.Op.IN);
scheduleSearchByVmIdAndIds.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearchByVmIdAndIds.entity().getVmId(), SearchCriteria.Op.EQ);
scheduleSearchByVmIdAndIds.done();
scheduleSearch = createSearchBuilder();
scheduleSearch.and(ApiConstants.ID, scheduleSearch.entity().getId(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.VIRTUAL_MACHINE_ID, scheduleSearch.entity().getVmId(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.ACTION, scheduleSearch.entity().getAction(), SearchCriteria.Op.EQ);
scheduleSearch.and(ApiConstants.ENABLED, scheduleSearch.entity().getEnabled(), SearchCriteria.Op.EQ);
scheduleSearch.done();
}
@Override
public List<VMScheduleVO> listAllActiveSchedules() {
// WHERE enabled = true AND (end_date IS NULL OR end_date > current_date)
SearchCriteria<VMScheduleVO> sc = activeScheduleSearch.create();
sc.setParameters(ApiConstants.ENABLED, true);
sc.setParameters(ApiConstants.END_DATE, new Date());
return search(sc, null);
}
@Override
public long removeSchedulesForVmIdAndIds(Long vmId, List<Long> ids) {
SearchCriteria<VMScheduleVO> sc = scheduleSearchByVmIdAndIds.create();
sc.setParameters(ApiConstants.ID, ids.toArray());
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
return remove(sc);
}
@Override
public Pair<List<VMScheduleVO>, Integer> searchAndCount(Long id, Long vmId, VMSchedule.Action action, Boolean enabled, Long offset, Long limit) {
SearchCriteria<VMScheduleVO> sc = scheduleSearch.create();
if (id != null) {
sc.setParameters(ApiConstants.ID, id);
}
if (enabled != null) {
sc.setParameters(ApiConstants.ENABLED, enabled);
}
if (action != null) {
sc.setParameters(ApiConstants.ACTION, action);
}
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
Filter filter = new Filter(VMScheduleVO.class, ApiConstants.ID, false, offset, limit);
return searchAndCount(sc, filter);
}
@Override
public SearchCriteria<VMScheduleVO> getSearchCriteriaForVMId(Long vmId) {
SearchCriteria<VMScheduleVO> sc = scheduleSearch.create();
sc.setParameters(ApiConstants.VIRTUAL_MACHINE_ID, vmId);
return sc;
}
}

View File

@ -108,6 +108,7 @@
<bean id="instanceGroupJoinDaoImpl" class="com.cloud.api.query.dao.InstanceGroupJoinDaoImpl" />
<bean id="managementServerJoinDaoImpl" class="com.cloud.api.query.dao.ManagementServerJoinDaoImpl" />
<bean id="instanceGroupVMMapDaoImpl" class="com.cloud.vm.dao.InstanceGroupVMMapDaoImpl" />
<bean id="vmIsoMapDaoImpl" class="com.cloud.vm.dao.VmIsoMapDaoImpl" />
<bean id="itWorkDaoImpl" class="com.cloud.vm.ItWorkDaoImpl" />
<bean id="lBHealthCheckPolicyDaoImpl" class="com.cloud.network.dao.LBHealthCheckPolicyDaoImpl" />
<bean id="lBStickinessPolicyDaoImpl" class="com.cloud.network.dao.LBStickinessPolicyDaoImpl" />
@ -284,8 +285,9 @@
<bean id="secondaryStorageHeuristicDaoImpl" class="org.apache.cloudstack.secstorage.dao.SecondaryStorageHeuristicDaoImpl" />
<bean id="heuristicRuleHelper" class="org.apache.cloudstack.storage.heuristics.HeuristicRuleHelper" />
<bean id="publicIpQuarantineDaoImpl" class="com.cloud.network.dao.PublicIpQuarantineDaoImpl" />
<bean id="VMScheduleDaoImpl" class="org.apache.cloudstack.vm.schedule.dao.VMScheduleDaoImpl" />
<bean id="VMScheduledJobDaoImpl" class="org.apache.cloudstack.vm.schedule.dao.VMScheduledJobDaoImpl" />
<bean id="ResourceScheduleDaoImpl" class="org.apache.cloudstack.schedule.dao.ResourceScheduleDaoImpl" />
<bean id="ResourceScheduleDetailsDaoImpl" class="org.apache.cloudstack.schedule.dao.ResourceScheduleDetailsDaoImpl" />
<bean id="ResourceScheduledJobDaoImpl" class="org.apache.cloudstack.schedule.dao.ResourceScheduledJobDaoImpl" />
<bean id="VmwareDatacenterDaoImpl" class="com.cloud.hypervisor.vmware.dao.VmwareDatacenterDaoImpl" />
<bean id="vnfTemplateDetailsDaoImpl" class="com.cloud.storage.dao.VnfTemplateDetailsDaoImpl" />
<bean id="vnfTemplateNicDaoImpl" class="com.cloud.storage.dao.VnfTemplateNicDaoImpl" />

View File

@ -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,
@ -128,6 +136,20 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` (
CONSTRAINT `fk_quota_tariff_usage__tariff_id` FOREIGN KEY (`tariff_id`) REFERENCES `cloud_usage`.`quota_tariff` (`id`),
CONSTRAINT `fk_quota_tariff_usage__quota_usage_id` FOREIGN KEY (`quota_usage_id`) REFERENCES `cloud_usage`.`quota_usage` (`id`));
--- Per-VM ISO attachments. user_vm.iso_id remains as the primary/bootable ISO pointer.
CREATE TABLE IF NOT EXISTS `cloud`.`vm_iso_map` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`vm_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to user_vm',
`iso_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to vm_template (ISOs are templates of format ISO)',
`device_seq` int(10) unsigned NOT NULL COMMENT 'cdrom slot index used to derive the libvirt device label (3=hdc, 4=hdd)',
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uc_vm_iso_map__vm_iso` (`vm_id`, `iso_id`),
UNIQUE KEY `uc_vm_iso_map__vm_seq` (`vm_id`, `device_seq`),
CONSTRAINT `fk_vm_iso_map__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `cloud`.`user_vm` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_vm_iso_map__iso_id` FOREIGN KEY (`iso_id`) REFERENCES `cloud`.`vm_template` (`id`)
);
-- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
@ -150,6 +172,61 @@ FROM `cloud`.`configuration` `cfg`
WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`configuration` WHERE `name` = 'kvm.cpu.dynamic.scaling.capacity')
AND `cfg`.`name` = 'vm.serviceoffering.cpu.cores.max';
-- Generalise VM schedule tables into resource-agnostic resource_schedule / resource_scheduled_job.
-- Step 1: rename vm_schedule → resource_schedule, rename vm_id → resource_id, add resource_type column.
ALTER TABLE `cloud`.`vm_schedule`
DROP FOREIGN KEY `fk_vm_schedule__vm_id`,
DROP INDEX `i_vm_schedule__vm_id`,
DROP INDEX `i_vm_schedule__enabled_end_date`,
CHANGE COLUMN `vm_id` `resource_id` bigint unsigned NOT NULL COMMENT 'id of the scheduled resource',
ADD COLUMN `resource_type` varchar(64) NOT NULL DEFAULT 'VirtualMachine' COMMENT 'type of the scheduled resource' AFTER `uuid`;
RENAME TABLE `cloud`.`vm_schedule` TO `cloud`.`resource_schedule`;
ALTER TABLE `cloud`.`resource_schedule`
ADD INDEX `i_resource_schedule__resource` (`resource_type`, `resource_id`),
ADD INDEX `i_resource_schedule__enabled_end_date` (`enabled`, `end_date`);
-- Step 2: rename vm_scheduled_job → resource_scheduled_job, rename columns.
ALTER TABLE `cloud`.`vm_scheduled_job`
DROP FOREIGN KEY `fk_vm_scheduled_job__vm_id`,
DROP FOREIGN KEY `fk_vm_scheduled_job__vm_schedule_id`,
DROP INDEX `i_vm_scheduled_job__vm_id`,
DROP INDEX `i_vm_scheduled_job__scheduled_timestamp`,
DROP INDEX `vm_schedule_id`,
CHANGE COLUMN `vm_id` `resource_id` bigint unsigned NOT NULL COMMENT 'id of the scheduled resource',
CHANGE COLUMN `vm_schedule_id` `schedule_id` bigint unsigned NOT NULL COMMENT 'id of the resource_schedule row',
ADD COLUMN `resource_type` varchar(64) NOT NULL DEFAULT 'VirtualMachine' COMMENT 'type of the scheduled resource' AFTER `uuid`;
RENAME TABLE `cloud`.`vm_scheduled_job` TO `cloud`.`resource_scheduled_job`;
ALTER TABLE `cloud`.`resource_scheduled_job`
ADD UNIQUE KEY `uc_resource_scheduled_job__schedule_timestamp` (`schedule_id`, `scheduled_timestamp`),
ADD INDEX `i_resource_scheduled_job__resource` (`resource_type`, `resource_id`),
ADD INDEX `i_resource_scheduled_job__scheduled_timestamp` (`scheduled_timestamp`),
ADD CONSTRAINT `fk_resource_scheduled_job__schedule_id` FOREIGN KEY (`schedule_id`) REFERENCES `resource_schedule`(`id`) ON DELETE CASCADE;
-- Step 3: details table for action-specific parameters (used by the generic resource schedule API in Commit 2).
CREATE TABLE IF NOT EXISTS `cloud`.`resource_schedule_details` (
`id` bigint unsigned NOT NULL auto_increment,
`schedule_id` bigint unsigned NOT NULL COMMENT 'id of the resource_schedule row',
`name` varchar(255) NOT NULL,
`value` varchar(1024) NOT NULL,
`display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'should this detail be visible to the end user',
PRIMARY KEY (`id`),
INDEX `i_resource_schedule_details__schedule_id` (`schedule_id`),
CONSTRAINT `fk_resource_schedule_details__schedule_id` FOREIGN KEY (`schedule_id`) REFERENCES `resource_schedule`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Step 4: rename CRUD event types from VM.SCHEDULE.{CREATE,UPDATE,DELETE} to the new generic SCHEDULE.{CREATE,UPDATE,DELETE}.
-- Action-execution events (VM.SCHEDULE.START, .STOP, .REBOOT, .FORCE_STOP, .FORCE_REBOOT) keep their existing names.
UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.CREATE' WHERE `type` = 'VM.SCHEDULE.CREATE';
UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.UPDATE' WHERE `type` = 'VM.SCHEDULE.UPDATE';
UPDATE `cloud`.`event` SET `type` = 'SCHEDULE.DELETE' WHERE `type` = 'VM.SCHEDULE.DELETE';
-- Step 5: Rename the global configuration key for the scheduler
UPDATE `cloud`.`configuration` SET name='scheduler.jobs.expire.interval' WHERE name='vmscheduler.jobs.expire.interval';
-- Remove stale realhostip.com default values; domain has been dead since ~2015.
UPDATE `cloud`.`configuration`
SET value = NULL
@ -208,3 +285,6 @@ 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');

View File

@ -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,

View File

@ -0,0 +1,41 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.vm;
import org.junit.Assert;
import org.junit.Test;
public class VmIsoMapVOTest {
@Test
public void testFullConstructorPopulatesAllFields() {
VmIsoMapVO row = new VmIsoMapVO(7L, 42L, 4);
Assert.assertEquals(7L, row.getVmId());
Assert.assertEquals(42L, row.getIsoId());
Assert.assertEquals(4, row.getDeviceSeq());
Assert.assertNotNull(row.getCreated());
}
@Test
public void testNoArgConstructorLeavesNonIdFieldsAtDefaults() {
VmIsoMapVO row = new VmIsoMapVO();
Assert.assertEquals(0L, row.getVmId());
Assert.assertEquals(0L, row.getIsoId());
Assert.assertEquals(0, row.getDeviceSeq());
Assert.assertNull(row.getCreated());
}
}

View File

@ -27,6 +27,7 @@ import java.util.Objects;
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.ScopeType;
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 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
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 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
}
}
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 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
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();

View File

@ -144,12 +144,16 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot
}
/**
* Configures a {@link MigrateDiskInfo} object configured for migrating a File System volume and calls rootImageProvisioning.
* Configures a {@link MigrateDiskInfo} object configured for migrating a File System volume.
*/
@Override
protected MigrateCommand.MigrateDiskInfo configureMigrateDiskInfo(VolumeInfo srcVolumeInfo, String destPath, String backingPath) {
return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(), MigrateCommand.MigrateDiskInfo.DiskType.FILE, MigrateCommand.MigrateDiskInfo.DriverType.QCOW2,
MigrateCommand.MigrateDiskInfo.Source.FILE, destPath, backingPath);
return new MigrateCommand.MigrateDiskInfo(srcVolumeInfo.getPath(),
MigrateCommand.MigrateDiskInfo.DiskType.FILE,
MigrateCommand.MigrateDiskInfo.DriverType.QCOW2,
MigrateCommand.MigrateDiskInfo.Source.FILE,
destPath,
backingPath);
}
/**
@ -158,6 +162,17 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot
*/
@Override
protected String generateDestPath(Host destHost, StoragePoolVO destStoragePool, VolumeInfo destVolumeInfo) {
if (destStoragePool.getPoolType() == StoragePoolType.CLVM || destStoragePool.getPoolType() == StoragePoolType.CLVM_NG) {
String vgName = destStoragePool.getPath();
if (StringUtils.isBlank(vgName)) {
throw new CloudRuntimeException(String.format("CLVM/CLVM_NG destination pool [%s] has empty VG path", destStoragePool.getUuid()));
}
if (vgName.startsWith("/")) {
vgName = vgName.substring(1);
}
return String.format("/dev/%s/%s", vgName, destVolumeInfo.getUuid());
}
return new File(destStoragePool.getPath(), destVolumeInfo.getUuid()).getAbsolutePath();
}
@ -285,6 +300,7 @@ public class KvmNonManagedStorageDataMotionStrategy extends StorageSystemDataMot
}
protected Boolean supportStoragePoolType(StoragePoolType storagePoolType) {
return super.supportStoragePoolType(storagePoolType, StoragePoolType.Filesystem);
return super.supportStoragePoolType(storagePoolType, StoragePoolType.Filesystem,
StoragePoolType.CLVM, StoragePoolType.CLVM_NG);
}
}

View File

@ -36,6 +36,8 @@ import com.cloud.agent.api.CheckVirtualMachineAnswer;
import com.cloud.agent.api.CheckVirtualMachineCommand;
import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.resource.ResourceManager;
import com.cloud.storage.clvm.ClvmPoolManager;
import org.apache.cloudstack.storage.clvm.command.ClvmLockTransferCommand;
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
@ -87,6 +89,7 @@ import com.cloud.agent.api.MigrateCommand;
import com.cloud.agent.api.MigrateCommand.MigrateDiskInfo;
import com.cloud.agent.api.ModifyTargetsAnswer;
import com.cloud.agent.api.ModifyTargetsCommand;
import com.cloud.agent.api.PreMigrationCommand;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.storage.CopyVolumeAnswer;
import com.cloud.agent.api.storage.CopyVolumeCommand;
@ -206,6 +209,8 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
private VolumeDataFactory _volFactory;
@Inject
ResourceManager resourceManager;
@Inject
private ClvmPoolManager clvmPoolManager;
@Override
public StrategyPriority canHandle(DataObject srcData, DataObject destData) {
@ -2023,6 +2028,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
String errMsg = null;
boolean success = false;
Map<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo = new HashMap<>();
List<VolumeInfo> samePoolClvmVolumes = new ArrayList<>();
try {
if (srcHost.getHypervisorType() != HypervisorType.KVM) {
@ -2052,6 +2058,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
} 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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
srcVolumeInfoToDestVolumeInfo.put(srcVolumeInfo, destVolumeInfo);
}
prepareDisksForMigrationForClvm(vmTO, volumeDataStoreMap, srcHost);
PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO);
Answer pfma;
@ -2132,6 +2157,25 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 class StorageSystemDataMotionStrategy implements DataMotionStrategy {
}
}
handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, destHost);
handlePostMigration(success, srcVolumeInfoToDestVolumeInfo, vmTO, srcHost, destHost);
if (!success) {
if (migrateAnswer == null) {
@ -2211,10 +2258,43 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
}
}
private void prepareDisksForMigrationForClvm(VirtualMachineTO vmTO, Map<VolumeInfo, DataStore> 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 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
return null;
}
private void handlePostMigration(boolean success, Map<VolumeInfo, VolumeInfo> 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<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo, VirtualMachineTO vmTO, Host srcHost, Host destHost) {
if (!success) {
try {
PrepareForMigrationCommand pfmc = new PrepareForMigrationCommand(vmTO);
@ -2339,6 +2475,17 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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<VolumeInfo, VolumeInfo> entry : srcVolumeInfoToDestVolumeInfo.entrySet()) {
@ -2349,12 +2496,13 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
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 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
}
_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);

View File

@ -28,14 +28,18 @@ import static org.mockito.Mockito.verify;
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 @@ public class AncientDataMotionStrategyTest {
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 class AncientDataMotionStrategyTest {
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());
}
}

View File

@ -168,6 +168,8 @@ public class KvmNonManagedStorageSystemDataMotionTest {
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 class KvmNonManagedStorageSystemDataMotionTest {
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);

View File

@ -369,4 +369,286 @@ public class StorageSystemDataMotionStrategyTest {
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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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<VolumeInfo, DataStore> 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);
}
}

View File

@ -60,6 +60,7 @@ import com.cloud.event.UsageEventUtils;
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 class DefaultSnapshotStrategy extends SnapshotStrategyBase {
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 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase {
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<SnapshotDataStoreVO> 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()));
}
}

Some files were not shown because too many files have changed in this diff Show More